mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 15:43:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
919c2ffd46 | ||
|
|
e6d63a811e | ||
|
|
92227098b7 | ||
|
|
571080c1d6 | ||
|
|
d07feb837d | ||
|
|
63e378524b | ||
|
|
ea5f54e421 | ||
|
|
72f7514114 | ||
|
|
44e1f00ffc | ||
|
|
4a3ac2cfcd | ||
|
|
bbc31e01c4 | ||
|
|
770a198772 | ||
|
|
a26bee3696 | ||
|
|
abb8f4f963 | ||
|
|
43d2adff70 | ||
|
|
e2dffea423 | ||
|
|
3d5eaba46f | ||
|
|
1bb8fbd56f | ||
|
|
b699886953 | ||
|
|
12c957d48b | ||
|
|
48e890c1b1 | ||
|
|
05331f001a | ||
|
|
aa77bfdfc4 | ||
|
|
a2baef8911 | ||
|
|
3fcfb65d45 | ||
|
|
8f266bea0d | ||
|
|
0a765f4aac | ||
|
|
ee3f0555d1 | ||
|
|
9fc1285c09 | ||
|
|
e6e8ccee50 | ||
|
|
f9632af016 | ||
|
|
81359bf419 | ||
|
|
a144570ed7 | ||
|
|
0732b72e8f | ||
|
|
8332445a52 | ||
|
|
c9111d1bfc | ||
|
|
e7d999278d | ||
|
|
4043d20ea6 | ||
|
|
afe88ccfd6 | ||
|
|
56c3a04809 | ||
|
|
4530345a1f | ||
|
|
4a89ec8515 | ||
|
|
cb5f1c6485 | ||
|
|
15c0677a4b | ||
|
|
dc68b8148a | ||
|
|
1976034f12 | ||
|
|
727621fdac | ||
|
|
0f3806d358 | ||
|
|
dfb2bf3923 | ||
|
|
bf08a91191 | ||
|
|
f0e353cc1a | ||
|
|
456b1eddcc | ||
|
|
5e08f05437 | ||
|
|
9f9227bae4 | ||
|
|
76f47bce37 | ||
|
|
e1f54b30dc | ||
|
|
fc4e7e6c05 | ||
|
|
7aaa79b644 | ||
|
|
9b6f894be2 | ||
|
|
48f7ef2023 | ||
|
|
54692859fd | ||
|
|
1719244015 | ||
|
|
48f8c6c5b7 | ||
|
|
f0815e595b | ||
|
|
4f829017ed | ||
|
|
d2f8f05acb | ||
|
|
e8b1156bfd | ||
|
|
bf748773c8 | ||
|
|
a05b25ae90 | ||
|
|
3ceb3feccf | ||
|
|
b4944f23ef | ||
|
|
09c17b2dc5 | ||
|
|
6f7ff9f858 | ||
|
|
9e9af2a031 | ||
|
|
7d74e853ae | ||
|
|
1d627d570d | ||
|
|
9b0088f4f0 | ||
|
|
b50c7dbed4 | ||
|
|
f92c0313a6 | ||
|
|
233e1ee0a6 | ||
|
|
805b40ec76 | ||
|
|
86ca3e5be0 | ||
|
|
bb2143d9a0 | ||
|
|
ef662e0ccf | ||
|
|
30708a59f1 | ||
|
|
b0262cfe72 | ||
|
|
259886c1a8 | ||
|
|
f1b4b33727 | ||
|
|
38e8b690bd | ||
|
|
9a52b43e09 | ||
|
|
5e35cfc44a | ||
|
|
ac0ec6dfe6 | ||
|
|
361defc348 | ||
|
|
b9fc0da215 | ||
|
|
6ac8947168 | ||
|
|
5655930d12 | ||
|
|
18acc37324 | ||
|
|
e9155d719e | ||
|
|
16e77b02c2 | ||
|
|
adc1902649 | ||
|
|
1cc470bc5a | ||
|
|
b3f6efc7d0 | ||
|
|
d91fbf3ff4 | ||
|
|
e552593474 | ||
|
|
1f5a78f458 | ||
|
|
f490c15282 | ||
|
|
90a31debd1 | ||
|
|
416145a90e | ||
|
|
6285786282 | ||
|
|
f9f6a69c89 | ||
|
|
61da60bdf1 | ||
|
|
2d5ef3011a | ||
|
|
63dab6d224 | ||
|
|
38dd76dfb3 | ||
|
|
fe642de526 | ||
|
|
f4b7eefc38 | ||
|
|
5bbbad02b4 | ||
|
|
bd85dd5faf | ||
|
|
d91ba3d219 | ||
|
|
7992c40815 | ||
|
|
574b01db97 | ||
|
|
2d6049b7bd | ||
|
|
96d0f2e6f9 | ||
|
|
b46bd4d0dd | ||
|
|
244c76793c | ||
|
|
411fc46189 | ||
|
|
48427e330f | ||
|
|
5ddc2451c1 | ||
|
|
e357cad2bb | ||
|
|
80be8b2eaa | ||
|
|
9a5aaea8ca | ||
|
|
7d585615a9 | ||
|
|
3f3375dcac | ||
|
|
f555eae653 | ||
|
|
e805a6e037 | ||
|
|
3ed82ed7bb | ||
|
|
d638e512c9 | ||
|
|
204b966935 | ||
|
|
8318ba5e55 | ||
|
|
d39c595828 | ||
|
|
04154162c6 | ||
|
|
47f8e0f04c | ||
|
|
7e4ada5f4b | ||
|
|
b0b4f87848 | ||
|
|
d9085b82bf | ||
|
|
8e5770ff59 | ||
|
|
beb420c69d | ||
|
|
1d8b898924 | ||
|
|
b5b4c5ced7 | ||
|
|
b4e9d77895 | ||
|
|
21a08b3c7a | ||
|
|
6977d6bf73 | ||
|
|
39f81dd0c0 | ||
|
|
b3191f5e0b | ||
|
|
78f2f21142 | ||
|
|
497fc1d1ae | ||
|
|
d83e1c6fb2 | ||
|
|
c5218d485a | ||
|
|
19cbd3b8da | ||
|
|
c0c5630bfb | ||
|
|
969b5fcfbb | ||
|
|
0739fa2195 | ||
|
|
3c62adab3c | ||
|
|
edc601c35e | ||
|
|
951370eee7 | ||
|
|
fa81f4a150 | ||
|
|
e6a90c40fb | ||
|
|
2e0a1c00fa | ||
|
|
109754fac9 | ||
|
|
c3fbff3d7a | ||
|
|
6dd967fe24 | ||
|
|
f0bfdba944 | ||
|
|
7f76b5f4ee | ||
|
|
35f4d8f15a | ||
|
|
783f17e9e8 | ||
|
|
9e305c2840 | ||
|
|
78fa5d3110 | ||
|
|
d57482d966 | ||
|
|
3d7c2aff4b | ||
|
|
3757c5dde7 | ||
|
|
13c0a966ec | ||
|
|
d987619946 | ||
|
|
8fb9498fe6 | ||
|
|
b8f64a6693 | ||
|
|
e31bb047bf | ||
|
|
6897b813a6 | ||
|
|
83f86406b5 | ||
|
|
dde237ff0d | ||
|
|
6a4887e252 | ||
|
|
d945b94910 | ||
|
|
bc7dcf791d | ||
|
|
66c71fe3b7 | ||
|
|
16a7aeec3b | ||
|
|
94605345e5 | ||
|
|
ec9fa32c81 | ||
|
|
264aa15281 | ||
|
|
180a6ff824 | ||
|
|
5904ee1571 | ||
|
|
bc99341f85 | ||
|
|
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. 比如界面反应
|
||||
|
||||
## 📋 其他信息
|
||||
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
||||
|
||||
@@ -164,3 +164,5 @@ main/manager-api/.vscode
|
||||
# Ignore webpack cache directory
|
||||
main/manager-web/.webpack_cache/
|
||||
main/xiaozhi-server/mysql
|
||||
uploadfile
|
||||
.vscode
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ RUN apt-get update && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 配置Nginx
|
||||
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# 复制前端构建产物
|
||||
COPY --from=web-builder /app/dist /usr/share/nginx/html
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
|
||||
|
||||
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
||||
|
||||
<p align="center">
|
||||
本项目为开源智能硬件项目
|
||||
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
||||
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python实现<br/>
|
||||
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
|
||||
帮助您快速搭建小智服务器
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">English</a>
|
||||
· 简体中文
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</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/issues ">反馈问题</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">
|
||||
@@ -131,22 +130,14 @@
|
||||
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
||||
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
||||
|
||||
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件
|
||||
`config.yaml` 中开启防护:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
auth:
|
||||
# 开启防护
|
||||
enabled: true
|
||||
```
|
||||
|
||||
开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。
|
||||
2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。
|
||||
|
||||
---
|
||||
|
||||
## 部署文档
|
||||
|
||||

|
||||
|
||||
本项目提供两种部署方式,请根据您的具体需求选择:
|
||||
|
||||
#### 🚀 部署方式选择
|
||||
@@ -160,14 +151,11 @@ server:
|
||||
|
||||
```
|
||||
智控台地址: https://2662r3426b.vicp.fun
|
||||
|
||||
服务测试工具: https://2662r3426b.vicp.fun/test/
|
||||
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
```
|
||||
---
|
||||
|
||||
## 常见问题 ❓
|
||||
|
||||
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
||||
|
||||
---
|
||||
## 功能清单 ✨
|
||||
@@ -243,7 +231,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
| 使用方式 | 支持平台 | 免费平台 |
|
||||
|:---:|:---:|:---:|
|
||||
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
||||
| 接口调用 | DoubaoASR | - |
|
||||
| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+30
-32
@@ -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固件`需要用到。
|
||||
|
||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||
|
||||
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||
|
||||
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||
|
||||
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
+39
-13
@@ -122,11 +122,11 @@ http://localhost:8002/xiaozhi/doc.html
|
||||
|
||||
这时,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8002 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
||||
|
||||
接下来要做两件重要的事情:
|
||||
接下来要做三件重要的事情:
|
||||
|
||||
### 第一件重要的事情
|
||||
|
||||
使用超级管理员账号,登录系统,在顶部菜单找到`参数管理`,找到列表中第三条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到列表中第一条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||
|
||||
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||
|
||||
@@ -138,7 +138,12 @@ manager-api:
|
||||
secret: 你的server.secret值
|
||||
```
|
||||
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
||||
2、注意,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||
|
||||
2、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||
|
||||
3、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||
|
||||
4、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||
|
||||
类似这样的效果
|
||||
```
|
||||
@@ -151,7 +156,7 @@ manager-api:
|
||||
|
||||
### 第二件重要的事情
|
||||
|
||||
使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||
使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||
|
||||
## 5.重启xiaozhi-esp32-server
|
||||
@@ -164,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 - =======================================================
|
||||
@@ -182,7 +187,17 @@ Websocket接口:
|
||||
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||
```
|
||||
|
||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||
### 第三件重要的事情
|
||||
|
||||
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`。
|
||||
|
||||
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`。
|
||||
|
||||
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||
|
||||
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||
|
||||
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||
|
||||
|
||||
# 方式二:本地源码运行全模块
|
||||
@@ -198,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
|
||||
@@ -278,13 +293,13 @@ npm run serve
|
||||
运行成功后,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8001 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
||||
|
||||
|
||||
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||
|
||||
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||
|
||||
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||
|
||||
## 5.安装Python环境
|
||||
@@ -349,7 +364,7 @@ pip install -r requirements.txt
|
||||
|
||||
## 8.配置项目文件
|
||||
|
||||
使用超级管理员账号,登录智控台 http://127.0.0.1:8001 ,在顶部菜单找到`参数管理`,找到列表中第三条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||
使用超级管理员账号,登录智控台 ,在顶部菜单找到`参数管理`,找到列表中第一条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||
|
||||
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||
|
||||
@@ -390,7 +405,7 @@ python app.py
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||
```
|
||||
|
||||
由于你是全模块部署,因此你有两个重要的接口需要写入到esp32中。
|
||||
由于你是全模块部署,因此你有两个重要的接口。
|
||||
|
||||
OTA接口:
|
||||
```
|
||||
@@ -402,7 +417,18 @@ Websocket接口:
|
||||
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||
```
|
||||
|
||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||
请你务必把以上两个接口地址写入到智控台中:他们将会影响websocket地址发放和自动升级功能。
|
||||
|
||||
1、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`。
|
||||
|
||||
2、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`。
|
||||
|
||||
|
||||
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||
|
||||
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||
|
||||
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||
|
||||
# 常见问题
|
||||
|
||||
|
||||
+3
-41
@@ -102,48 +102,10 @@ VAD:
|
||||
|
||||
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||
|
||||
本项目,支持以工具调用的方式控制HomeAssistant设备
|
||||
|
||||
1、首先选择一款支持function call支持的LLM,例如`ChatGLMLLM`。
|
||||
|
||||
2、在配置文件中,将 `selected_module.Intent` 设置为 `function_call`。
|
||||
|
||||
3、登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
||||
|
||||
在配置文件中,配置好你的home assistant的`devices`(被控制的设备)和`api_key`和`base_url`等信息。例如:
|
||||
|
||||
``` yaml
|
||||
plugins
|
||||
home_assistant:
|
||||
devices:
|
||||
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
|
||||
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
|
||||
base_url: http://你的homeassistant地址:8123
|
||||
api_key: 你的home assistant api访问令牌
|
||||
```
|
||||
|
||||
最后,允许function_call 插件在配置文件中启用`hass_get_state`(必须)、`hass_set_state`(必须)、`hass_play_music`(不想用ha听音乐可以不启动),例如:
|
||||
|
||||
``` yaml
|
||||
Intent:
|
||||
...
|
||||
function_call:
|
||||
type: nointent
|
||||
functions:
|
||||
- change_role
|
||||
- get_weather
|
||||
- get_news
|
||||
- hass_get_state
|
||||
- hass_set_state
|
||||
- hass_play_music
|
||||
```
|
||||
参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md)
|
||||
|
||||
### 8、更多问题,可联系我们反馈 💬
|
||||
|
||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
||||
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
|
||||
|
||||
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
|
||||
|
||||
- 硬件烧录QQ群:适用于硬件烧录问题
|
||||
- 开源服务端交流群:适用于服务端问题
|
||||
- 产品建议联系人:适用于产品功能、产品设计等建议
|
||||
也可以发邮件我们取得联系:huangrongzhuang@xin-nan.com
|
||||
+47
-20
@@ -1,26 +1,53 @@
|
||||
server {
|
||||
listen 8002;
|
||||
server_name localhost;
|
||||
user root;
|
||||
worker_processes 4;
|
||||
|
||||
# 静态资源服务(Vue项目)
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
# API反向代理(Java项目)
|
||||
location /xiaozhi/ {
|
||||
proxy_pass http://127.0.0.1:8003;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cookie_path /api/ /;
|
||||
proxy_set_header Referer $http_referer;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
keepalive_timeout 300;
|
||||
client_header_timeout 180s;
|
||||
client_body_timeout 180s;
|
||||
client_max_body_size 1024M;
|
||||
|
||||
proxy_connect_timeout 10;
|
||||
proxy_send_timeout 10;
|
||||
proxy_read_timeout 10;
|
||||
gzip on;
|
||||
gzip_buffers 32 4K;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 100;
|
||||
gzip_types application/javascript text/css text/xml image/jpeg image/gif image/png;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
gzip_vary on;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
server {
|
||||
# 无域名访问,就用localhost
|
||||
server_name localhost;
|
||||
# 80端口
|
||||
listen 8002;
|
||||
|
||||
# 转发到编译后到web目录
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 转发到manager-api
|
||||
location /xiaozhi/ {
|
||||
proxy_pass http://127.0.0.1:8003;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cookie_path /manager/ /;
|
||||
proxy_set_header Referer $http_referer;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
|
||||
proxy_connect_timeout 10;
|
||||
proxy_send_timeout 10;
|
||||
proxy_read_timeout 10;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
-59
@@ -1,67 +1,79 @@
|
||||
# 编译esp32固件
|
||||
# esp32固件编译
|
||||
|
||||
## 第1步 配置环境
|
||||
## 第1步 准备你的ota地址
|
||||
|
||||
如果你,使用的是本项目0.3.12版本,不管是简单Server部署还是全模块部署,都会有ota地址。
|
||||
|
||||
由于简单Server部署和全模块部署的OTA地址设置方式不一样,请你选择下面的具体方式:
|
||||
|
||||
### 如果你用的是简单Server部署
|
||||
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||
```
|
||||
http://192.168.1.25:8002/xiaozhi/ota/
|
||||
```
|
||||
如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/
|
||||
|
||||
你可以使用项目自带的`test_page.html`测试一下,是否能连上ota页面输出的websocket地址。
|
||||
|
||||
如果访问不到,你需要到配置文件`.config.yaml`里修改`server.websocket`的地址,重启后再重新测试,直到`test_page.html`能正常访问。
|
||||
|
||||
成功后,请往下进行第2步
|
||||
|
||||
### 如果你用的是全模块部署
|
||||
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||
```
|
||||
http://192.168.1.25:8002/xiaozhi/ota/
|
||||
```
|
||||
|
||||
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下进行2步。
|
||||
|
||||
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
|
||||
|
||||
- 1、使用超级管理员登录智控台
|
||||
|
||||
- 2、顶部菜单点击`参数管理`
|
||||
|
||||
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
|
||||
|
||||
```
|
||||
ws://192.168.1.25:8000/xiaozhi/v1/
|
||||
```
|
||||
|
||||
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
|
||||
|
||||
## 第2步 配置环境
|
||||
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||
|
||||
## 第2步 打开配置文件
|
||||
配置好编译环境后,下载虾哥[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)项目源码,进入虾哥项目,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
||||
## 第3步 打开配置文件
|
||||
配置好编译环境后,下载虾哥iaozhi-esp32项目源码,
|
||||
|
||||
## 第3步 修改WEBSOCKET地址
|
||||
找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net/xiaozhi/v1/`
|
||||
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000/xiaozhi/v1/`,就把内容改成这个。
|
||||
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32)。
|
||||
|
||||
修改前:
|
||||
|
||||
```
|
||||
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://`开头,一定不要写错了。
|
||||
下载后,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
||||
|
||||
## 第4步 修改OTA地址
|
||||
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
|
||||
|
||||
找到`OTA_VERSION_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
||||
找到`OTA_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
||||
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。
|
||||
|
||||
修改前:
|
||||
```
|
||||
config OTA_VERSION_URL
|
||||
string "OTA Version URL"
|
||||
config OTA_URL
|
||||
string "Default OTA URL"
|
||||
default "https://api.tenclass.net/xiaozhi/ota/"
|
||||
help
|
||||
The application will access this URL to check for updates.
|
||||
The application will access this URL to check for new firmwares and server address.
|
||||
```
|
||||
修改后:
|
||||
```
|
||||
config OTA_VERSION_URL
|
||||
string "OTA Version URL"
|
||||
config OTA_URL
|
||||
string "Default OTA URL"
|
||||
default "http://192.168.1.25:8002/xiaozhi/ota/"
|
||||
help
|
||||
The application will access this URL to check for updates.
|
||||
The application will access this URL to check for new firmwares and server address.
|
||||
```
|
||||
|
||||
## 第5步 设置编译参数
|
||||
## 第4步 设置编译参数
|
||||
|
||||
设置编译参数
|
||||
|
||||
@@ -74,40 +86,29 @@ idf.py set-target esp32s3
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||

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

|
||||
|
||||
## 第6步 编译固件
|
||||
## 第5步 编译固件
|
||||
|
||||
```
|
||||
idf.py build
|
||||
```
|
||||
|
||||
如果是vscode安装的idf可以使用`F1`或者`ctrl+shift+p`,输入idf然后直接选择进行编译
|
||||
|
||||
还可以直接进行烧录不用接下来的操作
|
||||
|
||||
<img src="./images/vscode_idf.png" width="500px"/>
|
||||
|
||||
## 第7步 打包bin固件
|
||||
## 第6步 打包bin固件
|
||||
|
||||
```
|
||||
cd scripts
|
||||
python release.py
|
||||
```
|
||||
|
||||
编译成功后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`。
|
||||
上面的打包命令执行完成后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`。
|
||||
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
|
||||
|
||||
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
|
||||
,对你没有太大影响,请继续。
|
||||
|
||||
## 第8步 烧录固件
|
||||
## 第7步 烧录固件
|
||||
将esp32设备连接电脑,使用chrome浏览器,打开以下网址
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# 基于虾哥编译好的固件配置自定义服务器
|
||||
|
||||
## 第1步 确认版本
|
||||
烧录虾哥已经编译好的1.6.1版本固件
|
||||
|
||||
## 第2步 准备你的ota地址
|
||||
如果你按照教程使用的是全模块部署,就应该会有ota地址。
|
||||
|
||||
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||
```
|
||||
https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||
```
|
||||
|
||||
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下。
|
||||
|
||||
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
|
||||
|
||||
- 1、使用超级管理员登录智控台
|
||||
|
||||
- 2、顶部菜单点击`参数管理`
|
||||
|
||||
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
|
||||
|
||||
```
|
||||
wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
```
|
||||
|
||||
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
|
||||
|
||||
## 第3步 进入配网模式
|
||||
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
|
||||
|
||||
## 第4步 唤醒小智,查看日志输出
|
||||
|
||||
唤醒小智,看看日志是不是正常输出。
|
||||
|
||||
|
||||
## 常见问题
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||
|
||||
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||
|
||||
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||
|
||||
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||
|
||||
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||
|
||||
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||
@@ -0,0 +1,111 @@
|
||||
# ESP32设备与HomeAssistant集成指南
|
||||
|
||||
## 简介
|
||||
|
||||
本文档将指导您如何将ESP32设备与HomeAssistant进行集成。
|
||||
|
||||
## 前提条件
|
||||
|
||||
- 已安装并配置好`HomeAssistant`
|
||||
- 已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`
|
||||
- `xiaozhi-esp32-server智控台`能正常使用
|
||||
- 本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用
|
||||
|
||||
## 网络环境
|
||||
|
||||
1、我的`HomeAssistant`部署在下面,版本是`2025.3.4`
|
||||
```
|
||||
http://192.168.4.7:8123
|
||||
```
|
||||
|
||||
2、我的`xiaozhi-esp32-server智控台`和`HomeAssistant`部署在同一台机器的另一个端口,版本是`0.3.10`
|
||||
```
|
||||
http://192.168.4.7:8002
|
||||
```
|
||||
|
||||
|
||||
## 配置步骤
|
||||
|
||||
### 1. 登录`HomeAssistant`整理需要控制的设备清单
|
||||
|
||||
登录`HomeAssistant`,点击`左下角的设置`,然后进入`设备与服务`,再点击顶部的`实体`。
|
||||
|
||||
然后在实体中搜索你相关控制的开关,结果出来后,在列表中,点击其中一个结果,这是会出现一个开关的界面。
|
||||
|
||||
在开关的界面,我们尝试点击开关,看看是开发会随着我们的点击开/关。如果能操作,说明是正常联网的。
|
||||
|
||||
接着在开关面板找到设置按钮,点击后,可以查看这个开关的`实体标识符`。
|
||||
|
||||
我们打开一个记事本,按照这样格式整理一条数据:
|
||||
|
||||
位置+英文逗号+设备名称+英文逗号+`实体标识符`+英文分号
|
||||
|
||||
例如,我在公司,我有一个玩具灯,他的标识符是switch.cuco_cn_460494544_cp1_on_p_2_1,那么就这个写这一条数据
|
||||
|
||||
```
|
||||
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
|
||||
```
|
||||
|
||||
当然最后我可能要操作两个灯,我的最终的结果是:
|
||||
|
||||
```
|
||||
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
|
||||
公司,台灯,switch.iot_cn_831898993_socn1_on_p_2_1;
|
||||
```
|
||||
|
||||
这段字符,我们成为“设备清单字符”需要保存好,等一下有用。
|
||||
|
||||
|
||||
### 2. 登录`HomeAssistant`拿到开发密钥
|
||||
|
||||
登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
||||
|
||||
|
||||
### 3. 登录`智控台`
|
||||
|
||||
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来
|
||||
|
||||
编辑`plugins.home_assistant.devices`,把刚才整理的设备清单字符粘贴进去。
|
||||
|
||||
|
||||
编辑`plugins.home_assistant.base_url`,把你部署的`HomeAssistant`接口地址粘贴进去,我粘贴进去的地址是这样的
|
||||
|
||||
```
|
||||
http://192.168.4.7:8123
|
||||
```
|
||||
|
||||
编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去
|
||||
|
||||
|
||||
### 4. 设置`意图识别`函数
|
||||
|
||||
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑
|
||||
|
||||
然后在弹框中,在原来的基础上追加两个函数:“hass_get_state”和“hass_set_state”
|
||||
|
||||
修改前
|
||||
```
|
||||
change_role;get_weather;get_news;play_music
|
||||
```
|
||||
|
||||
修改后
|
||||
|
||||
```
|
||||
change_role;get_weather;get_news;play_music;hass_get_state;hass_set_state
|
||||
```
|
||||
|
||||
### 6. 手动重启xiaozhi-server
|
||||
|
||||
重启xiaozhi-server程序
|
||||
|
||||
|
||||
### 5. 确认角色配置是否设置了函数意图识别
|
||||
|
||||
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
|
||||
|
||||
确认意图识别(Intent),是否选择“函数调用意图识别”
|
||||
|
||||
|
||||
### 6. 唤醒设别进行控制
|
||||
|
||||
尝试和esp32说,“打开XXX灯”
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 137 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB |
Binary file not shown.
|
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仓库 -->
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package xiaozhi.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* RestTemplate配置
|
||||
*/
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,16 @@ public interface Constant {
|
||||
*/
|
||||
String SERVER_SECRET = "server.secret";
|
||||
|
||||
/**
|
||||
* websocket地址
|
||||
*/
|
||||
String SERVER_WEBSOCKET = "server.websocket";
|
||||
|
||||
/**
|
||||
* ota地址
|
||||
*/
|
||||
String SERVER_OTA = "server.ota";
|
||||
|
||||
/**
|
||||
* 是否允许用户注册
|
||||
*/
|
||||
@@ -167,5 +177,5 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.3.8";
|
||||
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
|
||||
*/
|
||||
@@ -80,6 +87,34 @@ public class RedisKeys {
|
||||
* 获取版本号Key
|
||||
*/
|
||||
public static String getVersionKey() {
|
||||
return "system:version";
|
||||
return "sys:version";
|
||||
}
|
||||
|
||||
/**
|
||||
* OTA固件ID的Key
|
||||
*/
|
||||
public static String getOtaIdKey(String uuid) {
|
||||
return "ota:id:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* OTA固件下载次数的Key
|
||||
*/
|
||||
public static String getOtaDownloadCountKey(String uuid) {
|
||||
return "ota:download:count:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数据的缓存key
|
||||
*/
|
||||
public static String getDictDataByTypeKey(String dictType) {
|
||||
return "sys:dict:data:" + dictType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体音频ID的缓存key
|
||||
*/
|
||||
public static String getAgentAudioIdKey(String uuid) {
|
||||
return "agent:audio:id:" + uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
+85
-2
@@ -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;
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
+81
@@ -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;
|
||||
}
|
||||
+30
@@ -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);
|
||||
}
|
||||
+45
@@ -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);
|
||||
}
|
||||
|
||||
+22
@@ -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);
|
||||
}
|
||||
+88
@@ -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;
|
||||
}
|
||||
}
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
+85
@@ -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);
|
||||
}
|
||||
}
|
||||
+38
-16
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+3
-20
@@ -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地址")
|
||||
|
||||
+7
-20
@@ -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) {
|
||||
|
||||
// 查询所有系统参数
|
||||
@@ -291,7 +278,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
|
||||
result.put("selected_module", selectedModule);
|
||||
if (StringUtils.isNotBlank(prompt)) {
|
||||
prompt = prompt.replace("{{assistant_name}}", "小智");
|
||||
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
||||
}
|
||||
result.put("prompt", prompt);
|
||||
}
|
||||
|
||||
+13
@@ -7,6 +7,7 @@ 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.RestController;
|
||||
@@ -79,4 +80,16 @@ public class DeviceController {
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@PutMapping("/enableOta/{id}/{status}")
|
||||
@Operation(summary = "启用/关闭OTA自动升级")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> enableOtaUpgrade(@PathVariable String id, @PathVariable Integer status) {
|
||||
DeviceEntity entity = deviceService.selectById(id);
|
||||
if (entity == null) {
|
||||
return new Result<Void>().error("设备不存在");
|
||||
}
|
||||
entity.setAutoUpdate(status);
|
||||
deviceService.updateById(entity);
|
||||
return new Result<Void>();
|
||||
}
|
||||
}
|
||||
+50
-5
@@ -15,25 +15,31 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
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 相关接口")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/ota/")
|
||||
public class OTAController {
|
||||
private final DeviceService deviceService;
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
@Operation(summary = "检查 OTA 版本和设备激活状态")
|
||||
@Operation(summary = "OTA版本和设备激活状态检查")
|
||||
@PostMapping
|
||||
public ResponseEntity<String> checkOTAVersion(
|
||||
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||
@@ -46,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"));
|
||||
@@ -54,9 +60,33 @@ public class OTAController {
|
||||
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
|
||||
}
|
||||
|
||||
@Operation(summary = "设备快速检查激活状态")
|
||||
@PostMapping("activate")
|
||||
public ResponseEntity<String> activateDevice(
|
||||
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||
if (StringUtils.isBlank(deviceId)) {
|
||||
return ResponseEntity.status(202).build();
|
||||
}
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(deviceId);
|
||||
if (device == null) {
|
||||
return ResponseEntity.status(202).build();
|
||||
}
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<String> getOTAPrompt() {
|
||||
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
|
||||
@Hidden
|
||||
public ResponseEntity<String> getOTA() {
|
||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
}
|
||||
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||
return ResponseEntity.ok("OTA接口不正常,缺少ota地址,请登录智控台,在参数管理找到【server.ota】配置");
|
||||
}
|
||||
return ResponseEntity.ok("OTA接口运行正常,websocket集群数量:" + wsUrl.split(";").length);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@@ -71,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);
|
||||
}
|
||||
}
|
||||
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
package xiaozhi.modules.device.controller;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
import xiaozhi.modules.device.service.OtaService;
|
||||
|
||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/otaMag")
|
||||
public class OTAMagController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
|
||||
private final OtaService otaService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询 OTA 固件信息")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||
})
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<OtaEntity>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
ValidatorUtils.validateEntity(params);
|
||||
PageData<OtaEntity> page = otaService.page(params);
|
||||
return new Result<PageData<OtaEntity>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("{id}")
|
||||
@Operation(summary = "信息 OTA 固件信息")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<OtaEntity> get(@PathVariable("id") String id) {
|
||||
OtaEntity data = otaService.selectById(id);
|
||||
return new Result<OtaEntity>().ok(data);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "保存 OTA 固件信息")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> save(@RequestBody OtaEntity entity) {
|
||||
if (entity == null) {
|
||||
return new Result<Void>().error("固件信息不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getFirmwareName())) {
|
||||
return new Result<Void>().error("固件名称不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getType())) {
|
||||
return new Result<Void>().error("固件类型不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getVersion())) {
|
||||
return new Result<Void>().error("版本号不能为空");
|
||||
}
|
||||
try {
|
||||
otaService.save(entity);
|
||||
return new Result<Void>();
|
||||
} catch (RuntimeException e) {
|
||||
return new Result<Void>().error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "OTA 删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@PathVariable("id") String[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return new Result<Void>().error("删除的固件ID不能为空");
|
||||
}
|
||||
otaService.delete(ids);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "修改 OTA 固件信息")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<?> update(@PathVariable("id") String id, @RequestBody OtaEntity entity) {
|
||||
if (entity == null) {
|
||||
return new Result<>().error("固件信息不能为空");
|
||||
}
|
||||
entity.setId(id);
|
||||
try {
|
||||
otaService.update(entity);
|
||||
return new Result<>();
|
||||
} catch (RuntimeException e) {
|
||||
return new Result<>().error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/getDownloadUrl/{id}")
|
||||
@Operation(summary = "获取 OTA 固件下载链接")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<String> getDownloadUrl(@PathVariable("id") String id) {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getOtaIdKey(uuid), id);
|
||||
return new Result<String>().ok(uuid);
|
||||
}
|
||||
|
||||
@GetMapping("/download/{uuid}")
|
||||
@Operation(summary = "下载固件文件")
|
||||
public ResponseEntity<byte[]> downloadFirmware(@PathVariable("uuid") String uuid) {
|
||||
String id = (String) redisUtils.get(RedisKeys.getOtaIdKey(uuid));
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// 检查下载次数
|
||||
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
|
||||
Integer downloadCount = (Integer) redisUtils.get(downloadCountKey);
|
||||
if (downloadCount == null) {
|
||||
downloadCount = 0;
|
||||
}
|
||||
|
||||
// 如果下载次数超过3次,返回404
|
||||
if (downloadCount >= 3) {
|
||||
redisUtils.delete(downloadCountKey);
|
||||
redisUtils.delete(RedisKeys.getOtaIdKey(uuid));
|
||||
logger.warn("Download limit exceeded for UUID: {}", uuid);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
redisUtils.set(downloadCountKey, downloadCount + 1);
|
||||
|
||||
try {
|
||||
// 获取固件信息
|
||||
OtaEntity otaEntity = otaService.selectById(id);
|
||||
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
|
||||
logger.warn("Firmware not found or path is empty for ID: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
|
||||
String firmwarePath = otaEntity.getFirmwarePath();
|
||||
Path path;
|
||||
|
||||
// 检查是否是绝对路径
|
||||
if (Paths.get(firmwarePath).isAbsolute()) {
|
||||
path = Paths.get(firmwarePath);
|
||||
} else {
|
||||
// 如果是相对路径,则从当前工作目录解析
|
||||
path = Paths.get(System.getProperty("user.dir"), firmwarePath);
|
||||
}
|
||||
|
||||
logger.info("Attempting to download firmware for ID: {}, DB path: {}, resolved path: {}",
|
||||
id, firmwarePath, path.toAbsolutePath());
|
||||
|
||||
if (!Files.exists(path) || !Files.isRegularFile(path)) {
|
||||
// 尝试直接从firmware目录下查找文件名
|
||||
String fileName = new File(firmwarePath).getName();
|
||||
Path altPath = Paths.get(System.getProperty("user.dir"), "firmware", fileName);
|
||||
|
||||
logger.info("File not found at primary path, trying alternative path: {}", altPath.toAbsolutePath());
|
||||
|
||||
if (Files.exists(altPath) && Files.isRegularFile(altPath)) {
|
||||
path = altPath;
|
||||
} else {
|
||||
logger.error("Firmware file not found at either path: {} or {}",
|
||||
path.toAbsolutePath(), altPath.toAbsolutePath());
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
byte[] fileContent = Files.readAllBytes(path);
|
||||
|
||||
// 设置响应头
|
||||
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
||||
if (firmwarePath.contains(".")) {
|
||||
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
|
||||
originalFilename += extension;
|
||||
}
|
||||
|
||||
// 清理文件名,移除不安全字符
|
||||
String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||
|
||||
logger.info("Providing download for firmware ID: {}, filename: {}, size: {} bytes",
|
||||
id, safeFilename, fileContent.length);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + safeFilename + "\"")
|
||||
.body(fileContent);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error reading firmware file for ID: {}", id, e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error during firmware download for ID: {}", id, e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
@Operation(summary = "上传固件文件")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<String> uploadFirmware(@RequestParam("file") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
return new Result<String>().error("上传文件不能为空");
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null) {
|
||||
return new Result<String>().error("文件名不能为空");
|
||||
}
|
||||
|
||||
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
|
||||
if (!extension.equals(".bin") && !extension.equals(".apk")) {
|
||||
return new Result<String>().error("只允许上传.bin和.apk格式的文件");
|
||||
}
|
||||
|
||||
try {
|
||||
// 计算文件的MD5值
|
||||
String md5 = calculateMD5(file);
|
||||
|
||||
// 设置存储路径
|
||||
String uploadDir = "uploadfile";
|
||||
Path uploadPath = Paths.get(uploadDir);
|
||||
|
||||
// 如果目录不存在,创建目录
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
|
||||
// 使用MD5作为文件名,固定使用.bin扩展名
|
||||
String uniqueFileName = md5 + extension;
|
||||
Path filePath = uploadPath.resolve(uniqueFileName);
|
||||
|
||||
// 检查文件是否已存在
|
||||
if (Files.exists(filePath)) {
|
||||
return new Result<String>().ok(filePath.toString());
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
Files.copy(file.getInputStream(), filePath);
|
||||
|
||||
// 返回文件路径
|
||||
return new Result<String>().ok(filePath.toString());
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
return new Result<String>().error("文件上传失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(file.getBytes());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,16 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
||||
/**
|
||||
* 获取此智能体全部设备的最后连接时间
|
||||
* @param agentId 智能体id
|
||||
* @return
|
||||
*/
|
||||
Date getAllLastConnectedAtByAgentId(String agentId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package xiaozhi.modules.device.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
|
||||
/**
|
||||
* OTA固件管理
|
||||
*/
|
||||
@Mapper
|
||||
public interface OtaDao extends BaseMapper<OtaEntity> {
|
||||
|
||||
}
|
||||
@@ -19,6 +19,9 @@ public class DeviceReportRespDTO {
|
||||
|
||||
@Schema(description = "固件版本信息")
|
||||
private Firmware firmware;
|
||||
|
||||
@Schema(description = "WebSocket配置")
|
||||
private Websocket websocket;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -44,6 +47,8 @@ public class DeviceReportRespDTO {
|
||||
@Schema(description = "激活码信息: 激活地址")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "挑战码")
|
||||
private String challenge;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -58,4 +63,11 @@ public class DeviceReportRespDTO {
|
||||
@Schema(description = "时区偏移量,单位为分钟")
|
||||
private Integer timezone_offset;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Websocket {
|
||||
@Schema(description = "WebSocket服务器地址")
|
||||
private String url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package xiaozhi.modules.device.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@TableName("ai_ota")
|
||||
@Schema(description = "固件信息")
|
||||
public class OtaEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "固件名称")
|
||||
private String firmwareName;
|
||||
|
||||
@Schema(description = "固件类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "版本号")
|
||||
private String version;
|
||||
|
||||
@Schema(description = "文件大小(字节)")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "备注/说明")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "固件路径")
|
||||
private String firmwarePath;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private Date updateDate;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createDate;
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
package xiaozhi.modules.device.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
|
||||
public interface DeviceService {
|
||||
|
||||
/**
|
||||
* 根据Mac地址获取设备信息
|
||||
*/
|
||||
DeviceEntity getDeviceById(String macAddress);
|
||||
public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
|
||||
/**
|
||||
* 检查设备是否激活
|
||||
@@ -82,4 +79,13 @@ public interface DeviceService {
|
||||
* @return 激活码
|
||||
*/
|
||||
String geCodeByDeviceId(String deviceId);
|
||||
|
||||
/**
|
||||
* 获取这个智能体设备理的最近的最后连接时间
|
||||
* @param agentId 智能体id
|
||||
* @return 返回设备最近的最后连接时间
|
||||
*/
|
||||
Date getLatestLastConnectionTime(String agentId);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package xiaozhi.modules.device.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
|
||||
/**
|
||||
* OTA固件管理
|
||||
*/
|
||||
public interface OtaService extends BaseService<OtaEntity> {
|
||||
PageData<OtaEntity> page(Map<String, Object> params);
|
||||
|
||||
boolean save(OtaEntity entity);
|
||||
|
||||
void update(OtaEntity entity);
|
||||
|
||||
void delete(String[] ids);
|
||||
|
||||
OtaEntity getLatestOta(String type);
|
||||
}
|
||||
+146
-43
@@ -6,21 +6,26 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.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.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
@@ -30,39 +35,24 @@ import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.device.service.OtaService;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||
|
||||
private final DeviceDao deviceDao;
|
||||
|
||||
private final SysUserUtilService sysUserUtilService;
|
||||
|
||||
private final String frontedUrl;
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
// 添加构造函数来初始化 deviceMapper
|
||||
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
||||
SysParamsService sysParamsService,
|
||||
RedisTemplate<String, Object> redisTemplate) {
|
||||
this.deviceDao = deviceDao;
|
||||
this.sysUserUtilService = sysUserUtilService;
|
||||
this.frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceEntity getDeviceById(String deviceId) {
|
||||
LambdaQueryWrapper<DeviceEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(DeviceEntity::getId, deviceId);
|
||||
return deviceDao.selectOne(queryWrapper);
|
||||
}
|
||||
private final SysParamsService sysParamsService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final OtaService otaService;
|
||||
|
||||
@Override
|
||||
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||
@@ -70,14 +60,14 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
throw new RenException("激活码不能为空");
|
||||
}
|
||||
String deviceKey = "ota:activation:code:" + activationCode;
|
||||
Object cacheDeviceId = redisTemplate.opsForValue().get(deviceKey);
|
||||
Object cacheDeviceId = redisUtils.get(deviceKey);
|
||||
if (cacheDeviceId == null) {
|
||||
throw new RenException("激活码错误");
|
||||
}
|
||||
String deviceId = (String) cacheDeviceId;
|
||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(cacheDeviceKey);
|
||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
||||
if (cacheMap == null) {
|
||||
throw new RenException("激活码错误");
|
||||
}
|
||||
@@ -107,6 +97,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
deviceEntity.setMacAddress(macAddress);
|
||||
deviceEntity.setUserId(user.getId());
|
||||
deviceEntity.setCreator(user.getId());
|
||||
deviceEntity.setAutoUpdate(1);
|
||||
deviceEntity.setCreateDate(currentTime);
|
||||
deviceEntity.setUpdater(user.getId());
|
||||
deviceEntity.setUpdateDate(currentTime);
|
||||
@@ -114,8 +105,8 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
deviceDao.insert(deviceEntity);
|
||||
|
||||
// 清理redis缓存
|
||||
redisTemplate.delete(cacheDeviceKey);
|
||||
redisTemplate.delete(deviceKey);
|
||||
redisUtils.delete(cacheDeviceKey);
|
||||
redisUtils.delete(deviceKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -124,19 +115,49 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
DeviceReportReqDTO deviceReport) {
|
||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||
response.setServer_time(buildServerTime());
|
||||
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
|
||||
// 在未来开发了固件更新功能,需要更换此处代码,
|
||||
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
|
||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
||||
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
||||
response.setFirmware(firmware);
|
||||
|
||||
DeviceEntity deviceById = getDeviceById(macAddress);
|
||||
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
||||
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
||||
|
||||
if (deviceById == null || deviceById.getAutoUpdate() != 0) {
|
||||
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||
response.setFirmware(firmware);
|
||||
} else {
|
||||
response.setFirmware(null);
|
||||
}
|
||||
|
||||
// 添加WebSocket配置
|
||||
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||
websocket.setUrl(wsUrl);
|
||||
} else {
|
||||
String[] wsUrls = wsUrl.split("\\;");
|
||||
if (wsUrls.length > 0) {
|
||||
// 随机选择一个WebSocket URL
|
||||
websocket.setUrl(wsUrls[RandomUtil.randomInt(0, wsUrls.length)]);
|
||||
} else {
|
||||
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
websocket.setUrl("ws://xiaozhi.server.com:8000/xiaozhi/v1/");
|
||||
}
|
||||
}
|
||||
|
||||
response.setWebsocket(websocket);
|
||||
|
||||
if (deviceById != null) {
|
||||
// 如果设备存在,则更新上次连接时间
|
||||
deviceById.setLastConnectedAt(new Date());
|
||||
if (deviceReport.getApplication() != null
|
||||
&& StringUtils.isNotBlank(deviceReport.getApplication().getVersion())) {
|
||||
deviceById.setAppVersion(deviceReport.getApplication().getVersion());
|
||||
}
|
||||
deviceDao.updateById(deviceById);
|
||||
} else { // 如果设备不存在,则生成激活码
|
||||
} else {
|
||||
// 如果设备不存在,则生成激活码
|
||||
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
||||
response.setActivation(code);
|
||||
}
|
||||
@@ -229,7 +250,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
public String geCodeByDeviceId(String deviceId) {
|
||||
String dataKey = getDeviceCacheKey(deviceId);
|
||||
|
||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
|
||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey);
|
||||
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||
String cachedCode = (String) cacheMap.get("activation_code");
|
||||
return cachedCode;
|
||||
@@ -237,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);
|
||||
@@ -250,11 +285,15 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
if (StringUtils.isNotBlank(cachedCode)) {
|
||||
code.setCode(cachedCode);
|
||||
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||
code.setChallenge(deviceId);
|
||||
} else {
|
||||
String newCode = RandomUtil.randomNumbers(6);
|
||||
code.setCode(newCode);
|
||||
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||
code.setMessage(frontedUrl + "\n" + newCode);
|
||||
code.setChallenge(deviceId);
|
||||
|
||||
Map<String, Object> dataMap = new HashMap<>();
|
||||
dataMap.put("id", deviceId);
|
||||
@@ -272,13 +311,77 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
// 写入主数据 key
|
||||
String dataKey = getDeviceCacheKey(deviceId);
|
||||
redisTemplate.opsForHash().putAll(dataKey, dataMap);
|
||||
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
|
||||
redisUtils.set(dataKey, dataMap);
|
||||
|
||||
// 写入反查激活码 key
|
||||
String codeKey = "ota:activation:code:" + newCode;
|
||||
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
|
||||
redisUtils.set(codeKey, deviceId);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private DeviceReportRespDTO.Firmware buildFirmwareInfo(String type, String currentVersion) {
|
||||
if (StringUtils.isBlank(type)) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isBlank(currentVersion)) {
|
||||
currentVersion = "0.0.0";
|
||||
}
|
||||
|
||||
OtaEntity ota = otaService.getLatestOta(type);
|
||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||
String downloadUrl = null;
|
||||
|
||||
if (ota != null) {
|
||||
// 如果设备没有版本信息,或者OTA版本比设备版本新,则返回下载地址
|
||||
if (compareVersions(ota.getVersion(), currentVersion) > 0) {
|
||||
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||
log.error("OTA地址未配置,请登录智控台,在参数管理找到【server.ota】配置");
|
||||
// 尝试从请求中获取
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes())
|
||||
.getRequest();
|
||||
otaUrl = request.getRequestURL().toString();
|
||||
}
|
||||
// 将URL中的/ota/替换为/otaMag/download/
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getOtaIdKey(uuid), ota.getId());
|
||||
downloadUrl = otaUrl.replace("/ota/", "/otaMag/download/") + uuid;
|
||||
}
|
||||
}
|
||||
|
||||
firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
|
||||
firmware.setUrl(downloadUrl == null ? "" : downloadUrl);
|
||||
return firmware;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个版本号
|
||||
*
|
||||
* @param version1 版本1
|
||||
* @param version2 版本2
|
||||
* @return 如果version1 > version2返回1,version1 < version2返回-1,相等返回0
|
||||
*/
|
||||
private static int compareVersions(String version1, String version2) {
|
||||
if (version1 == null || version2 == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String[] v1Parts = version1.split("\\.");
|
||||
String[] v2Parts = version2.split("\\.");
|
||||
|
||||
int length = Math.max(v1Parts.length, v2Parts.length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
int v1 = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0;
|
||||
int v2 = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0;
|
||||
|
||||
if (v1 > v2) {
|
||||
return 1;
|
||||
} else if (v1 < v2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.modules.device.dao.OtaDao;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
import xiaozhi.modules.device.service.OtaService;
|
||||
|
||||
@Service
|
||||
public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implements OtaService {
|
||||
|
||||
@Override
|
||||
public PageData<OtaEntity> page(Map<String, Object> params) {
|
||||
IPage<OtaEntity> page = baseDao.selectPage(
|
||||
getPage(params, "update_date", true),
|
||||
getWrapper(params));
|
||||
|
||||
return new PageData<>(page.getRecords(), page.getTotal());
|
||||
}
|
||||
|
||||
private QueryWrapper<OtaEntity> getWrapper(Map<String, Object> params) {
|
||||
String firmwareName = (String) params.get("firmwareName");
|
||||
|
||||
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.like(StringUtils.isNotBlank(firmwareName), "firmware_name", firmwareName);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(OtaEntity entity) {
|
||||
// 检查是否存在相同类型和版本的固件(排除当前记录)
|
||||
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||
.eq("type", entity.getType())
|
||||
.eq("version", entity.getVersion())
|
||||
.ne("id", entity.getId()); // 排除当前记录
|
||||
|
||||
if (baseDao.selectCount(queryWrapper) > 0) {
|
||||
throw new RuntimeException("已存在相同类型和版本的固件,请修改后重试");
|
||||
}
|
||||
|
||||
entity.setUpdateDate(new Date());
|
||||
baseDao.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String[] ids) {
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean save(OtaEntity entity) {
|
||||
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||
.eq("type", entity.getType());
|
||||
// 同类固件只保留最新的一条
|
||||
List<OtaEntity> otaList = baseDao.selectList(queryWrapper);
|
||||
if (otaList != null && otaList.size() > 0) {
|
||||
OtaEntity otaBefore = otaList.getFirst();
|
||||
entity.setId(otaBefore.getId());
|
||||
baseDao.updateById(entity);
|
||||
return true;
|
||||
}
|
||||
return baseDao.insert(entity) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OtaEntity getLatestOta(String type) {
|
||||
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("type", type)
|
||||
.orderByDesc("update_date")
|
||||
.last("LIMIT 1");
|
||||
return baseDao.selectOne(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package xiaozhi.modules.device.utils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 网络工具类
|
||||
*/
|
||||
public class NetworkUtil {
|
||||
/**
|
||||
* MAC地址正则表达式
|
||||
*/
|
||||
private static final Pattern macPattern = Pattern.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
|
||||
|
||||
/**
|
||||
* 判断MAC地址是否合法
|
||||
*/
|
||||
public static boolean isMacAddressValid(String mac) {
|
||||
if (StringUtils.isBlank(mac)) {
|
||||
return false;
|
||||
}
|
||||
// 正则校验格式
|
||||
if (!macPattern.matcher(mac).matches()) {
|
||||
return false;
|
||||
}
|
||||
// 校验MAC地址是否为单播地址
|
||||
String normalized = mac.toLowerCase();
|
||||
String[] parts = normalized.split("[:-]");
|
||||
int firstByte = Integer.parseInt(parts[0], 16);
|
||||
return (firstByte & 1) == 0; // 最低位为0表示单播地址,合法
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -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的内置过滤器
|
||||
@@ -69,6 +73,7 @@ public class ShiroConfig {
|
||||
*/
|
||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||
filterMap.put("/ota/**", "anon");
|
||||
filterMap.put("/otaMag/download/**", "anon");
|
||||
filterMap.put("/webjars/**", "anon");
|
||||
filterMap.put("/druid/**", "anon");
|
||||
filterMap.put("/v3/api-docs/**", "anon");
|
||||
@@ -78,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);
|
||||
|
||||
@@ -97,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 {
|
||||
@@ -33,11 +43,15 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
// 特殊用途的转换器
|
||||
converters.add(new ByteArrayHttpMessageConverter());
|
||||
converters.add(new StringHttpMessageConverter());
|
||||
converters.add(new ResourceHttpMessageConverter());
|
||||
converters.add(new AllEncompassingFormHttpMessageConverter());
|
||||
|
||||
// 通用转换器
|
||||
converters.add(new StringHttpMessageConverter());
|
||||
converters.add(new AllEncompassingFormHttpMessageConverter());
|
||||
|
||||
// JSON 转换器
|
||||
converters.add(jackson2HttpMessageConverter());
|
||||
}
|
||||
|
||||
@@ -49,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);
|
||||
|
||||
+105
@@ -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);
|
||||
}
|
||||
|
||||
+105
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+92
@@ -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<>();
|
||||
}
|
||||
}
|
||||
+87
@@ -2,7 +2,10 @@ package xiaozhi.modules.sys.controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -11,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -20,6 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.annotation.LogOperation;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
@@ -30,6 +35,7 @@ import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.utils.WebSocketValidator;
|
||||
|
||||
/**
|
||||
* 参数管理
|
||||
@@ -44,6 +50,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
||||
public class SysParamsController {
|
||||
private final SysParamsService sysParamsService;
|
||||
private final ConfigService configService;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@GetMapping("page")
|
||||
@Operation(summary = "分页")
|
||||
@@ -91,11 +98,91 @@ public class SysParamsController {
|
||||
// 效验数据
|
||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||
|
||||
// 验证WebSocket地址列表
|
||||
validateWebSocketUrls(dto.getParamCode(), dto.getParamValue());
|
||||
|
||||
// 验证OTA地址
|
||||
validateOtaUrl(dto.getParamCode(), dto.getParamValue());
|
||||
|
||||
sysParamsService.update(dto);
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证WebSocket地址列表
|
||||
*
|
||||
* @param urls WebSocket地址列表,以分号分隔
|
||||
* @return 验证结果
|
||||
*/
|
||||
private void validateWebSocketUrls(String paramCode, String urls) {
|
||||
if (!paramCode.equals(Constant.SERVER_WEBSOCKET)) {
|
||||
return;
|
||||
}
|
||||
String[] wsUrls = urls.split("\\;");
|
||||
if (wsUrls.length == 0) {
|
||||
throw new RenException("WebSocket地址列表不能为空");
|
||||
}
|
||||
for (String url : wsUrls) {
|
||||
if (StringUtils.isNotBlank(url)) {
|
||||
// 检查是否包含localhost或127.0.0.1
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException("WebSocket地址不能使用localhost或127.0.0.1");
|
||||
}
|
||||
|
||||
// 验证WebSocket地址格式
|
||||
if (!WebSocketValidator.validateUrlFormat(url)) {
|
||||
throw new RenException("WebSocket地址格式不正确: " + url);
|
||||
}
|
||||
|
||||
// 测试WebSocket连接
|
||||
if (!WebSocketValidator.testConnection(url)) {
|
||||
throw new RenException("WebSocket连接测试失败: " + url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证OTA地址
|
||||
*/
|
||||
private void validateOtaUrl(String paramCode, String url) {
|
||||
if (!paramCode.equals(Constant.SERVER_OTA)) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||
throw new RenException("OTA地址不能为空");
|
||||
}
|
||||
|
||||
// 检查是否包含localhost或127.0.0.1
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException("OTA地址不能使用localhost或127.0.0.1");
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
if (!url.toLowerCase().startsWith("http")) {
|
||||
throw new RenException("OTA地址必须以http或https开头");
|
||||
}
|
||||
if (!url.endsWith("/ota/")) {
|
||||
throw new RenException("OTA地址必须以/ota/结尾");
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送GET请求
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode());
|
||||
}
|
||||
// 检查响应内容是否包含OTA相关信息
|
||||
String body = response.getBody();
|
||||
if (body == null || !body.contains("OTA")) {
|
||||
throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RenException("OTA接口验证失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除")
|
||||
@LogOperation("删除")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+114
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
+76
-27
@@ -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("字典类型编码重复");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -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());
|
||||
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package xiaozhi.modules.sys.utils;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
public class WebSocketTestHandler implements WebSocketHandler {
|
||||
private final CompletableFuture<Boolean> future;
|
||||
|
||||
public WebSocketTestHandler(CompletableFuture<Boolean> future) {
|
||||
this.future = future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
future.complete(true);
|
||||
try {
|
||||
session.close();
|
||||
} catch (Exception e) {
|
||||
// 忽略关闭异常
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||
// 不需要处理消息
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable exception) {
|
||||
future.complete(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
|
||||
// 连接关闭时不做任何处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPartialMessages() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package xiaozhi.modules.sys.utils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||
import org.springframework.web.socket.client.WebSocketClient;
|
||||
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||
|
||||
public class WebSocketValidator {
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketValidator.class);
|
||||
|
||||
// WebSocket URL正则表达式
|
||||
private static final Pattern WS_URL_PATTERN = Pattern
|
||||
.compile("^wss?://[\\w.-]+(?:\\.[\\w.-]+)*(?::\\d+)?(?:/[\\w.-]*)*$");
|
||||
|
||||
/**
|
||||
* 验证WebSocket地址格式
|
||||
*
|
||||
* @param url WebSocket地址
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean validateUrlFormat(String url) {
|
||||
if (StringUtils.isBlank(url)) {
|
||||
return false;
|
||||
}
|
||||
return WS_URL_PATTERN.matcher(url.trim()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试WebSocket连接
|
||||
*
|
||||
* @param url WebSocket地址
|
||||
* @return 是否可连接
|
||||
*/
|
||||
public static boolean testConnection(String url) {
|
||||
if (!validateUrlFormat(url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
WebSocketClient client = new StandardWebSocketClient();
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
|
||||
client.execute(new WebSocketTestHandler(future), headers, URI.create(url));
|
||||
|
||||
// 等待最多5秒获取连接结果
|
||||
return future.get(5, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
logger.error("WebSocket连接测试失败: {}", url, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package xiaozhi.modules.sys.vo;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -24,7 +24,6 @@ public class TimbreDataDTO {
|
||||
private String name;
|
||||
|
||||
@Schema(description = "备注")
|
||||
@NotBlank(message = "{timbre.remark.require}")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "排序")
|
||||
@@ -40,6 +39,5 @@ public class TimbreDataDTO {
|
||||
private String ttsVoice;
|
||||
|
||||
@Schema(description = "音频播放地址")
|
||||
@NotBlank(message = "{timbre.voiceDemo.require}")
|
||||
private String voiceDemo;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
-- 对0.3.0版本之前的参数进行修改
|
||||
update `sys_params` set param_value = '.mp3;.wav;.p3' where param_code = 'plugins.play_music.music_ext';
|
||||
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Intent_intent_llm';
|
||||
@@ -1,2 +0,0 @@
|
||||
delete from `ai_model_config` where id = 'Intent_function_call';
|
||||
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 0, 1, '{\"type\": \"function_call\", \"functions\": \"change_role;get_weather;get_news;play_music\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||
@@ -0,0 +1,88 @@
|
||||
-- 删除无用模型供应器
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_LLM_doubao';
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_LLM_chatglm';
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_TTS_302ai';
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_TTS_gizwits';
|
||||
|
||||
-- 添加模型供应器
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_ASR_TencentASR';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_ASR_TencentASR', 'ASR', 'tencent', '腾讯语音识别', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 4, 1, NOW(), 1, NOW());
|
||||
|
||||
-- 添加腾讯语音合成模型供应器
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_TTS_TencentTTS';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_TTS_TencentTTS', 'TTS', 'tencent', '腾讯语音合成', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"region","label":"区域","type":"string"},{"key":"voice","label":"音色ID","type":"string"}]', 5, 1, NOW(), 1, NOW());
|
||||
|
||||
|
||||
-- 添加edge音色
|
||||
delete from `ai_tts_voice` where id in ('TTS_EdgeTTS0001', 'TTS_EdgeTTS0002', 'TTS_EdgeTTS0003', 'TTS_EdgeTTS0004', 'TTS_EdgeTTS0005', 'TTS_EdgeTTS0006', 'TTS_EdgeTTS0007', 'TTS_EdgeTTS0008', 'TTS_EdgeTTS0009', 'TTS_EdgeTTS0010', 'TTS_EdgeTTS0011');
|
||||
INSERT INTO `ai_tts_voice` VALUES
|
||||
('TTS_EdgeTTS0001', 'TTS_EdgeTTS', 'EdgeTTS女声-晓晓', 'zh-CN-XiaoxiaoNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0002', 'TTS_EdgeTTS', 'EdgeTTS男声-云扬', 'zh-CN-YunyangNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0003', 'TTS_EdgeTTS', 'EdgeTTS女声-晓伊', 'zh-CN-XiaoyiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0004', 'TTS_EdgeTTS', 'EdgeTTS男声-云健', 'zh-CN-YunjianNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0005', 'TTS_EdgeTTS', 'EdgeTTS男声-云希', 'zh-CN-YunxiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0006', 'TTS_EdgeTTS', 'EdgeTTS男声-云夏', 'zh-CN-YunxiaNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0007', 'TTS_EdgeTTS', 'EdgeTTS女声-辽宁小贝', 'zh-CN-liaoning-XiaobeiNeural', '辽宁', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0008', 'TTS_EdgeTTS', 'EdgeTTS女声-陕西小妮', 'zh-CN-shaanxi-XiaoniNeural', '陕西', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- DoubaoTTS音色
|
||||
delete from `ai_tts_voice` where id in ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS0002', 'TTS_DoubaoTTS0003', 'TTS_DoubaoTTS0004', 'TTS_DoubaoTTS0005');
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS', '通用女声', 'BV001_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV001.mp3', NULL, 3, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0002', 'TTS_DoubaoTTS', '通用男声', 'BV002_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV002.mp3', NULL, 2, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0003', 'TTS_DoubaoTTS', '阳光男生', 'BV056_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV056.mp3', NULL, 4, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0004', 'TTS_DoubaoTTS', '奶气萌娃', 'BV051_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV051.mp3', NULL, 5, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0005', 'TTS_DoubaoTTS', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '普通话', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 修正CosyVoiceSiliconflow音色
|
||||
delete from `ai_tts_voice` where id in ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow0002');
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'FunAudioLLM/CosyVoice2-0.5B:alex', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0002', 'TTS_CosyVoiceSiliconflow', 'CosyVoice女声', 'FunAudioLLM/CosyVoice2-0.5B:bella', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- CozeCnTTS音色
|
||||
delete from `ai_tts_voice` where id = 'TTS_CozeCnTTS0001';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CozeCnTTS0001', 'TTS_CozeCnTTS', 'CozeCn音色', '7426720361733046281', '中文', NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- MinimaxTTS音色
|
||||
delete from `ai_tts_voice` where id = 'TTS_MinimaxTTS0001';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxTTS0001', 'TTS_MinimaxTTS', 'Minimax少女音', 'female-shaonv', '中文', NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- AliyunTTS音色
|
||||
delete from `ai_tts_voice` where id = 'TTS_AliyunTTS0001';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunTTS0001', 'TTS_AliyunTTS', '阿里云小云', 'xiaoyun', '中文', NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- TTS302AI音色
|
||||
delete from `ai_tts_voice` where id = 'TTS_TTS302AI0001';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_TTS302AI0001', 'TTS_TTS302AI', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 10, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- GizwitsTTS音色
|
||||
delete from `ai_tts_voice` where id = 'TTS_GizwitsTTS0001';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GizwitsTTS0001', 'TTS_GizwitsTTS', '机智云湾湾', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 11, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- ACGNTTS音色
|
||||
delete from `ai_tts_voice` where id = 'TTS_ACGNTTS0001';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_ACGNTTS0001', 'TTS_ACGNTTS', 'ACG音色', '1695', '中文', NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- OpenAITTS音色
|
||||
delete from `ai_tts_voice` where id = 'TTS_OpenAITTS0001';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_OpenAITTS0001', 'TTS_OpenAITTS', 'OpenAI男声', 'onyx', '中文', NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 添加腾讯语音合成音色
|
||||
delete from `ai_tts_voice` where id = 'TTS_TencentTTS0001';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_TencentTTS0001', 'TTS_TencentTTS', '智瑜', '101001', '中文', NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 其他音色
|
||||
delete from `ai_tts_voice` where id = 'TTS_FishSpeech0000';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_FishSpeech0000', 'TTS_FishSpeech', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||
|
||||
delete from `ai_tts_voice` where id = 'TTS_GPT_SOVITS_V20000';
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V20000', 'TTS_GPT_SOVITS_V2', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||
|
||||
delete from `ai_tts_voice` where id in ('TTS_GPT_SOVITS_V30000', 'TTS_CustomTTS0000');
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V30000', 'TTS_GPT_SOVITS_V3', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CustomTTS0000', 'TTS_CustomTTS', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
-- 添加火山引擎边缘大模型网关(LLM + TTS)
|
||||
INSERT INTO `ai_model_config` VALUES ('LLM_VolcesAiGatewayLLM', 'LLM', 'VolcesAiGatewayTTS', '火山引擎边缘大模型网关', 0, 1, '{\"type\": \"openai\", \"base_url\": \"https://ai-gateway.vei.volces.com/v1\", \"model_name\": \"doubao-pro-32k-functioncall\", \"api_key\": \"你的网关访问密钥\"}', NULL, NULL, 14, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_model_config` VALUES ('TTS_VolcesAiGatewayTTS', 'TTS', 'VolcesAiGatewayTTS', '火山引擎边缘大模型网关', 0, 1, '{\"type\": \"openai\", \"api_key\": \"你的网关访问密钥\", \"api_url\": \"https://ai-gateway.vei.volces.com/v1/audio/speech\", \"model\": \"doubao-tts\", \"voice\": \"zh_male_shaonianzixin_moon_bigtts\", \"speed\": 1, \"output_dir\": \"tmp/\"}', NULL, NULL, 15, NULL, NULL, NULL, NULL);
|
||||
-- 添加火山引擎边缘大模型网关语音合成音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0001', 'TTS_VolcesAiGatewayTTS', '灿灿/Shiny', 'zh_female_cancan_mars_bigtts', '中文、美式英语', NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0002', 'TTS_VolcesAiGatewayTTS', '清新女声', 'zh_female_qingxinnvsheng_mars_bigtts', '中文', NULL, NULL, 2, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0003', 'TTS_VolcesAiGatewayTTS', '爽快思思/Skye', 'zh_female_shuangkuaisisi_moon_bigtts', '中文、美式英语', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0004', 'TTS_VolcesAiGatewayTTS', '温暖阿虎/Alvin', 'zh_male_wennuanahu_moon_bigtts', '中文、美式英语', NULL, NULL, 4, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0005', 'TTS_VolcesAiGatewayTTS', '少年梓辛/Brayan', 'zh_male_shaonianzixin_moon_bigtts', '中文、美式英语', NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0006', 'TTS_VolcesAiGatewayTTS', '知性女声', 'zh_female_zhixingnvsheng_mars_bigtts', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0007', 'TTS_VolcesAiGatewayTTS', '清爽男大', 'zh_male_qingshuangnanda_mars_bigtts', '中文', NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0008', 'TTS_VolcesAiGatewayTTS', '邻家女孩', 'zh_female_linjianvhai_moon_bigtts', '中文', NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0009', 'TTS_VolcesAiGatewayTTS', '渊博小叔', 'zh_male_yuanboxiaoshu_moon_bigtts', '中文', NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0010', 'TTS_VolcesAiGatewayTTS', '阳光青年', 'zh_male_yangguangqingnian_moon_bigtts', '中文', NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0011', 'TTS_VolcesAiGatewayTTS', '甜美小源', 'zh_female_tianmeixiaoyuan_moon_bigtts', '中文', NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0012', 'TTS_VolcesAiGatewayTTS', '清澈梓梓', 'zh_female_qingchezizi_moon_bigtts', '中文', NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0013', 'TTS_VolcesAiGatewayTTS', '解说小明', 'zh_male_jieshuoxiaoming_moon_bigtts', '中文', NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0014', 'TTS_VolcesAiGatewayTTS', '开朗姐姐', 'zh_female_kailangjiejie_moon_bigtts', '中文', NULL, NULL, 14, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0015', 'TTS_VolcesAiGatewayTTS', '邻家男孩', 'zh_male_linjiananhai_moon_bigtts', '中文', NULL, NULL, 15, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0016', 'TTS_VolcesAiGatewayTTS', '甜美悦悦', 'zh_female_tianmeiyueyue_moon_bigtts', '中文', NULL, NULL, 16, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0017', 'TTS_VolcesAiGatewayTTS', '心灵鸡汤', 'zh_female_xinlingjitang_moon_bigtts', '中文', NULL, NULL, 17, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0018', 'TTS_VolcesAiGatewayTTS', '知性温婉', 'ICL_zh_female_zhixingwenwan_tob', '中文', NULL, NULL, 18, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0019', 'TTS_VolcesAiGatewayTTS', '暖心体贴', 'ICL_zh_male_nuanxintitie_tob', '中文', NULL, NULL, 19, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0020', 'TTS_VolcesAiGatewayTTS', '温柔文雅', 'ICL_zh_female_wenrouwenya_tob', '中文', NULL, NULL, 20, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0021', 'TTS_VolcesAiGatewayTTS', '开朗轻快', 'ICL_zh_male_kailangqingkuai_tob', '中文', NULL, NULL, 21, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0022', 'TTS_VolcesAiGatewayTTS', '活泼爽朗', 'ICL_zh_male_huoposhuanglang_tob', '中文', NULL, NULL, 22, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0023', 'TTS_VolcesAiGatewayTTS', '率真小伙', 'ICL_zh_male_shuaizhenxiaohuo_tob', '中文', NULL, NULL, 23, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0024', 'TTS_VolcesAiGatewayTTS', '温柔小哥', 'zh_male_wenrouxiaoge_mars_bigtts', '中文', NULL, NULL, 24, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0025', 'TTS_VolcesAiGatewayTTS', 'Smith', 'en_male_smith_mars_bigtts', '英式英语', NULL, NULL, 25, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0026', 'TTS_VolcesAiGatewayTTS', 'Anna', 'en_female_anna_mars_bigtts', '英式英语', NULL, NULL, 26, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0027', 'TTS_VolcesAiGatewayTTS', 'Adam', 'en_male_adam_mars_bigtts', '美式英语', NULL, NULL, 27, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0028', 'TTS_VolcesAiGatewayTTS', 'Sarah', 'en_female_sarah_mars_bigtts', '澳洲英语', NULL, NULL, 28, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0029', 'TTS_VolcesAiGatewayTTS', 'Dryw', 'en_male_dryw_mars_bigtts', '澳洲英语', NULL, NULL, 29, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0030', 'TTS_VolcesAiGatewayTTS', 'かずね(和音)', 'multi_male_jingqiangkanye_moon_bigtts', '日语、西语', NULL, NULL, 30, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0031', 'TTS_VolcesAiGatewayTTS', 'はるこ(晴子)', 'multi_female_shuangkuaisisi_moon_bigtts', '日语、西语', NULL, NULL, 31, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0032', 'TTS_VolcesAiGatewayTTS', 'ひろし(広志)', 'multi_male_wanqudashu_moon_bigtts', '日语、西语', NULL, NULL, 32, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0033', 'TTS_VolcesAiGatewayTTS', 'あけみ(朱美)', 'multi_female_gaolengyujie_moon_bigtts', '日语', NULL, NULL, 33, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0034', 'TTS_VolcesAiGatewayTTS', 'Amanda', 'en_female_amanda_mars_bigtts', '美式英语', NULL, NULL, 34, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0035', 'TTS_VolcesAiGatewayTTS', 'Jackson', 'en_male_jackson_mars_bigtts', '美式英语', NULL, NULL, 35, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0036', 'TTS_VolcesAiGatewayTTS', '京腔侃爷/Harmony', 'zh_male_jingqiangkanye_moon_bigtts', '中文-北京口音、英文', NULL, NULL, 36, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0037', 'TTS_VolcesAiGatewayTTS', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文-台湾口音', NULL, NULL, 37, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0038', 'TTS_VolcesAiGatewayTTS', '湾区大叔', 'zh_female_wanqudashu_moon_bigtts', '中文-广东口音', NULL, NULL, 38, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0039', 'TTS_VolcesAiGatewayTTS', '呆萌川妹', 'zh_female_daimengchuanmei_moon_bigtts', '中文-四川口音', NULL, NULL, 39, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0040', 'TTS_VolcesAiGatewayTTS', '广州德哥', 'zh_male_guozhoudege_moon_bigtts', '中文-广东口音', NULL, NULL, 40, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0041', 'TTS_VolcesAiGatewayTTS', '北京小爷', 'zh_male_beijingxiaoye_moon_bigtts', '中文-北京口音', NULL, NULL, 41, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0042', 'TTS_VolcesAiGatewayTTS', '浩宇小哥', 'zh_male_haoyuxiaoge_moon_bigtts', '中文-青岛口音', NULL, NULL, 42, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0043', 'TTS_VolcesAiGatewayTTS', '广西远舟', 'zh_male_guangxiyuanzhou_moon_bigtts', '中文-广西口音', NULL, NULL, 43, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0044', 'TTS_VolcesAiGatewayTTS', '妹坨洁儿', 'zh_female_meituojieer_moon_bigtts', '中文-长沙口音', NULL, NULL, 44, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0045', 'TTS_VolcesAiGatewayTTS', '豫州子轩', 'zh_male_yuzhouzixuan_moon_bigtts', '中文-河南口音', NULL, NULL, 45, NULL, NULL, NULL, NULL);
|
||||
@@ -0,0 +1,30 @@
|
||||
-- 增加server.ota,用于配置ota地址
|
||||
|
||||
delete from `sys_params` where id = 100;
|
||||
delete from `sys_params` where id = 101;
|
||||
|
||||
delete from `sys_params` where id = 106;
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (106, 'server.websocket', 'null', 'string', 1, 'websocket地址,多个用;分隔');
|
||||
|
||||
delete from `sys_params` where id = 107;
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (107, 'server.ota', 'null', 'string', 1, 'ota地址');
|
||||
|
||||
|
||||
-- 增加固件信息表
|
||||
CREATE TABLE IF NOT EXISTS `ai_ota` (
|
||||
`id` varchar(32) NOT NULL COMMENT 'ID',
|
||||
`firmware_name` varchar(100) DEFAULT NULL COMMENT '固件名称',
|
||||
`type` varchar(50) DEFAULT NULL COMMENT '固件类型',
|
||||
`version` varchar(50) DEFAULT NULL COMMENT '版本号',
|
||||
`size` bigint DEFAULT NULL COMMENT '文件大小(字节)',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注/说明',
|
||||
`firmware_path` varchar(255) DEFAULT NULL COMMENT '固件路径',
|
||||
`sort` int unsigned DEFAULT '0' COMMENT '排序',
|
||||
`updater` bigint DEFAULT NULL COMMENT '更新者',
|
||||
`update_date` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`creator` bigint DEFAULT NULL COMMENT '创建者',
|
||||
`create_date` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='固件信息表';
|
||||
|
||||
update ai_device set auto_update = 1;
|
||||
@@ -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 '智能体聊天音频数据表';
|
||||
@@ -1,44 +0,0 @@
|
||||
-- 本文件用于初始化音色数据,无需手动执行,在项目启动时会自动执行
|
||||
-- ----------------------------------------------------
|
||||
|
||||
-- 初始化音色数据
|
||||
DELETE FROM `ai_tts_voice`;
|
||||
|
||||
-- EdgeTTS音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_EdgeTTS0001', 'TTS_EdgeTTS', 'EdgeTTS女声', 'zh-CN-XiaoxiaoNeural', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/EdgeTTS.mp3', NULL, 1, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- DoubaoTTS音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS', '通用女声', 'BV001_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV001.mp3', NULL, 3, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0002', 'TTS_DoubaoTTS', '通用男声', 'BV002_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV002.mp3', NULL, 2, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0003', 'TTS_DoubaoTTS', '阳光男生', 'BV056_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV056.mp3', NULL, 4, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0004', 'TTS_DoubaoTTS', '奶气萌娃', 'BV051_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV051.mp3', NULL, 5, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- CosyVoiceSiliconflow音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'alex', '中文', 'https://example.com/cosyvoice/alex.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- CozeCnTTS音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CozeCnTTS0001', 'TTS_CozeCnTTS', 'CozeCn音色', '7426720361733046281', '中文', 'https://example.com/cozecn/voice.mp3', NULL, 7, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- MinimaxTTS音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxTTS0001', 'TTS_MinimaxTTS', 'Minimax少女音', 'female-shaonv', '中文', 'https://example.com/minimax/female-shaonv.mp3', NULL, 8, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- AliyunTTS音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunTTS0001', 'TTS_AliyunTTS', '阿里云小云', 'xiaoyun', '中文', 'https://example.com/aliyun/xiaoyun.mp3', NULL, 9, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- TTS302AI音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_TTS302AI0001', 'TTS_TTS302AI', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://example.com/302ai/wanwanxiaohe.mp3', NULL, 10, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- GizwitsTTS音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GizwitsTTS0001', 'TTS_GizwitsTTS', '机智云湾湾', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://example.com/gizwits/wanwanxiaohe.mp3', NULL, 11, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- ACGNTTS音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_ACGNTTS0001', 'TTS_ACGNTTS', 'ACG音色', '1695', '中文', 'https://example.com/acgn/voice.mp3', NULL, 12, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- OpenAITTS音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_OpenAITTS0001', 'TTS_OpenAITTS', 'OpenAI男声', 'onyx', '中文', 'https://example.com/openai/onyx.mp3', NULL, 13, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 其他音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_FishSpeech0000', 'TTS_FishSpeech', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V20000', 'TTS_GPT_SOVITS_V2', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V30000', 'TTS_GPT_SOVITS_V3', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CustomTTS0000', 'TTS_CustomTTS', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||
@@ -16,13 +16,6 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202503141346.sql
|
||||
- changeSet:
|
||||
id: 2025_tts_voive
|
||||
author: John
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/2025_tts_voive.sql
|
||||
- changeSet:
|
||||
id: 202504082211
|
||||
author: John
|
||||
@@ -65,3 +58,45 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202504181536.sql
|
||||
- changeSet:
|
||||
id: 202504221135
|
||||
author: John
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202504221135.sql
|
||||
- changeSet:
|
||||
id: 202504221555
|
||||
author: John
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202504221555.sql
|
||||
- changeSet:
|
||||
id: 202504251422
|
||||
author: jiangkunyin
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
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
|
||||
@@ -28,10 +28,8 @@ sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
|
||||
ota.device.not.found=\u8BBE\u5907\u672A\u627E\u5230
|
||||
ota.device.need.bind={0}
|
||||
|
||||
@@ -27,10 +27,8 @@ sysuser.uuid.require=The unique identifier cannot be empty
|
||||
|
||||
timbre.languages.require=The language of the timbre cannot be empty
|
||||
timbre.name.require=The name of the timbre cannot be empty
|
||||
timbre.remark.require=The remark of the timbre cannot be empty
|
||||
timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
|
||||
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
|
||||
timbre.voiceDemo.require=The voice demo of the timbre cannot be empty
|
||||
|
||||
ota.device.not.found=Device not found
|
||||
ota.device.need.bind={0}
|
||||
@@ -27,10 +27,8 @@ sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
timbre.languages.require=\u97F3\u8272\u7684\u8A9E\u8A00\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||
timbre.name.require=\u97F3\u8272\u7684\u540D\u7A31\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||
timbre.remark.require=\u97F3\u8272\u7684\u5099\u6CE8\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653E\u5730\u5740\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||
|
||||
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
||||
ota.device.need.bind={0}
|
||||
@@ -27,10 +27,8 @@ sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||
|
||||
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
||||
ota.device.need.bind={0}
|
||||
@@ -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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user