Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5b64c3ef0 | ||
|
|
2aa6c5ee49 | ||
|
|
8ec5851027 | ||
|
|
48d629c79c | ||
|
|
14e57a1add | ||
|
|
634967b2fe | ||
|
|
e648453c67 | ||
|
|
32fbbf92fe | ||
|
|
bd981aac79 | ||
|
|
adee070bb0 | ||
|
|
03ec3bac3f | ||
|
|
af5fbb5282 | ||
|
|
9301c1889f | ||
|
|
ef2ed07c1c | ||
|
|
1e2c4c6a28 | ||
|
|
a32500a6a4 | ||
|
|
772af6c918 | ||
|
|
2a917c1e62 | ||
|
|
c32fbe69c5 | ||
|
|
3b6e8f0e4b | ||
|
|
c82b351457 | ||
|
|
04c10ef225 | ||
|
|
0b8bb728e2 | ||
|
|
35ec3ba803 | ||
|
|
8808b69303 | ||
|
|
fc573a72ea | ||
|
|
59f6566cfb | ||
|
|
d8c97a9f81 | ||
|
|
3d204fb181 | ||
|
|
209fd3fbc8 | ||
|
|
bc0ffd1bd3 | ||
|
|
f0d61442b8 | ||
|
|
e6c2fac7ec | ||
|
|
5a9b5af441 | ||
|
|
16e5dc8c78 | ||
|
|
f77970dd7d | ||
|
|
af3ca4f609 | ||
|
|
1ebfcd1f13 | ||
|
|
3ca322961e | ||
|
|
0eb0a600a7 | ||
|
|
3ed06a9e34 | ||
|
|
2fae96d60c | ||
|
|
67dde7cc47 | ||
|
|
efedc18f02 | ||
|
|
0270de1844 | ||
|
|
06b0ff139b | ||
|
|
799ea28d82 | ||
|
|
5a68252d22 | ||
|
|
97404e53fe | ||
|
|
b052158cee | ||
|
|
979ef24b79 | ||
|
|
0f4f0f01fd | ||
|
|
18e1de4f45 | ||
|
|
aa846a096f | ||
|
|
a679412230 | ||
|
|
cf3e14bb07 | ||
|
|
3a73a17253 | ||
|
|
3b27ebcd35 | ||
|
|
fa6074d284 | ||
|
|
d809cfcb01 | ||
|
|
eb2c09bbdc | ||
|
|
2cf8f79482 | ||
|
|
1c94844bb9 | ||
|
|
0c53b8f543 | ||
|
|
1518dfca30 | ||
|
|
7529c500a4 | ||
|
|
b6ed220e48 | ||
|
|
b152c5dc23 | ||
|
|
04bfddf457 | ||
|
|
9d78ea29c3 | ||
|
|
f82f364189 | ||
|
|
9705149b2b | ||
|
|
7986148013 | ||
|
|
19f5cb5b4f | ||
|
|
ea48b948bf | ||
|
|
3c5ca9f143 | ||
|
|
da72ecaa1b | ||
|
|
f297e1b648 | ||
|
|
a1973e07b7 | ||
|
|
cd58b2c3cb | ||
|
|
a5be67d04b | ||
|
|
e6dc6187c8 | ||
|
|
7f4011f9de | ||
|
|
211cf891af | ||
|
|
89a1229b53 | ||
|
|
071ed99a32 | ||
|
|
ea14be27e6 | ||
|
|
a834f163b7 | ||
|
|
7849e6b030 | ||
|
|
0d869baaa0 | ||
|
|
164eb5481c | ||
|
|
969ee3cb12 | ||
|
|
ee54d87383 | ||
|
|
849026c015 | ||
|
|
e5bb3e7791 | ||
|
|
1ffc40e71d | ||
|
|
45fd850757 | ||
|
|
8a5dac181e | ||
|
|
ecf20300ff | ||
|
|
74def844f0 | ||
|
|
a5a9bd1abe | ||
|
|
2d9b915d51 | ||
|
|
5f87fc8361 | ||
|
|
6859fdc90e | ||
|
|
b8ae0568e9 | ||
|
|
1dc6f143f3 | ||
|
|
67532dc230 | ||
|
|
b2876def99 | ||
|
|
0c73a8b6a4 | ||
|
|
c857a3164d | ||
|
|
a964192eb0 | ||
|
|
32da03818e | ||
|
|
7f17e3e08e | ||
|
|
c237e1f323 | ||
|
|
087a202e31 | ||
|
|
4b1460d938 | ||
|
|
f2aec524d2 | ||
|
|
1a97ca137c | ||
|
|
2b6f457ae6 | ||
|
|
3f4e0b4aa0 | ||
|
|
e30ded4de9 | ||
|
|
07242a84ff | ||
|
|
a2074dc0d8 | ||
|
|
28b6c85650 | ||
|
|
af300138eb | ||
|
|
2bf5725048 | ||
|
|
3d73141788 | ||
|
|
945ab29bc3 | ||
|
|
fe0f7dcf75 | ||
|
|
1a3ffbc8e2 | ||
|
|
71278b1898 | ||
|
|
0173c7abe1 | ||
|
|
8a50eea4ce | ||
|
|
b9891bb795 | ||
|
|
b903a88cd5 | ||
|
|
f3fcf2fedf | ||
|
|
a825f0874c | ||
|
|
8a5ca46418 |
@@ -53,113 +53,98 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="docs/images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -65,6 +65,7 @@ VAD:
|
||||
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
6、[修改代码后怎么编译自己的Docker镜像](./docker-build.md)<br/>
|
||||
|
||||
### 8、编译固件相关教程
|
||||
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
||||
@@ -85,6 +86,7 @@ VAD:
|
||||
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
|
||||
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
|
||||
13、[如何开启设备呼叫插件](./device-call-guide.md)<br/>
|
||||
14、[如何开启联网搜索功能](./web-search-integration.md)<br/>
|
||||
|
||||
### 10、数字人相关教程
|
||||
1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/>
|
||||
|
||||
@@ -1,21 +1,74 @@
|
||||
# 本地编译docker镜像方法
|
||||
|
||||
现在本项目已经使用github自动编译docker功能,本文档是提供给有本地编译docker镜像需求的朋友准备的。
|
||||
现在本项目已经使用`github`的`自动编译docker镜像`功能,如果您拉取的是项目发行的镜像,您没有自己编译镜像的需求,那就忽略本文档。
|
||||
|
||||
1、安装docker
|
||||
```
|
||||
如果您修改了源码,然后想采用`docker`的方式部署运行,可以参照以下步骤操作:
|
||||
|
||||
## 1、环境准备
|
||||
|
||||
安装docker:
|
||||
```bash
|
||||
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
```
|
||||
2、编译docker镜像
|
||||
```
|
||||
#进入项目根目录
|
||||
# 编译server
|
||||
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
|
||||
# 编译web
|
||||
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
|
||||
|
||||
# 编译完成后,可以使用docker-compose启动项目
|
||||
# docker-compose.yml你需要修改成自己编译的镜像版本
|
||||
## 2、编译镜像
|
||||
|
||||
当你修改好代码后,需要编译新的镜像时,需要按照以下步骤操作:
|
||||
|
||||
准备好你的`你的用户名`和`新的版本号`。
|
||||
- 这个`你的用户名`是你在`docker hub`注册的用户名,例如`xiaozhi`。当然,如果你不需要推送到`docker hub`,你可以自由定义。
|
||||
- 这个`新的版本号`是你编译的镜像版本,例如`1.2.3`,你可以根据需要自定义或者使用日期格式(例如`20260609`)主要是方便和现在运行的版本号做区分,同时也方便下次回忆你是什么时候构建的,不要和现在你本机运行的版本号相同。
|
||||
|
||||
进入`xiaozhi-esp32-server`项目根目录,编译 server 和 web 两个镜像:
|
||||
|
||||
```bash
|
||||
cd 项目根目录
|
||||
|
||||
# 编译server镜像
|
||||
docker build -f Dockerfile-server -t 你的用户名/xiaozhi-esp32-server:新的版本号 .
|
||||
|
||||
# 编译web镜像
|
||||
docker build -f Dockerfile-web -t 你的用户名/xiaozhi-esp32-server-web:新的版本号 .
|
||||
|
||||
```
|
||||
|
||||
## 3、修改docker-compose配置
|
||||
|
||||
```bash
|
||||
cd main/xiaozhi-server
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
编辑 `docker-compose_all.yml` 文件,将镜像版本替换为你刚才编译的版本:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
xiaozhi-esp32-server:
|
||||
image: 你的用户名/xiaozhi-esp32-server:新的版本号 # 修改为你的镜像地址
|
||||
...
|
||||
|
||||
xiaozhi-esp32-server-web:
|
||||
image: 你的用户名/xiaozhi-esp32-server-web:新的版本号 #修改为你的镜像地址
|
||||
...
|
||||
```
|
||||
|
||||
## 4、重启服务
|
||||
|
||||
```bash
|
||||
# 停止旧容器
|
||||
docker compose -f docker-compose_all.yml down
|
||||
|
||||
# 启动新容器
|
||||
docker compose -f docker-compose_all.yml up -d
|
||||
```
|
||||
|
||||
## 5、验证
|
||||
|
||||
查看日志确认服务启动正常:
|
||||
|
||||
```bash
|
||||
# 查看server日志
|
||||
docker logs -f -n 50 xiaozhi-esp32-server
|
||||
|
||||
# 查看web日志
|
||||
docker logs -f -n 50 xiaozhi-esp32-server-web
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 108 KiB |
@@ -52,113 +52,98 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="../images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="../images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="../images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="../images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="../images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="../images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="../images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -52,113 +52,98 @@ Want to see the usage effects? Click the videos below 🎥
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="../images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="../images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="../images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="../images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="../images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="../images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="../images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -52,113 +52,98 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="Experiência de velocidade de resposta" src="../images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="Segredo da otimização de velocidade" src="../images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="Cenário médico complexo" src="../images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="Envio de comandos MQTT" src="../images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="Reconhecimento de impressão vocal" src="../images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="Controle de interruptores de eletrodomésticos" src="../images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="Ponto de acesso MCP" src="../images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Tarefas com múltiplos comandos" src="../images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Reproduzir música" src="../images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="Plugin de clima" src="../images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Interrupção em tempo real" src="../images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="Fotografar e identificar objetos" src="../images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Timbre de voz personalizado" src="../images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="Comunicação em cantonês" src="../images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="Transmissão de notícias" src="../images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -52,113 +52,98 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="../images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="../images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="../images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="../images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="../images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="../images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="../images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# 联网搜索插件使用指南
|
||||
|
||||
## 功能简介
|
||||
|
||||
联网搜索插件 `web_search` 支持在对话过程中实时联网搜索信息并返回结果。插件支持两个搜索源:秘塔(Metaso)和Tavily,用户可根据需要选择其中一个。
|
||||
|
||||
## API Key申请指南
|
||||
|
||||
目前我们适配了`秘塔搜索`和`Tavily搜索`。
|
||||
- Tavily搜索:每个月1000次免费额度。
|
||||
- 秘塔搜索:拥有较为优质的国内数据源。
|
||||
|
||||
## API Key申请指南
|
||||
|
||||
### 方式一:使用秘塔搜索
|
||||
|
||||
- 访问 [秘塔搜索API](https://metaso.cn/search-api/api-keys),注册并登录账号
|
||||
- 在API密钥管理页面,点击"创建新的Key"
|
||||
- 复制生成的API Key(以 `mk-` 为前缀),这是配置所需的关键信息
|
||||
|
||||
### 方式二:使用Tavily搜索
|
||||
|
||||
- 访问 [Tavily控制台](https://app.tavily.com/home),注册并登录账号
|
||||
- 在控制台中创建API Key
|
||||
- 复制生成的API Key(以 `tvly-` 为前缀),这是配置所需的关键信息
|
||||
|
||||
## 配置方式
|
||||
|
||||
### 方式1. 使用智控台部署(推荐)
|
||||
|
||||
- 登录智控台
|
||||
- 进入"配置角色"页面,选择要配置的智能体
|
||||
- 点击"编辑功能"按钮,在右侧参数配置区域找到"联网搜索"插件
|
||||
- 勾选"联网搜索"
|
||||
- 填入搜索源(`metaso`或`tavily`),并将对应的`API Key`填入配置项
|
||||
- 保存配置,再保存智能体配置
|
||||
|
||||
### 方式2. 单模块xiaozhi-server部署
|
||||
|
||||
在 `data/.config.yaml` 中配置:
|
||||
|
||||
- 将搜索源填入 `provider`,可选值为 `metaso` 或 `tavily`
|
||||
- 将申请到的API Key填入 `api_key`
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
web_search:
|
||||
provider: "metaso"
|
||||
api_key: "你的API Key"
|
||||
```
|
||||
|
||||
如需自定义返回结果数量和工具描述,可额外配置 `max_results` 和 `description`:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
web_search:
|
||||
provider: "metaso"
|
||||
description: "联网搜索工具。当用户明确需要联网搜索问题时使用此工具。"
|
||||
max_results: 5
|
||||
api_key: "你的API Key"
|
||||
```
|
||||
|
||||
同时在 `functions` 列表中确保已启用 `web_search`:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
functions:
|
||||
- web_search
|
||||
```
|
||||
|
||||
配置完成后重启服务即可生效。
|
||||
@@ -32,6 +32,7 @@
|
||||
<liquibase-core.version>4.20.0</liquibase-core.version>
|
||||
<aliyun-sms-version>4.1.0</aliyun-sms-version>
|
||||
<okio-version>3.4.0</okio-version>
|
||||
<skipTests>true</skipTests>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -275,7 +276,7 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
@@ -324,7 +324,7 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.9.4";
|
||||
public static final String VERSION = "0.9.5";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package xiaozhi.modules.agent.Enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import lombok.Getter;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
|
||||
@Getter
|
||||
public enum AgentSnapshotField {
|
||||
AGENT_CODE("agentCode", AgentSnapshotDataDTO::getAgentCode, AgentUpdateDTO::getAgentCode,
|
||||
(agent, data) -> agent.setAgentCode(data.getAgentCode())),
|
||||
AGENT_NAME("agentName", AgentSnapshotDataDTO::getAgentName, AgentUpdateDTO::getAgentName,
|
||||
(agent, data) -> agent.setAgentName(data.getAgentName())),
|
||||
ASR_MODEL_ID("asrModelId", AgentSnapshotDataDTO::getAsrModelId, AgentUpdateDTO::getAsrModelId,
|
||||
(agent, data) -> agent.setAsrModelId(data.getAsrModelId())),
|
||||
VAD_MODEL_ID("vadModelId", AgentSnapshotDataDTO::getVadModelId, AgentUpdateDTO::getVadModelId,
|
||||
(agent, data) -> agent.setVadModelId(data.getVadModelId())),
|
||||
LLM_MODEL_ID("llmModelId", AgentSnapshotDataDTO::getLlmModelId, AgentUpdateDTO::getLlmModelId,
|
||||
(agent, data) -> agent.setLlmModelId(data.getLlmModelId())),
|
||||
SLM_MODEL_ID("slmModelId", AgentSnapshotDataDTO::getSlmModelId, AgentUpdateDTO::getSlmModelId,
|
||||
(agent, data) -> agent.setSlmModelId(data.getSlmModelId())),
|
||||
VLLM_MODEL_ID("vllmModelId", AgentSnapshotDataDTO::getVllmModelId, AgentUpdateDTO::getVllmModelId,
|
||||
(agent, data) -> agent.setVllmModelId(data.getVllmModelId())),
|
||||
TTS_MODEL_ID("ttsModelId", AgentSnapshotDataDTO::getTtsModelId, AgentUpdateDTO::getTtsModelId,
|
||||
(agent, data) -> agent.setTtsModelId(data.getTtsModelId())),
|
||||
TTS_VOICE_ID("ttsVoiceId", AgentSnapshotDataDTO::getTtsVoiceId, AgentUpdateDTO::getTtsVoiceId,
|
||||
(agent, data) -> agent.setTtsVoiceId(data.getTtsVoiceId())),
|
||||
TTS_LANGUAGE("ttsLanguage", AgentSnapshotDataDTO::getTtsLanguage, AgentUpdateDTO::getTtsLanguage,
|
||||
(agent, data) -> agent.setTtsLanguage(data.getTtsLanguage())),
|
||||
TTS_VOLUME("ttsVolume", AgentSnapshotDataDTO::getTtsVolume, AgentUpdateDTO::getTtsVolume,
|
||||
(agent, data) -> agent.setTtsVolume(data.getTtsVolume())),
|
||||
TTS_RATE("ttsRate", AgentSnapshotDataDTO::getTtsRate, AgentUpdateDTO::getTtsRate,
|
||||
(agent, data) -> agent.setTtsRate(data.getTtsRate())),
|
||||
TTS_PITCH("ttsPitch", AgentSnapshotDataDTO::getTtsPitch, AgentUpdateDTO::getTtsPitch,
|
||||
(agent, data) -> agent.setTtsPitch(data.getTtsPitch())),
|
||||
MEM_MODEL_ID("memModelId", AgentSnapshotDataDTO::getMemModelId, AgentUpdateDTO::getMemModelId,
|
||||
(agent, data) -> agent.setMemModelId(data.getMemModelId())),
|
||||
INTENT_MODEL_ID("intentModelId", AgentSnapshotDataDTO::getIntentModelId, AgentUpdateDTO::getIntentModelId,
|
||||
(agent, data) -> agent.setIntentModelId(data.getIntentModelId())),
|
||||
CHAT_HISTORY_CONF("chatHistoryConf", AgentSnapshotDataDTO::getChatHistoryConf, AgentUpdateDTO::getChatHistoryConf,
|
||||
(agent, data) -> agent.setChatHistoryConf(data.getChatHistoryConf())),
|
||||
SYSTEM_PROMPT("systemPrompt", AgentSnapshotDataDTO::getSystemPrompt, AgentUpdateDTO::getSystemPrompt,
|
||||
(agent, data) -> agent.setSystemPrompt(data.getSystemPrompt())),
|
||||
SUMMARY_MEMORY("summaryMemory", AgentSnapshotDataDTO::getSummaryMemory, AgentUpdateDTO::getSummaryMemory,
|
||||
(agent, data) -> agent.setSummaryMemory(data.getSummaryMemory())),
|
||||
LANG_CODE("langCode", AgentSnapshotDataDTO::getLangCode, AgentUpdateDTO::getLangCode,
|
||||
(agent, data) -> agent.setLangCode(data.getLangCode())),
|
||||
LANGUAGE("language", AgentSnapshotDataDTO::getLanguage, AgentUpdateDTO::getLanguage,
|
||||
(agent, data) -> agent.setLanguage(data.getLanguage())),
|
||||
SORT("sort", AgentSnapshotDataDTO::getSort, AgentUpdateDTO::getSort,
|
||||
(agent, data) -> agent.setSort(data.getSort())),
|
||||
FUNCTIONS("functions", AgentSnapshotDataDTO::getFunctions, AgentUpdateDTO::getFunctions, null),
|
||||
CONTEXT_PROVIDERS("contextProviders", AgentSnapshotDataDTO::getContextProviders,
|
||||
AgentUpdateDTO::getContextProviders, null),
|
||||
CORRECT_WORD_FILE_IDS("correctWordFileIds", AgentSnapshotDataDTO::getCorrectWordFileIds,
|
||||
AgentUpdateDTO::getCorrectWordFileIds, null),
|
||||
TAG_NAMES("tagNames", AgentSnapshotDataDTO::getTagNames, AgentUpdateDTO::getTagNames, null);
|
||||
|
||||
private final String fieldName;
|
||||
private final Function<AgentSnapshotDataDTO, Object> snapshotGetter;
|
||||
private final Function<AgentUpdateDTO, Object> updateGetter;
|
||||
private final BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier;
|
||||
|
||||
AgentSnapshotField(String fieldName, Function<AgentSnapshotDataDTO, Object> snapshotGetter,
|
||||
Function<AgentUpdateDTO, Object> updateGetter,
|
||||
BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier) {
|
||||
this.fieldName = fieldName;
|
||||
this.snapshotGetter = snapshotGetter;
|
||||
this.updateGetter = updateGetter;
|
||||
this.restoreApplier = restoreApplier;
|
||||
}
|
||||
|
||||
public static List<String> names() {
|
||||
return Arrays.stream(values()).map(AgentSnapshotField::getFieldName).toList();
|
||||
}
|
||||
|
||||
public static String canonicalName(String fieldName) {
|
||||
return "tags".equals(fieldName) ? TAG_NAMES.getFieldName() : fieldName;
|
||||
}
|
||||
|
||||
public Object snapshotValue(AgentSnapshotDataDTO data) {
|
||||
return data == null ? null : snapshotGetter.apply(data);
|
||||
}
|
||||
|
||||
public Object updateValue(AgentUpdateDTO data) {
|
||||
return data == null ? null : updateGetter.apply(data);
|
||||
}
|
||||
|
||||
public boolean isRestorableAgentField() {
|
||||
return restoreApplier != null;
|
||||
}
|
||||
|
||||
public void applyTo(AgentEntity agent, AgentSnapshotDataDTO data) {
|
||||
if (restoreApplier != null) {
|
||||
restoreApplier.accept(agent, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,11 +48,8 @@ import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
@@ -69,12 +66,9 @@ public class AgentController {
|
||||
private final DeviceService deviceService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentChatSummaryService agentChatSummaryService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final AgentTagService agentTagService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取用户智能体列表")
|
||||
@@ -127,7 +121,7 @@ public class AgentController {
|
||||
}
|
||||
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
|
||||
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
|
||||
agentService.updateAgentById(device.getAgentId(), agentUpdateDTO);
|
||||
agentService.updateAgentById(device.getAgentId(), agentUpdateDTO, false);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@@ -171,18 +165,7 @@ public class AgentController {
|
||||
@Operation(summary = "删除智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
// 先删除关联的设备
|
||||
deviceService.deleteByAgentId(id);
|
||||
// 删除关联的聊天记录
|
||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||
// 删除关联的插件
|
||||
agentPluginMappingService.deleteByAgentId(id);
|
||||
// 删除关联的上下文源配置
|
||||
agentContextProviderService.deleteByAgentId(id);
|
||||
// 删除关联的替换词文件关联记录
|
||||
correctWordFileService.deleteMappingsByAgentId(id);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
agentService.deleteAgent(id);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@@ -332,8 +315,11 @@ public class AgentController {
|
||||
public Result<Void> saveAgentTags(@PathVariable String id, @RequestBody Map<String, Object> params) {
|
||||
List<String> tagIds = (List<String>) params.get("tagIds");
|
||||
List<String> tagNames = (List<String>) params.get("tagNames");
|
||||
agentTagService.saveAgentTags(id, tagIds, tagNames);
|
||||
AgentUpdateDTO dto = new AgentUpdateDTO();
|
||||
dto.setTagIds(tagIds);
|
||||
dto.setTagNames(tagNames);
|
||||
agentService.updateAgentById(id, dto);
|
||||
return new Result<Void>().ok(null);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package xiaozhi.modules.agent.controller;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Tag(name = "智能体快照")
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/agent/{agentId}/snapshots")
|
||||
public class AgentSnapshotController {
|
||||
private final AgentSnapshotService agentSnapshotService;
|
||||
private final AgentService agentService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取智能体快照列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<PageData<AgentSnapshotVO>> page(
|
||||
@PathVariable String agentId,
|
||||
@ParameterObject AgentSnapshotPageDTO params) {
|
||||
checkPermission(agentId);
|
||||
return new Result<PageData<AgentSnapshotVO>>().ok(agentSnapshotService.page(agentId, params));
|
||||
}
|
||||
|
||||
@GetMapping("/{snapshotId}")
|
||||
@Operation(summary = "获取智能体快照详情")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<AgentSnapshotVO> getSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
|
||||
checkPermission(agentId);
|
||||
return new Result<AgentSnapshotVO>().ok(agentSnapshotService.getSnapshot(agentId, snapshotId));
|
||||
}
|
||||
|
||||
@PostMapping("/{snapshotId}/restore")
|
||||
@Operation(summary = "恢复智能体快照")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> restore(@PathVariable String agentId, @PathVariable String snapshotId) {
|
||||
checkPermission(agentId);
|
||||
agentSnapshotService.restoreSnapshot(agentId, snapshotId);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{snapshotId}")
|
||||
@Operation(summary = "删除智能体历史快照")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> deleteSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
|
||||
checkPermission(agentId);
|
||||
agentSnapshotService.deleteSnapshot(agentId, snapshotId);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
private void checkPermission(String agentId) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
if (user == null || !agentService.checkAgentPermission(agentId, user.getId())) {
|
||||
throw new RenException("没有权限访问该智能体快照");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,11 @@ public interface AgentDao extends BaseDao<AgentEntity> {
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId);
|
||||
|
||||
/**
|
||||
* 锁定智能体主记录,用于串行化同一智能体的配置写入
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
AgentEntity selectByIdForUpdate(@Param("agentId") String agentId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
|
||||
@Mapper
|
||||
public interface AgentSnapshotDao extends BaseDao<AgentSnapshotEntity> {
|
||||
Integer selectMaxVersionNo(@Param("agentId") String agentId);
|
||||
|
||||
AgentSnapshotEntity selectLatestSnapshot(@Param("agentId") String agentId);
|
||||
|
||||
AgentSnapshotEntity selectNextSnapshot(@Param("agentId") String agentId, @Param("versionNo") Integer versionNo);
|
||||
|
||||
int insertWithNextVersion(@Param("snapshot") AgentSnapshotEntity snapshot);
|
||||
|
||||
int deleteOlderThanKeepLimit(@Param("agentId") String agentId, @Param("keepLimit") int keepLimit);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体快照数据")
|
||||
public class AgentSnapshotDataDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private String asrModelId;
|
||||
private String vadModelId;
|
||||
private String llmModelId;
|
||||
private String slmModelId;
|
||||
private String vllmModelId;
|
||||
private String ttsModelId;
|
||||
private String ttsVoiceId;
|
||||
private String ttsLanguage;
|
||||
private Integer ttsVolume;
|
||||
private Integer ttsRate;
|
||||
private Integer ttsPitch;
|
||||
private String memModelId;
|
||||
private String intentModelId;
|
||||
private Integer chatHistoryConf;
|
||||
private String systemPrompt;
|
||||
private String summaryMemory;
|
||||
private String langCode;
|
||||
private String language;
|
||||
private Integer sort;
|
||||
private List<AgentUpdateDTO.FunctionInfo> functions;
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
private List<String> correctWordFileIds;
|
||||
private List<String> tagNames;
|
||||
private List<AgentSnapshotTagDTO> tags;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体快照分页查询参数")
|
||||
public class AgentSnapshotPageDTO {
|
||||
@Schema(description = "当前页码,从1开始", example = "1")
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(description = "每页数量", example = "10")
|
||||
private Integer limit = 10;
|
||||
|
||||
@Schema(description = "版本锚点,只查询小于等于该版本号的历史快照", example = "20")
|
||||
private Integer maxVersionNo;
|
||||
|
||||
public int pageOrDefault() {
|
||||
return page == null || page < 1 ? 1 : page;
|
||||
}
|
||||
|
||||
public int limitOrDefault() {
|
||||
if (limit == null || limit < 1) {
|
||||
return 10;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体快照标签")
|
||||
public class AgentSnapshotTagDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String id;
|
||||
private String tagName;
|
||||
private Integer sort;
|
||||
}
|
||||
@@ -4,9 +4,12 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* 智能体更新DTO
|
||||
@@ -91,15 +94,50 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "替换词文件ID列表", nullable = true)
|
||||
private List<String> correctWordFileIds;
|
||||
|
||||
@Schema(description = "标签名称列表", nullable = true)
|
||||
private List<String> tagNames;
|
||||
|
||||
@Schema(description = "标签ID列表", nullable = true)
|
||||
private List<String> tagIds;
|
||||
|
||||
@Data
|
||||
@Schema(description = "插件函数信息")
|
||||
public static class FunctionInfo implements Serializable {
|
||||
private static final TypeReference<HashMap<String, Object>> PARAM_INFO_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
@Schema(description = "插件ID", example = "plugin_01")
|
||||
private String pluginId;
|
||||
|
||||
@Schema(description = "函数参数信息", nullable = true)
|
||||
private HashMap<String, Object> paramInfo;
|
||||
private HashMap<String, Object> paramInfo = new HashMap<>();
|
||||
|
||||
public void setParamInfo(Object paramInfo) {
|
||||
this.paramInfo = normalizeParamInfo(paramInfo);
|
||||
}
|
||||
|
||||
private static HashMap<String, Object> normalizeParamInfo(Object paramInfo) {
|
||||
if (paramInfo == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
if (paramInfo instanceof String value) {
|
||||
if (value.trim().isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return JsonUtils.parseObject(value, PARAM_INFO_TYPE);
|
||||
}
|
||||
if (paramInfo instanceof Map<?, ?> value) {
|
||||
HashMap<String, Object> normalized = new HashMap<>();
|
||||
value.forEach((key, val) -> {
|
||||
if (key != null) {
|
||||
normalized.put(String.valueOf(key), val);
|
||||
}
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
return JsonUtils.parseObject(JsonUtils.toJsonString(paramInfo), PARAM_INFO_TYPE);
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* MCP JSON-RPC 响应 DTO
|
||||
*/
|
||||
@Data
|
||||
public class McpJsonRpcResponse {
|
||||
private String jsonrpc = "2.0";
|
||||
private Integer id;
|
||||
private McpResult result;
|
||||
private McpError error;
|
||||
|
||||
public McpJsonRpcResponse() {
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class McpResult {
|
||||
private String type;
|
||||
private String message;
|
||||
private String agent_id;
|
||||
private McpTool[] tools;
|
||||
|
||||
public McpResult() {
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class McpTool {
|
||||
private String name;
|
||||
private String description;
|
||||
private Object inputSchema;
|
||||
|
||||
public McpTool() {
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class McpError {
|
||||
private Integer code;
|
||||
private String message;
|
||||
private Object data;
|
||||
|
||||
public McpError() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
import xiaozhi.modules.agent.typehandler.ContextProviderListTypeHandler;
|
||||
|
||||
@Data
|
||||
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
|
||||
@@ -26,7 +26,7 @@ public class AgentContextProviderEntity {
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@TableField(typeHandler = ContextProviderListTypeHandler.class)
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ai_agent_snapshot")
|
||||
@Schema(description = "智能体配置快照")
|
||||
public class AgentSnapshotEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "快照ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "所属用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "版本号")
|
||||
private Integer versionNo;
|
||||
|
||||
@Schema(description = "快照数据JSON")
|
||||
private String snapshotData;
|
||||
|
||||
@Schema(description = "变更字段JSON")
|
||||
private String changedFields;
|
||||
|
||||
@Schema(description = "快照来源")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "恢复来源快照ID")
|
||||
private String restoreFromSnapshotId;
|
||||
|
||||
@Schema(description = "恢复来源版本号")
|
||||
private Integer restoreFromVersionNo;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
}
|
||||
@@ -50,6 +50,13 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
*/
|
||||
void deleteAgentByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 删除智能体及其关联数据
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAgent(String agentId);
|
||||
|
||||
/**
|
||||
* 获取用户智能体列表
|
||||
*
|
||||
@@ -93,6 +100,15 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
*/
|
||||
void updateAgentById(String agentId, AgentUpdateDTO dto);
|
||||
|
||||
/**
|
||||
* 更新智能体
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param dto 更新智能体所需的信息
|
||||
* @param createSnapshot 是否创建配置快照
|
||||
*/
|
||||
void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot);
|
||||
|
||||
/**
|
||||
* 创建智能体
|
||||
*
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
|
||||
public interface AgentSnapshotService extends BaseService<AgentSnapshotEntity> {
|
||||
void createSnapshot(String agentId, String source);
|
||||
|
||||
PageData<AgentSnapshotVO> page(String agentId, AgentSnapshotPageDTO params);
|
||||
|
||||
AgentSnapshotVO getSnapshot(String agentId, String snapshotId);
|
||||
|
||||
void restoreSnapshot(String agentId, String snapshotId);
|
||||
|
||||
void deleteSnapshot(String agentId, String snapshotId);
|
||||
|
||||
Integer getCurrentVersionNo(String agentId);
|
||||
|
||||
void deleteByAgentId(String agentId);
|
||||
}
|
||||
@@ -118,7 +118,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
{
|
||||
setSummaryMemory(summaryDTO.getSummary());
|
||||
}
|
||||
});
|
||||
}, false);
|
||||
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
|
||||
} else {
|
||||
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
||||
@@ -518,4 +518,4 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
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;
|
||||
@@ -43,9 +42,10 @@ import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
@@ -73,9 +73,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentTagService agentTagService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentTagService agentTagService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
private final AgentSnapshotService agentSnapshotService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
@@ -106,12 +107,13 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
agent.setContextProviders(contextProviderEntity.getContextProviders());
|
||||
}
|
||||
|
||||
// 查询替换词文件ID列表
|
||||
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
|
||||
agent.setCorrectWordFileIds(correctWordFileIds);
|
||||
|
||||
// 无需额外查询插件列表,已通过SQL查询出来
|
||||
return agent;
|
||||
// 查询替换词文件ID列表
|
||||
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
|
||||
agent.setCorrectWordFileIds(correctWordFileIds);
|
||||
agent.setCurrentVersionNo(agentSnapshotService.getCurrentVersionNo(id));
|
||||
|
||||
// 无需额外查询插件列表,已通过SQL查询出来
|
||||
return agent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,15 +136,35 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
return super.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAgentByUserId(Long userId) {
|
||||
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
baseDao.delete(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType) {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteAgentByUserId(Long userId) {
|
||||
List<AgentEntity> agents = baseDao.selectList(new QueryWrapper<AgentEntity>()
|
||||
.select("id")
|
||||
.eq("user_id", userId));
|
||||
for (AgentEntity agent : agents) {
|
||||
deleteAgent(agent.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteAgent(String agentId) {
|
||||
if (agentDao.selectByIdForUpdate(agentId) == null) {
|
||||
return;
|
||||
}
|
||||
deviceService.deleteByAgentId(agentId);
|
||||
agentChatHistoryService.deleteByAgentId(agentId, true, true);
|
||||
agentPluginMappingService.deleteByAgentId(agentId);
|
||||
agentContextProviderService.deleteByAgentId(agentId);
|
||||
correctWordFileService.deleteMappingsByAgentId(agentId);
|
||||
agentTagService.deleteAgentTags(agentId);
|
||||
agentSnapshotService.deleteByAgentId(agentId);
|
||||
deleteById(agentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType) {
|
||||
QueryWrapper<AgentEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId).orderByDesc("created_at");
|
||||
|
||||
@@ -272,20 +294,31 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
return userId.equals(agent.getUserId());
|
||||
}
|
||||
|
||||
// 根据id更新智能体信息
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
|
||||
// 先查询现有实体
|
||||
AgentEntity existingEntity = this.getAgentById(agentId);
|
||||
if (existingEntity == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 只更新提供的非空字段
|
||||
if (dto.getAgentName() != null) {
|
||||
existingEntity.setAgentName(dto.getAgentName());
|
||||
}
|
||||
// 根据id更新智能体信息
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
|
||||
updateAgentById(agentId, dto, true);
|
||||
}
|
||||
|
||||
// 根据id更新智能体信息
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot) {
|
||||
if (agentDao.selectByIdForUpdate(agentId) == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 锁定后查询现有实体和关联配置
|
||||
AgentEntity existingEntity = this.getAgentById(agentId);
|
||||
if (createSnapshot && agentSnapshotService.getCurrentVersionNo(agentId) == 0) {
|
||||
agentSnapshotService.createSnapshot(agentId, "initial");
|
||||
}
|
||||
|
||||
// 只更新提供的非空字段
|
||||
if (dto.getAgentName() != null) {
|
||||
existingEntity.setAgentName(dto.getAgentName());
|
||||
}
|
||||
if (dto.getAgentCode() != null) {
|
||||
existingEntity.setAgentCode(dto.getAgentCode());
|
||||
}
|
||||
@@ -426,16 +459,24 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
}
|
||||
|
||||
// 更新替换词文件关联
|
||||
if (dto.getCorrectWordFileIds() != null) {
|
||||
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
|
||||
}
|
||||
|
||||
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
||||
if (!b) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
this.updateById(existingEntity);
|
||||
}
|
||||
if (dto.getCorrectWordFileIds() != null) {
|
||||
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
|
||||
}
|
||||
|
||||
// 更新智能体标签
|
||||
if (dto.getTagNames() != null || dto.getTagIds() != null) {
|
||||
agentTagService.saveAgentTags(agentId, dto.getTagIds(), dto.getTagNames());
|
||||
}
|
||||
|
||||
boolean b = validateLLMIntentParams(existingEntity.getLlmModelId(), existingEntity.getIntentModelId());
|
||||
if (!b) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
this.updateById(existingEntity);
|
||||
if (createSnapshot) {
|
||||
agentSnapshotService.createSnapshot(agentId, "config");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证大语言模型和意图识别的参数是否符合匹配
|
||||
@@ -576,4 +617,4 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,759 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dao.AgentSnapshotDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTagDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTagRelationDao;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotTagDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTagRelationEntity;
|
||||
import xiaozhi.modules.agent.Enums.AgentSnapshotField;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class AgentSnapshotServiceImpl extends BaseServiceImpl<AgentSnapshotDao, AgentSnapshotEntity>
|
||||
implements AgentSnapshotService {
|
||||
private static final int MAX_SNAPSHOTS_PER_AGENT = 100;
|
||||
private static final TypeReference<List<String>> STRING_LIST_TYPE = new TypeReference<>() {
|
||||
};
|
||||
private static final TypeReference<HashMap<String, Object>> PARAM_INFO_TYPE = new TypeReference<>() {
|
||||
};
|
||||
private static final TypeReference<Map<String, Object>> OBJECT_MAP_TYPE = new TypeReference<>() {
|
||||
};
|
||||
private static final String SECRET_PLACEHOLDER = "__SNAPSHOT_SECRET_REDACTED__";
|
||||
|
||||
private final AgentSnapshotDao agentSnapshotDao;
|
||||
private final AgentDao agentDao;
|
||||
private final AgentTagDao agentTagDao;
|
||||
private final AgentTagRelationDao agentTagRelationDao;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentTagService agentTagService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void createSnapshot(String agentId, String source) {
|
||||
lockAgent(agentId);
|
||||
AgentInfoVO agent = getAgentInfo(agentId);
|
||||
AgentSnapshotDataDTO snapshotData = buildSnapshotData(agent);
|
||||
AgentSnapshotEntity previousSnapshot = agentSnapshotDao.selectLatestSnapshot(agentId);
|
||||
List<String> changedFields;
|
||||
if (previousSnapshot == null) {
|
||||
changedFields = List.of("initial");
|
||||
} else {
|
||||
AgentSnapshotDataDTO previousData = JsonUtils.parseObject(previousSnapshot.getSnapshotData(),
|
||||
AgentSnapshotDataDTO.class);
|
||||
changedFields = getChangedFields(previousData, snapshotData);
|
||||
}
|
||||
if (changedFields.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertSnapshot(agentId, agent.getUserId(), source, snapshotData, changedFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PageData<AgentSnapshotVO> page(String agentId, AgentSnapshotPageDTO params) {
|
||||
AgentSnapshotPageDTO pageParams = params == null ? new AgentSnapshotPageDTO() : params;
|
||||
Page<AgentSnapshotEntity> page = new Page<>(pageParams.pageOrDefault(), pageParams.limitOrDefault());
|
||||
page.addOrder(OrderItem.desc("version_no"));
|
||||
IPage<AgentSnapshotEntity> result = agentSnapshotDao.selectPage(page,
|
||||
new QueryWrapper<AgentSnapshotEntity>()
|
||||
.eq("agent_id", agentId)
|
||||
.le(pageParams.getMaxVersionNo() != null, "version_no", pageParams.getMaxVersionNo()));
|
||||
List<AgentSnapshotVO> list = result.getRecords().stream()
|
||||
.map(entity -> toVO(entity, false))
|
||||
.toList();
|
||||
return new PageData<>(list, result.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentSnapshotVO getSnapshot(String agentId, String snapshotId) {
|
||||
AgentSnapshotEntity entity = getSnapshotEntity(agentId, snapshotId);
|
||||
return toVO(entity, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void restoreSnapshot(String agentId, String snapshotId) {
|
||||
AgentEntity agent = agentDao.selectByIdForUpdate(agentId);
|
||||
if (agent == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
AgentSnapshotEntity snapshot = getSnapshotEntity(agentId, snapshotId);
|
||||
AgentSnapshotDataDTO data = JsonUtils.parseObject(snapshot.getSnapshotData(), AgentSnapshotDataDTO.class);
|
||||
if (data == null) {
|
||||
throw new RenException("快照数据为空,无法恢复");
|
||||
}
|
||||
|
||||
AgentInfoVO currentAgent = getAgentInfo(agentId);
|
||||
AgentSnapshotDataDTO currentData = buildSnapshotData(currentAgent);
|
||||
AgentSnapshotDataDTO restoreData = preserveCurrentSensitiveValues(data, currentData);
|
||||
List<String> changedFields = getChangedFields(currentData, restoreData);
|
||||
if (changedFields.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyAgentFields(agent, restoreData);
|
||||
validateRestoreParams(agent);
|
||||
applyMemoryPolicy(agent);
|
||||
agent.setUpdater(SecurityUser.getUserId());
|
||||
agent.setUpdatedAt(new Date());
|
||||
agentDao.updateById(agent);
|
||||
|
||||
restoreFunctions(agentId, restoreData.getFunctions());
|
||||
restoreContextProviders(agentId, restoreData.getContextProviders());
|
||||
correctWordFileService.saveAgentCorrectWords(agentId, nullToEmpty(restoreData.getCorrectWordFileIds()));
|
||||
restoreTags(agentId, restoreData);
|
||||
insertSnapshot(agentId, currentAgent.getUserId(), "restore", restoreData, changedFields,
|
||||
snapshot.getId(), snapshot.getVersionNo());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteSnapshot(String agentId, String snapshotId) {
|
||||
AgentSnapshotEntity entity = getSnapshotEntity(agentId, snapshotId);
|
||||
Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(agentId);
|
||||
if (Objects.equals(entity.getVersionNo(), maxVersionNo)) {
|
||||
throw new RenException("最新历史版本不能删除");
|
||||
}
|
||||
deleteById(snapshotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getCurrentVersionNo(String agentId) {
|
||||
Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(agentId);
|
||||
return maxVersionNo == null ? 0 : maxVersionNo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteByAgentId(String agentId) {
|
||||
agentSnapshotDao.delete(new QueryWrapper<AgentSnapshotEntity>().eq("agent_id", agentId));
|
||||
}
|
||||
|
||||
private void insertSnapshot(String agentId, Long userId, String source, AgentSnapshotDataDTO snapshotData,
|
||||
List<String> changedFields) {
|
||||
insertSnapshot(agentId, userId, source, snapshotData, changedFields, null, null);
|
||||
}
|
||||
|
||||
private void insertSnapshot(String agentId, Long userId, String source, AgentSnapshotDataDTO snapshotData,
|
||||
List<String> changedFields, String restoreFromSnapshotId, Integer restoreFromVersionNo) {
|
||||
AgentSnapshotEntity entity = new AgentSnapshotEntity();
|
||||
entity.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
entity.setAgentId(agentId);
|
||||
entity.setUserId(userId);
|
||||
entity.setSnapshotData(JsonUtils.toJsonString(redactSnapshotData(snapshotData)));
|
||||
entity.setChangedFields(JsonUtils.toJsonString(changedFields));
|
||||
entity.setSource(StringUtils.defaultIfBlank(source, "config"));
|
||||
entity.setRestoreFromSnapshotId(restoreFromSnapshotId);
|
||||
entity.setRestoreFromVersionNo(restoreFromVersionNo);
|
||||
entity.setCreator(SecurityUser.getUserId());
|
||||
entity.setCreatedAt(new Date());
|
||||
int inserted = agentSnapshotDao.insertWithNextVersion(entity);
|
||||
if (inserted != 1) {
|
||||
throw new RenException("快照版本号生成失败");
|
||||
}
|
||||
pruneSnapshots(agentId);
|
||||
}
|
||||
|
||||
private void lockAgent(String agentId) {
|
||||
if (agentDao.selectByIdForUpdate(agentId) == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
private void pruneSnapshots(String agentId) {
|
||||
agentSnapshotDao.deleteOlderThanKeepLimit(agentId, MAX_SNAPSHOTS_PER_AGENT);
|
||||
}
|
||||
|
||||
private AgentSnapshotEntity getSnapshotEntity(String agentId, String snapshotId) {
|
||||
AgentSnapshotEntity entity = selectById(snapshotId);
|
||||
if (entity == null || !Objects.equals(agentId, entity.getAgentId())) {
|
||||
throw new RenException("快照不存在");
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private AgentInfoVO getAgentInfo(String agentId) {
|
||||
AgentInfoVO agent = agentDao.selectAgentInfoById(agentId);
|
||||
if (agent == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO buildSnapshotData(String agentId) {
|
||||
return buildSnapshotData(getAgentInfo(agentId));
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO buildSnapshotData(AgentInfoVO agent) {
|
||||
AgentSnapshotDataDTO data = new AgentSnapshotDataDTO();
|
||||
data.setAgentCode(agent.getAgentCode());
|
||||
data.setAgentName(agent.getAgentName());
|
||||
data.setAsrModelId(agent.getAsrModelId());
|
||||
data.setVadModelId(agent.getVadModelId());
|
||||
data.setLlmModelId(agent.getLlmModelId());
|
||||
data.setSlmModelId(agent.getSlmModelId());
|
||||
data.setVllmModelId(agent.getVllmModelId());
|
||||
data.setTtsModelId(agent.getTtsModelId());
|
||||
data.setTtsVoiceId(agent.getTtsVoiceId());
|
||||
data.setTtsLanguage(agent.getTtsLanguage());
|
||||
data.setTtsVolume(agent.getTtsVolume());
|
||||
data.setTtsRate(agent.getTtsRate());
|
||||
data.setTtsPitch(agent.getTtsPitch());
|
||||
data.setMemModelId(agent.getMemModelId());
|
||||
data.setIntentModelId(agent.getIntentModelId());
|
||||
data.setChatHistoryConf(agent.getChatHistoryConf());
|
||||
data.setSystemPrompt(agent.getSystemPrompt());
|
||||
data.setSummaryMemory(agent.getSummaryMemory());
|
||||
data.setLangCode(agent.getLangCode());
|
||||
data.setLanguage(agent.getLanguage());
|
||||
data.setSort(agent.getSort());
|
||||
data.setFunctions(toFunctionInfo(agent.getFunctions()));
|
||||
data.setContextProviders(getContextProviders(agent.getId()));
|
||||
data.setCorrectWordFileIds(nullToEmpty(correctWordFileService.getAgentCorrectWordFileIds(agent.getId())));
|
||||
List<AgentSnapshotTagDTO> tags = getSnapshotTags(agent.getId());
|
||||
data.setTags(tags);
|
||||
data.setTagNames(tags.stream().map(AgentSnapshotTagDTO::getTagName).toList());
|
||||
return data;
|
||||
}
|
||||
|
||||
private List<AgentSnapshotTagDTO> getSnapshotTags(String agentId) {
|
||||
List<AgentTagEntity> tags = agentTagDao.selectByAgentId(agentId);
|
||||
if (tags == null || tags.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<AgentSnapshotTagDTO> snapshotTags = new ArrayList<>();
|
||||
for (int i = 0; i < tags.size(); i++) {
|
||||
AgentTagEntity tag = tags.get(i);
|
||||
AgentSnapshotTagDTO snapshotTag = new AgentSnapshotTagDTO();
|
||||
snapshotTag.setId(tag.getId());
|
||||
snapshotTag.setTagName(tag.getTagName());
|
||||
snapshotTag.setSort(i);
|
||||
snapshotTags.add(snapshotTag);
|
||||
}
|
||||
return snapshotTags;
|
||||
}
|
||||
|
||||
private List<AgentUpdateDTO.FunctionInfo> toFunctionInfo(List<AgentPluginMapping> mappings) {
|
||||
if (mappings == null || mappings.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return mappings.stream().map(mapping -> {
|
||||
AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo();
|
||||
info.setPluginId(mapping.getPluginId());
|
||||
info.setParamInfo(parseParamInfo(mapping.getParamInfo()));
|
||||
return info;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private HashMap<String, Object> parseParamInfo(String paramInfo) {
|
||||
if (StringUtils.isBlank(paramInfo)) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return JsonUtils.parseObject(paramInfo, PARAM_INFO_TYPE);
|
||||
}
|
||||
|
||||
private List<xiaozhi.modules.agent.dto.ContextProviderDTO> getContextProviders(String agentId) {
|
||||
AgentContextProviderEntity contextEntity = agentContextProviderService.getByAgentId(agentId);
|
||||
if (contextEntity == null || contextEntity.getContextProviders() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return contextEntity.getContextProviders();
|
||||
}
|
||||
|
||||
private List<String> getChangedFields(AgentSnapshotDataDTO current, AgentUpdateDTO pendingUpdate) {
|
||||
if (pendingUpdate == null) {
|
||||
return List.of("restore");
|
||||
}
|
||||
|
||||
List<String> fields = new ArrayList<>();
|
||||
for (AgentSnapshotField field : AgentSnapshotField.values()) {
|
||||
Object nextValue = field.updateValue(pendingUpdate);
|
||||
if (nextValue != null && isChanged(field, field.snapshotValue(current), nextValue)) {
|
||||
fields.add(field.getFieldName());
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private List<String> getChangedFields(AgentSnapshotDataDTO current, AgentSnapshotDataDTO next) {
|
||||
if (next == null) {
|
||||
return List.of("restore");
|
||||
}
|
||||
if (current == null) {
|
||||
current = new AgentSnapshotDataDTO();
|
||||
}
|
||||
|
||||
List<String> fields = new ArrayList<>();
|
||||
for (AgentSnapshotField field : AgentSnapshotField.values()) {
|
||||
if (isChanged(field, field.snapshotValue(current), field.snapshotValue(next))) {
|
||||
fields.add(field.getFieldName());
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private boolean isChanged(AgentSnapshotField field, Object current, Object next) {
|
||||
return !Objects.equals(normalizeForCompare(field, current), normalizeForCompare(field, next));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object normalizeForCompare(AgentSnapshotField field, Object value) {
|
||||
return switch (field) {
|
||||
case FUNCTIONS -> normalizeFunctions((List<AgentUpdateDTO.FunctionInfo>) value);
|
||||
case CONTEXT_PROVIDERS -> normalizeSortedJsonList((List<?>) value);
|
||||
case CORRECT_WORD_FILE_IDS -> normalizeStringList((List<String>) value);
|
||||
case TAG_NAMES -> normalizeStringList((List<String>) value);
|
||||
default -> value;
|
||||
};
|
||||
}
|
||||
|
||||
private Map<String, Object> normalizeFunctions(List<AgentUpdateDTO.FunctionInfo> functions) {
|
||||
Map<String, Object> result = new TreeMap<>();
|
||||
if (functions == null) {
|
||||
return result;
|
||||
}
|
||||
for (AgentUpdateDTO.FunctionInfo function : functions) {
|
||||
if (function == null || StringUtils.isBlank(function.getPluginId())) {
|
||||
continue;
|
||||
}
|
||||
result.put(function.getPluginId(), normalizeMap(function.getParamInfo()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<String> normalizeStringList(List<String> values) {
|
||||
if (values == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return values.stream()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private <T> List<String> normalizeSortedJsonList(List<T> values) {
|
||||
if (values == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return values.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(value -> JsonUtils.toJsonString(normalizeValue(value)))
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Object normalizeValue(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
return normalizeMap(map);
|
||||
}
|
||||
if (value instanceof List<?> list) {
|
||||
return list.stream().map(this::normalizeValue).toList();
|
||||
}
|
||||
if (isScalarValue(value)) {
|
||||
return value;
|
||||
}
|
||||
return normalizeMap(JsonUtils.parseObject(JsonUtils.toJsonString(value), OBJECT_MAP_TYPE));
|
||||
}
|
||||
|
||||
private boolean isScalarValue(Object value) {
|
||||
return value instanceof CharSequence
|
||||
|| value instanceof Number
|
||||
|| value instanceof Boolean
|
||||
|| value instanceof Character
|
||||
|| value instanceof Enum<?>
|
||||
|| value instanceof Date;
|
||||
}
|
||||
|
||||
private Map<String, Object> normalizeMap(Map<?, ?> map) {
|
||||
Map<String, Object> result = new TreeMap<>();
|
||||
if (map == null) {
|
||||
return result;
|
||||
}
|
||||
map.forEach((key, value) -> {
|
||||
if (key != null) {
|
||||
String keyText = String.valueOf(key);
|
||||
result.put(keyText, isSensitiveKey(keyText) ? SECRET_PLACEHOLDER : normalizeValue(value));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private void applyAgentFields(AgentEntity agent, AgentSnapshotDataDTO data) {
|
||||
for (AgentSnapshotField field : AgentSnapshotField.values()) {
|
||||
field.applyTo(agent, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRestoreParams(AgentEntity agent) {
|
||||
if (agent == null || StringUtils.isBlank(agent.getLlmModelId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModelConfigEntity llmModelData = modelConfigService.selectById(agent.getLlmModelId());
|
||||
if (llmModelData == null || llmModelData.getConfigJson() == null) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
Object typeValue = llmModelData.getConfigJson().get("type");
|
||||
String type = typeValue == null ? "" : typeValue.toString();
|
||||
if ("openai".equals(type) || "ollama".equals(type)) {
|
||||
return;
|
||||
}
|
||||
if ("Intent_function_call".equals(agent.getIntentModelId())) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMemoryPolicy(AgentEntity agent) {
|
||||
if (agent == null || StringUtils.isBlank(agent.getMemModelId())) {
|
||||
return;
|
||||
}
|
||||
if (Constant.MEMORY_NO_MEM.equals(agent.getMemModelId())) {
|
||||
agentChatHistoryService.deleteByAgentId(agent.getId(), true, true);
|
||||
agent.setSummaryMemory("");
|
||||
} else if (Constant.MEMORY_MEM_REPORT_ONLY.equals(agent.getMemModelId())) {
|
||||
agent.setSummaryMemory("");
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreFunctions(String agentId, List<AgentUpdateDTO.FunctionInfo> functions) {
|
||||
agentPluginMappingService.deleteByAgentId(agentId);
|
||||
if (functions == null || functions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<AgentPluginMapping> mappings = functions.stream().map(info -> {
|
||||
AgentPluginMapping mapping = new AgentPluginMapping();
|
||||
mapping.setAgentId(agentId);
|
||||
mapping.setPluginId(info.getPluginId());
|
||||
mapping.setParamInfo(JsonUtils.toJsonString(info.getParamInfo()));
|
||||
return mapping;
|
||||
}).toList();
|
||||
agentPluginMappingService.saveBatch(mappings);
|
||||
}
|
||||
|
||||
private void restoreContextProviders(String agentId,
|
||||
List<xiaozhi.modules.agent.dto.ContextProviderDTO> contextProviders) {
|
||||
AgentContextProviderEntity entity = new AgentContextProviderEntity();
|
||||
entity.setAgentId(agentId);
|
||||
entity.setContextProviders(nullToEmpty(contextProviders));
|
||||
entity.setUpdater(SecurityUser.getUserId());
|
||||
entity.setUpdatedAt(new Date());
|
||||
agentContextProviderService.saveOrUpdateByAgentId(entity);
|
||||
}
|
||||
|
||||
private void restoreTags(String agentId, AgentSnapshotDataDTO data) {
|
||||
List<AgentSnapshotTagDTO> tags = data.getTags();
|
||||
if (tags == null || tags.isEmpty()) {
|
||||
agentTagService.saveAgentTags(agentId, null, nullToEmpty(data.getTagNames()));
|
||||
return;
|
||||
}
|
||||
|
||||
agentTagRelationDao.deleteByAgentId(agentId);
|
||||
List<AgentTagRelationEntity> relations = new ArrayList<>();
|
||||
Date now = new Date();
|
||||
for (int i = 0; i < tags.size(); i++) {
|
||||
AgentSnapshotTagDTO snapshotTag = tags.get(i);
|
||||
String tagId = restoreTag(snapshotTag, now);
|
||||
if (StringUtils.isBlank(tagId)) {
|
||||
continue;
|
||||
}
|
||||
AgentTagRelationEntity relation = new AgentTagRelationEntity();
|
||||
relation.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
relation.setAgentId(agentId);
|
||||
relation.setTagId(tagId);
|
||||
relation.setSort(i);
|
||||
relation.setCreator(SecurityUser.getUserId());
|
||||
relation.setCreatedAt(now);
|
||||
relation.setUpdater(SecurityUser.getUserId());
|
||||
relation.setUpdatedAt(now);
|
||||
relations.add(relation);
|
||||
}
|
||||
if (!relations.isEmpty()) {
|
||||
agentTagRelationDao.batchInsertRelation(relations);
|
||||
}
|
||||
}
|
||||
|
||||
private String restoreTag(AgentSnapshotTagDTO snapshotTag, Date now) {
|
||||
if (snapshotTag == null || StringUtils.isBlank(snapshotTag.getTagName())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AgentTagEntity tag = null;
|
||||
if (StringUtils.isNotBlank(snapshotTag.getId())) {
|
||||
tag = agentTagDao.selectById(snapshotTag.getId());
|
||||
if (tag != null && Objects.equals(tag.getDeleted(), 1)) {
|
||||
tag = null;
|
||||
}
|
||||
}
|
||||
if (tag == null) {
|
||||
tag = agentTagDao.selectOne(new QueryWrapper<AgentTagEntity>()
|
||||
.eq("tag_name", snapshotTag.getTagName())
|
||||
.eq("deleted", 0)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
if (tag != null) {
|
||||
return tag.getId();
|
||||
}
|
||||
|
||||
AgentTagEntity newTag = new AgentTagEntity();
|
||||
newTag.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
newTag.setTagName(snapshotTag.getTagName());
|
||||
newTag.setSort(snapshotTag.getSort() == null ? 0 : snapshotTag.getSort());
|
||||
newTag.setDeleted(0);
|
||||
newTag.setCreator(SecurityUser.getUserId());
|
||||
newTag.setCreatedAt(now);
|
||||
newTag.setUpdater(SecurityUser.getUserId());
|
||||
newTag.setUpdatedAt(now);
|
||||
agentTagDao.insert(newTag);
|
||||
return newTag.getId();
|
||||
}
|
||||
|
||||
private AgentSnapshotVO toVO(AgentSnapshotEntity entity, boolean includeData) {
|
||||
AgentSnapshotVO vo = new AgentSnapshotVO();
|
||||
vo.setId(entity.getId());
|
||||
vo.setAgentId(entity.getAgentId());
|
||||
vo.setUserId(entity.getUserId());
|
||||
vo.setVersionNo(entity.getVersionNo());
|
||||
vo.setChangedFields(parseChangedFields(entity.getChangedFields()));
|
||||
vo.setFieldOrder(AgentSnapshotField.names());
|
||||
vo.setSource(entity.getSource());
|
||||
vo.setRestoreFromSnapshotId(entity.getRestoreFromSnapshotId());
|
||||
vo.setRestoreFromVersionNo(entity.getRestoreFromVersionNo());
|
||||
vo.setCreator(entity.getCreator());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
if (includeData) {
|
||||
vo.setSnapshotData(redactSnapshotData(JsonUtils.parseObject(entity.getSnapshotData(),
|
||||
AgentSnapshotDataDTO.class)));
|
||||
vo.setAfterSnapshotData(redactSnapshotData(getAfterSnapshotData(entity)));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO getAfterSnapshotData(AgentSnapshotEntity entity) {
|
||||
AgentSnapshotEntity nextSnapshot = agentSnapshotDao.selectNextSnapshot(entity.getAgentId(), entity.getVersionNo());
|
||||
if (nextSnapshot != null) {
|
||||
return JsonUtils.parseObject(nextSnapshot.getSnapshotData(), AgentSnapshotDataDTO.class);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> parseChangedFields(String changedFields) {
|
||||
if (StringUtils.isBlank(changedFields)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return JsonUtils.parseObject(changedFields, STRING_LIST_TYPE);
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO redactSnapshotData(AgentSnapshotDataDTO data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
AgentSnapshotDataDTO copy = JsonUtils.parseObject(JsonUtils.toJsonString(data), AgentSnapshotDataDTO.class);
|
||||
if (copy.getFunctions() != null) {
|
||||
copy.getFunctions().forEach(function -> {
|
||||
if (function != null) {
|
||||
function.setParamInfo(redactSensitiveMap(function.getParamInfo()));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (copy.getContextProviders() != null) {
|
||||
copy.getContextProviders().forEach(provider -> {
|
||||
if (provider != null) {
|
||||
provider.setHeaders(redactSensitiveMap(provider.getHeaders()));
|
||||
}
|
||||
});
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO preserveCurrentSensitiveValues(AgentSnapshotDataDTO target, AgentSnapshotDataDTO current) {
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
AgentSnapshotDataDTO copy = JsonUtils.parseObject(JsonUtils.toJsonString(target), AgentSnapshotDataDTO.class);
|
||||
Map<String, AgentUpdateDTO.FunctionInfo> currentFunctions = nullToEmpty(current == null ? null : current.getFunctions())
|
||||
.stream()
|
||||
.filter(function -> function != null && StringUtils.isNotBlank(function.getPluginId()))
|
||||
.collect(Collectors.toMap(AgentUpdateDTO.FunctionInfo::getPluginId, Function.identity(),
|
||||
(left, right) -> left));
|
||||
if (copy.getFunctions() != null) {
|
||||
copy.getFunctions().forEach(function -> {
|
||||
if (function == null) {
|
||||
return;
|
||||
}
|
||||
AgentUpdateDTO.FunctionInfo currentFunction = currentFunctions.get(function.getPluginId());
|
||||
function.setParamInfo(preserveCurrentSensitiveMap(function.getParamInfo(),
|
||||
currentFunction == null ? null : currentFunction.getParamInfo()));
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, xiaozhi.modules.agent.dto.ContextProviderDTO> currentProviders = nullToEmpty(
|
||||
current == null ? null : current.getContextProviders())
|
||||
.stream()
|
||||
.filter(provider -> provider != null && StringUtils.isNotBlank(provider.getUrl()))
|
||||
.collect(Collectors.toMap(xiaozhi.modules.agent.dto.ContextProviderDTO::getUrl, Function.identity(),
|
||||
(left, right) -> left));
|
||||
if (copy.getContextProviders() != null) {
|
||||
copy.getContextProviders().forEach(provider -> {
|
||||
if (provider == null) {
|
||||
return;
|
||||
}
|
||||
xiaozhi.modules.agent.dto.ContextProviderDTO currentProvider = currentProviders.get(provider.getUrl());
|
||||
provider.setHeaders(preserveCurrentSensitiveMap(provider.getHeaders(),
|
||||
currentProvider == null ? null : currentProvider.getHeaders()));
|
||||
});
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private HashMap<String, Object> redactSensitiveMap(Map<?, ?> map) {
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
if (map == null) {
|
||||
return result;
|
||||
}
|
||||
map.forEach((key, value) -> {
|
||||
if (key != null) {
|
||||
String keyText = String.valueOf(key);
|
||||
result.put(keyText, isSensitiveKey(keyText) ? SECRET_PLACEHOLDER : redactSensitiveValue(value));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object redactSensitiveValue(Object value) {
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
return redactSensitiveMap(map);
|
||||
}
|
||||
if (value instanceof List<?> list) {
|
||||
return list.stream().map(this::redactSensitiveValue).toList();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private HashMap<String, Object> preserveCurrentSensitiveMap(Map<?, ?> target, Map<?, ?> current) {
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
if (target == null) {
|
||||
return result;
|
||||
}
|
||||
target.forEach((key, value) -> {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
String keyText = String.valueOf(key);
|
||||
Object currentValue = getMapValue(current, keyText);
|
||||
if (isSensitiveKey(keyText)) {
|
||||
result.put(keyText, currentValue == null || SECRET_PLACEHOLDER.equals(currentValue) ? "" : currentValue);
|
||||
} else {
|
||||
result.put(keyText, preserveCurrentSensitiveValue(value, currentValue));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object preserveCurrentSensitiveValue(Object target, Object current) {
|
||||
if (target instanceof Map<?, ?> targetMap) {
|
||||
return preserveCurrentSensitiveMap(targetMap, current instanceof Map<?, ?> currentMap ? currentMap : null);
|
||||
}
|
||||
if (target instanceof List<?> targetList) {
|
||||
List<?> currentList = current instanceof List<?> list ? list : Collections.emptyList();
|
||||
List<Object> result = new ArrayList<>();
|
||||
for (int i = 0; i < targetList.size(); i++) {
|
||||
Object currentItem = i < currentList.size() ? currentList.get(i) : null;
|
||||
result.add(preserveCurrentSensitiveValue(targetList.get(i), currentItem));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private Object getMapValue(Map<?, ?> map, String key) {
|
||||
if (map == null) {
|
||||
return null;
|
||||
}
|
||||
if (map.containsKey(key)) {
|
||||
return map.get(key);
|
||||
}
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (entry.getKey() != null && Objects.equals(key, String.valueOf(entry.getKey()))) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isSensitiveKey(String key) {
|
||||
String normalized = StringUtils.defaultString(key).toLowerCase().replaceAll("[^a-z0-9]", "");
|
||||
return normalized.equals("authorization")
|
||||
|| normalized.equals("token")
|
||||
|| normalized.endsWith("token")
|
||||
|| normalized.contains("apikey")
|
||||
|| normalized.contains("appkey")
|
||||
|| normalized.contains("accesskey")
|
||||
|| normalized.contains("privatekey")
|
||||
|| normalized.contains("password")
|
||||
|| normalized.contains("passwd")
|
||||
|| normalized.contains("secret")
|
||||
|| normalized.contains("credential");
|
||||
}
|
||||
|
||||
private <T> List<T> nullToEmpty(List<T> list) {
|
||||
return list == null ? Collections.emptyList() : list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package xiaozhi.modules.agent.typehandler;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
|
||||
public class ContextProviderListTypeHandler extends AbstractJsonTypeHandler<List<ContextProviderDTO>> {
|
||||
private static final TypeReference<List<ContextProviderDTO>> CONTEXT_PROVIDER_LIST_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
@Override
|
||||
protected List<ContextProviderDTO> parse(String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ContextProviderDTO> providers = JsonUtils.parseObject(json, CONTEXT_PROVIDER_LIST_TYPE);
|
||||
return providers == null ? Collections.emptyList() : providers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toJson(List<ContextProviderDTO> obj) {
|
||||
return JsonUtils.toJsonString(obj == null ? Collections.emptyList() : obj);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package xiaozhi.modules.agent.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -20,7 +18,6 @@ import java.util.List;
|
||||
public class AgentInfoVO extends AgentEntity
|
||||
{
|
||||
@Schema(description = "插件列表Id")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<AgentPluginMapping> functions;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
@@ -28,4 +25,7 @@ public class AgentInfoVO extends AgentEntity
|
||||
|
||||
@Schema(description = "替换词文件ID列表")
|
||||
private List<String> correctWordFileIds;
|
||||
|
||||
@Schema(description = "当前配置版本号")
|
||||
private Integer currentVersionNo;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package xiaozhi.modules.agent.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体配置快照")
|
||||
public class AgentSnapshotVO {
|
||||
private String id;
|
||||
private String agentId;
|
||||
@Schema(description = "所属用户ID,表示该快照归属的智能体所有者")
|
||||
private Long userId;
|
||||
private Integer versionNo;
|
||||
private List<String> changedFields;
|
||||
private List<String> fieldOrder;
|
||||
private String source;
|
||||
@Schema(description = "恢复来源快照ID,仅恢复前自动备份版本有值")
|
||||
private String restoreFromSnapshotId;
|
||||
@Schema(description = "恢复来源版本号,仅恢复前自动备份版本有值")
|
||||
private Integer restoreFromVersionNo;
|
||||
@Schema(description = "创建者,表示触发本次快照写入的操作人")
|
||||
private Long creator;
|
||||
private Date createdAt;
|
||||
private AgentSnapshotDataDTO snapshotData;
|
||||
private AgentSnapshotDataDTO afterSnapshotData;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -174,20 +175,26 @@ public class DeviceController {
|
||||
return new Result<Object>().ok(deviceAddressBookService.getAddressBookList(macAddress));
|
||||
}
|
||||
|
||||
@GetMapping("/address-book/lookup")
|
||||
@Operation(summary = "根据昵称查找目标设备")
|
||||
public Result<Map<String, String>> lookupByNickname(String callerMac, String nickname) {
|
||||
Map<String, String> result = deviceAddressBookService.lookupByNickname(callerMac, nickname);
|
||||
@GetMapping("/address-book/call")
|
||||
@Operation(summary = "根据昵称发起呼叫")
|
||||
public Result<Map<String, Object>> callByNickname(String callerMac, String nickname,
|
||||
@RequestParam(required = false, defaultValue = "false") boolean answer) {
|
||||
Map<String, Object> result = deviceAddressBookService.callByNickname(callerMac, nickname, answer);
|
||||
if (result == null) {
|
||||
return new Result<Map<String, String>>().error("未找到对应设备");
|
||||
return new Result<Map<String, Object>>().error("未找到对应设备");
|
||||
}
|
||||
return new Result<Map<String, String>>().ok(result);
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
}
|
||||
|
||||
@PutMapping("/address-book/alias")
|
||||
@Operation(summary = "更新设备通讯录别名")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> updateAlias(@Valid @RequestBody DeviceAddressBookAliasDTO dto) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress());
|
||||
if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) {
|
||||
return new Result<Void>().error("无权限操作该设备");
|
||||
}
|
||||
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null);
|
||||
return new Result<Void>();
|
||||
}
|
||||
@@ -196,14 +203,12 @@ public class DeviceController {
|
||||
@Operation(summary = "更新设备通讯录权限")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> updatePermission(@Valid @RequestBody DeviceAddressBookPermissionDTO dto) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress());
|
||||
if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) {
|
||||
return new Result<Void>().error("无权限操作该设备");
|
||||
}
|
||||
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission());
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@GetMapping("/call/forward")
|
||||
@Operation(summary = "转发呼叫请求到网关")
|
||||
public Result<Map<String, Object>> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
|
||||
Map<String, Object> result = deviceService.forwardCallRequest(callerMac, targetMac, callerNickname);
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,9 @@ public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity
|
||||
* 更新权限
|
||||
*/
|
||||
void updatePermission(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("hasPermission") Boolean hasPermission);
|
||||
|
||||
/**
|
||||
* 批量删除设备相关的通讯录记录
|
||||
*/
|
||||
void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package xiaozhi.modules.device.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 设备绑定的DTO
|
||||
*
|
||||
* @author zjy
|
||||
* @since 2025-3-28
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "设备连接头信息")
|
||||
public class DeviceBindDTO {
|
||||
|
||||
@Schema(description = "mac地址")
|
||||
private String macAddress;
|
||||
|
||||
@Schema(description = "所属用户id")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "智能体id")
|
||||
private String agentId;
|
||||
|
||||
}
|
||||
@@ -38,10 +38,15 @@ public interface DeviceAddressBookService {
|
||||
void refreshCache();
|
||||
|
||||
/**
|
||||
* 根据昵称查找目标设备信息
|
||||
* 根据昵称发起呼叫
|
||||
* @param callerMac 主叫方MAC地址
|
||||
* @param nickname 被叫方昵称
|
||||
* @return {targetMac: 目标MAC, callerNickname: 目标如何称呼主叫方}
|
||||
* @param isAnswer 是否为接听模式(跳过权限检查)
|
||||
*/
|
||||
Map<String, String> lookupByNickname(String callerMac, String nickname);
|
||||
Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer);
|
||||
|
||||
/**
|
||||
* 批量删除设备相关的通讯录记录
|
||||
*/
|
||||
void deleteByMacAddresses(List<String> macAddresses);
|
||||
}
|
||||
@@ -138,10 +138,4 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
*/
|
||||
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
|
||||
|
||||
/**
|
||||
* 转发呼叫请求到网关
|
||||
* @return 网关响应 {status, message}
|
||||
*/
|
||||
Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
|
||||
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
|
||||
import xiaozhi.modules.device.service.DeviceAddressBookService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Service
|
||||
public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
@@ -22,12 +29,14 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
private final DeviceAddressBookDao deviceAddressBookDao;
|
||||
private final RedisUtils redisUtils;
|
||||
private final DeviceService deviceService;
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils,
|
||||
DeviceService deviceService) {
|
||||
@Lazy DeviceService deviceService, SysParamsService sysParamsService) {
|
||||
this.deviceAddressBookDao = deviceAddressBookDao;
|
||||
this.redisUtils = redisUtils;
|
||||
this.deviceService = deviceService;
|
||||
this.sysParamsService = sysParamsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,33 +56,46 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> lookupByNickname(String callerMac, String nickname) {
|
||||
public Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer) {
|
||||
Map<String, Map<String, String>> allBooks = getAllAddressBooks();
|
||||
|
||||
if (isAnswer) {
|
||||
return postToMqtt("/api/call/accept", Map.of("mac", callerMac), "接听");
|
||||
}
|
||||
|
||||
// 主动呼叫模式
|
||||
Map<String, String> callerBook = allBooks.get(callerMac.toLowerCase());
|
||||
if (callerBook == null) {
|
||||
return null;
|
||||
return errorResult("未找到备注为'" + nickname + "'的设备");
|
||||
}
|
||||
// 从缓存获取 targetMac,格式: "mac|permission"
|
||||
String targetMacWithPerm = callerBook.get(nickname);
|
||||
if (targetMacWithPerm == null) {
|
||||
return null;
|
||||
return errorResult("未找到备注为'" + nickname + "'的设备");
|
||||
}
|
||||
// 解析 targetMac 和 permission
|
||||
String[] parts = targetMacWithPerm.split("\\|");
|
||||
String targetMac = parts[0];
|
||||
boolean hasPermission = parts.length > 1 && "1".equals(parts[1]);
|
||||
|
||||
Map<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
|
||||
if (targetBook == null) {
|
||||
return null;
|
||||
if (!hasPermission) {
|
||||
return errorResult("呼叫失败,您没有权限呼叫该设备");
|
||||
}
|
||||
// 检查双向关系:目标设备是否也添加了主叫方为联系人
|
||||
String callerNickname = targetBook.get(callerMac.toLowerCase());
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("targetMac", targetMac);
|
||||
result.put("callerNickname", callerNickname);
|
||||
result.put("hasPermission", hasPermission ? "true" : "false");
|
||||
return result;
|
||||
|
||||
// 获取目标设备如何称呼主叫方
|
||||
Map<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
|
||||
String callerNickname = null;
|
||||
if (targetBook != null) {
|
||||
callerNickname = targetBook.get(callerMac.toLowerCase());
|
||||
}
|
||||
if (StringUtils.isBlank(callerNickname)) {
|
||||
callerNickname = deviceService.getDeviceByMacAddress(callerMac).getAlias();
|
||||
if (StringUtils.isBlank(callerNickname)) {
|
||||
callerNickname = formatMacAsDeviceName(callerMac);
|
||||
}
|
||||
}
|
||||
|
||||
return postToMqtt("/api/call/request",
|
||||
Map.of("caller_mac", callerMac, "target_mac", targetMac, "caller_nickname", callerNickname),
|
||||
"呼叫");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,43 +103,44 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
Map<String, Map<String, String>> result = new HashMap<>();
|
||||
List<DeviceAddressBookEntity> allRecords = deviceAddressBookDao.selectList(null);
|
||||
Map<String, String> reverseMap = new HashMap<>();
|
||||
Map<String, Boolean> permissionMap = new HashMap<>();
|
||||
|
||||
for (DeviceAddressBookEntity entity : allRecords) {
|
||||
String macA = entity.getMacAddress().toLowerCase();
|
||||
String macB = entity.getTargetMac().toLowerCase();
|
||||
String alias = entity.getAlias();
|
||||
Boolean hasPermission = entity.getHasPermission();
|
||||
|
||||
// 构建正向映射: A对B的映射 nickname -> macB|permission
|
||||
if (alias != null && !alias.isEmpty()) {
|
||||
result.computeIfAbsent(macA, k -> new HashMap<>());
|
||||
String permStr = (hasPermission != null && hasPermission) ? "1" : "0";
|
||||
result.get(macA).put(alias, macB + "|" + permStr);
|
||||
}
|
||||
|
||||
// 构建反向记录用于查找B对A的称呼
|
||||
if (alias != null && !alias.isEmpty()) {
|
||||
// 反向记录: (macB, macA) = B对A的称呼
|
||||
reverseMap.put(macB + ":" + macA, alias);
|
||||
}
|
||||
// 记录 A 对 B 是否有权限呼叫
|
||||
permissionMap.put(macA + ":" + macB, hasPermission != null && hasPermission);
|
||||
}
|
||||
|
||||
// 构建反向映射: B对A的映射 macA -> nickname
|
||||
for (DeviceAddressBookEntity entity : allRecords) {
|
||||
String macA = entity.getMacAddress().toLowerCase();
|
||||
String macB = entity.getTargetMac().toLowerCase();
|
||||
String aliasAtoB = entity.getAlias();
|
||||
result.computeIfAbsent(macA, k -> new HashMap<>());
|
||||
result.computeIfAbsent(macB, k -> new HashMap<>());
|
||||
if (aliasAtoB != null && !aliasAtoB.isEmpty()) {
|
||||
// A对B的映射: nickname -> macB|permission
|
||||
Boolean perm = entity.getHasPermission();
|
||||
String permStr = (perm != null && perm) ? "1" : "0";
|
||||
result.get(macA).put(aliasAtoB, macB + "|" + permStr);
|
||||
}
|
||||
// B对A的映射: macA -> nickname(查反向记录)
|
||||
String aliasBtoA = reverseMap.get(macA + ":" + macB);
|
||||
if (aliasBtoA != null && !aliasBtoA.isEmpty()) {
|
||||
result.computeIfAbsent(macB, k -> new HashMap<>());
|
||||
result.get(macB).put(macA, aliasBtoA);
|
||||
}
|
||||
}
|
||||
|
||||
redisUtils.set(RedisKeys.getAddressBookKey(), result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAlias(String macAddress, String targetMac, String alias) {
|
||||
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
|
||||
String finalAlias = generateUniqueAlias(macAddress, targetMac, alias);
|
||||
deviceAddressBookDao.updateAlias(macAddress, targetMac, finalAlias);
|
||||
refreshCache();
|
||||
}
|
||||
|
||||
@@ -132,20 +155,23 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("mac_address", macAddress).eq("target_mac", targetMac);
|
||||
DeviceAddressBookEntity record = deviceAddressBookDao.selectOne(wrapper);
|
||||
System.out.println("saveOrUpdate - mac=" + macAddress + ", targetMac=" + targetMac + ", alias=" + alias + ", record=" + (record == null ? "null" : "not null"));
|
||||
if (record == null) {
|
||||
DeviceAddressBookEntity entity = new DeviceAddressBookEntity();
|
||||
entity.setMacAddress(macAddress);
|
||||
entity.setTargetMac(targetMac);
|
||||
// 如果 alias 为空,就用默认使用设备名称
|
||||
// 如果 alias 为空,就默认使用设备名称
|
||||
if (StringUtils.isBlank(alias)) {
|
||||
alias = deviceService.getDeviceByMacAddress(targetMac).getAlias();
|
||||
}
|
||||
// 检查重名
|
||||
alias = generateUniqueAlias(macAddress, targetMac, alias);
|
||||
entity.setAlias(alias);
|
||||
entity.setHasPermission(hasPermission);
|
||||
deviceAddressBookDao.insert(entity);
|
||||
} else {
|
||||
if (alias != null) {
|
||||
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
|
||||
updateAlias(macAddress, targetMac, alias);
|
||||
}
|
||||
if (hasPermission != null) {
|
||||
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
|
||||
@@ -153,4 +179,95 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
}
|
||||
refreshCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByMacAddresses(List<String> macAddresses) {
|
||||
if (macAddresses == null || macAddresses.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
deviceAddressBookDao.deleteByMacAddresses(macAddresses);
|
||||
refreshCache();
|
||||
}
|
||||
|
||||
private Map<String, Object> errorResult(String message) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "error");
|
||||
result.put("message", message);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> postToMqtt(String path, Map<String, Object> body, String action) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "error");
|
||||
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)
|
||||
|| StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
|
||||
result.put("message", action + "失败,网关配置缺失");
|
||||
return result;
|
||||
}
|
||||
|
||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
String token = hexString.toString();
|
||||
|
||||
String url = "http://" + mqttGatewayUrl + path;
|
||||
String response = HttpRequest.post(url)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.timeout(5000)
|
||||
.execute()
|
||||
.body();
|
||||
|
||||
if (StringUtils.isNotBlank(response)) {
|
||||
Map<String, Object> gwResult = JSONUtil.parseObj(response);
|
||||
result.put("status", gwResult.get("status"));
|
||||
result.put("message", gwResult.get("message"));
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
result.put("message", action + "失败,请稍后再试");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private String formatMacAsDeviceName(String mac) {
|
||||
if (StringUtils.isBlank(mac) || mac.length() < 2) {
|
||||
return mac;
|
||||
}
|
||||
String lastTwo = mac.substring(mac.length() - 2);
|
||||
return "尾号为" + lastTwo + "的设备";
|
||||
}
|
||||
|
||||
private String generateUniqueAlias(String macAddress, String targetMac, String alias) {
|
||||
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("mac_address", macAddress);
|
||||
List<DeviceAddressBookEntity> existing = deviceAddressBookDao.selectList(wrapper);
|
||||
List<String> existNames = existing.stream()
|
||||
.map(DeviceAddressBookEntity::getAlias)
|
||||
.filter(a -> a != null && !a.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
if (!existNames.contains(alias)) {
|
||||
return alias;
|
||||
}
|
||||
int suffix = 1;
|
||||
String newAlias;
|
||||
while (existNames.contains(newAlias = alias + suffix)) {
|
||||
suffix++;
|
||||
}
|
||||
return newAlias;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -63,6 +64,7 @@ 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.DeviceAddressBookService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.device.service.OtaService;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
@@ -80,6 +82,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
private final SysParamsService sysParamsService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final OtaService otaService;
|
||||
private final DeviceAddressBookService deviceAddressBookService;
|
||||
|
||||
@Async
|
||||
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
|
||||
@@ -314,11 +317,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
@Override
|
||||
public void unbindDevice(Long userId, String deviceId) {
|
||||
// 先查询设备信息,获取agentId
|
||||
// 先查询设备信息,获取agentId和macAddress
|
||||
DeviceEntity device = baseDao.selectById(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
String macAddress = device.getMacAddress();
|
||||
if (StringUtils.isNotBlank(device.getAgentId())) {
|
||||
// 清除智能体设备数量缓存
|
||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
|
||||
@@ -328,6 +332,9 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
wrapper.eq("user_id", userId);
|
||||
wrapper.eq("id", deviceId);
|
||||
baseDao.delete(wrapper);
|
||||
|
||||
// 删除设备相关的通讯录权限记录
|
||||
deviceAddressBookService.deleteByMacAddresses(Collections.singletonList(macAddress));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -346,9 +353,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
@Override
|
||||
public void deleteByAgentId(String agentId) {
|
||||
// 先查询该智能体下的所有设备,获取mac地址用于删除通讯录记录
|
||||
QueryWrapper<DeviceEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("agent_id", agentId);
|
||||
List<DeviceEntity> devices = baseDao.selectList(queryWrapper);
|
||||
|
||||
// 删除设备
|
||||
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("agent_id", agentId);
|
||||
baseDao.delete(wrapper);
|
||||
|
||||
// 批量删除这些设备相关的所有通讯录权限记录
|
||||
if (!devices.isEmpty()) {
|
||||
List<String> macAddresses = devices.stream()
|
||||
.map(DeviceEntity::getMacAddress)
|
||||
.collect(Collectors.toList());
|
||||
deviceAddressBookService.deleteByMacAddresses(macAddresses);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -891,70 +912,4 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "error");
|
||||
result.put("message", "网关配置缺失");
|
||||
|
||||
// 从系统参数获取网关配置
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
log.error("MQTT网关地址未配置");
|
||||
result.put("message", "MQTT网关地址未配置");
|
||||
return result;
|
||||
}
|
||||
if (StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
|
||||
log.error("MQTT签名密钥未配置");
|
||||
result.put("message", "MQTT签名密钥未配置");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 生成token: SHA256(date + secret)
|
||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
String token = hexString.toString();
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("caller_mac", callerMac);
|
||||
body.put("target_mac", targetMac);
|
||||
body.put("caller_nickname", callerNickname);
|
||||
|
||||
// 发送请求并获取响应
|
||||
String url = "http://" + mqttGatewayUrl + "/api/call/request";
|
||||
String response = cn.hutool.http.HttpRequest.post(url)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.timeout(5000)
|
||||
.execute()
|
||||
.body();
|
||||
|
||||
// 解析网关响应
|
||||
if (StringUtils.isNotBlank(response)) {
|
||||
Map<String, Object> gwResult = JSONUtil.parseObj(response);
|
||||
result.put("status", gwResult.get("status"));
|
||||
result.put("message", gwResult.get("message"));
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("转发呼叫请求失败: {}", e.getMessage());
|
||||
result.put("message", "呼叫请求转发失败: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文档 DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "知识库文档")
|
||||
public class DocumentDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "本地ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "RAGFlow文档ID")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "文件大小")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "文件类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "分块方法")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析配置")
|
||||
private Map<String, Object> parserConfig;
|
||||
|
||||
@Schema(description = "处理状态 (1:解析中 3:成功 4:失败)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String error;
|
||||
|
||||
@Schema(description = "分块数量")
|
||||
private Integer chunkCount;
|
||||
|
||||
@Schema(description = "Token数量")
|
||||
private Long tokenCount;
|
||||
|
||||
@Schema(description = "是否启用")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
|
||||
@Schema(description = "上传进度 (虚拟字段)")
|
||||
private Double progress;
|
||||
|
||||
@Schema(description = "缩略图/预览图 (虚拟字段)")
|
||||
private String thumbnail;
|
||||
}
|
||||
@@ -67,4 +67,7 @@ public class KnowledgeBaseDTO implements Serializable {
|
||||
|
||||
@Schema(description = "文档数量")
|
||||
private Integer documentCount;
|
||||
|
||||
@Schema(description = "异常提示")
|
||||
private String errorMessage;
|
||||
}
|
||||
@@ -1,421 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.agent;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "智能体 (Agent) 管理聚合 DTO")
|
||||
public class AgentDTO {
|
||||
|
||||
// ========== 1. Agent 管理 (CRUD) - 对应 RAGFlow_Agent接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 创建请求")
|
||||
public static class CreateReq implements Serializable {
|
||||
@Schema(description = "Agent 标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "My Agent")
|
||||
@NotBlank(message = "Agent 标题不能为空")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "DSL 定义 (画布 JSON)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "DSL 定义不能为空")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "描述", example = "这是一个测试 Agent")
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "头像 URL", example = "http://example.com/avatar.png")
|
||||
@JsonProperty("avatar")
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 更新请求")
|
||||
public static class UpdateReq implements Serializable {
|
||||
@Schema(description = "Agent 标题", example = "Updated Agent")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "DSL 定义 (画布 JSON)")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "描述")
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "头像 URL")
|
||||
@JsonProperty("avatar")
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 列表请求")
|
||||
public static class ListReq implements Serializable {
|
||||
@Schema(description = "页码", defaultValue = "1")
|
||||
@JsonProperty("page")
|
||||
@Builder.Default
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(description = "每页大小", defaultValue = "10")
|
||||
@JsonProperty("page_size")
|
||||
@Builder.Default
|
||||
private Integer pageSize = 10;
|
||||
|
||||
@Schema(description = "排序字段", defaultValue = "update_time")
|
||||
@JsonProperty("orderby")
|
||||
@Builder.Default
|
||||
private String orderby = "update_time";
|
||||
|
||||
@Schema(description = "是否降序", defaultValue = "true")
|
||||
@JsonProperty("desc")
|
||||
@Builder.Default
|
||||
private Boolean desc = true;
|
||||
|
||||
@Schema(description = "Agent ID 过滤")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "标题模糊搜索")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 响应对象")
|
||||
public static class AgentVO implements Serializable {
|
||||
@Schema(description = "Agent ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "标题")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "描述")
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "头像")
|
||||
@JsonProperty("avatar")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "DSL 定义")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "创建者 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "画布分类")
|
||||
@JsonProperty("canvas_category")
|
||||
private String canvasCategory;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
}
|
||||
|
||||
// ========== 2. Webhook 调试与追踪 - 对应 RAGFlow_Agent接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Webhook 触发请求 (参数动态)")
|
||||
public static class WebhookTriggerReq implements Serializable {
|
||||
@Schema(description = "输入变量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "输入变量不能为空")
|
||||
@JsonProperty("inputs")
|
||||
private Map<String, Object> inputs;
|
||||
|
||||
@Schema(description = "查询词", example = "Hello")
|
||||
@JsonProperty("query")
|
||||
private String query;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Webhook 追踪请求")
|
||||
public static class WebhookTraceReq implements Serializable {
|
||||
@Schema(description = "时间戳游标", example = "1700000000.0")
|
||||
@JsonProperty("since_ts")
|
||||
private Double sinceTs;
|
||||
|
||||
@Schema(description = "Webhook ID")
|
||||
@JsonProperty("webhook_id")
|
||||
private String webhookId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Webhook 追踪响应")
|
||||
public static class WebhookTraceVO implements Serializable {
|
||||
@Schema(description = "Webhook ID")
|
||||
@JsonProperty("webhook_id")
|
||||
private String webhookId;
|
||||
|
||||
@Schema(description = "是否结束")
|
||||
@JsonProperty("finished")
|
||||
private Boolean finished;
|
||||
|
||||
@Schema(description = "下一次查询的时间戳游标")
|
||||
@JsonProperty("next_since_ts")
|
||||
private Double nextSinceTs;
|
||||
|
||||
@Schema(description = "事件列表")
|
||||
@JsonProperty("events")
|
||||
private List<TraceEvent> events;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "追踪事件项")
|
||||
public static class TraceEvent implements Serializable {
|
||||
@Schema(description = "时间戳")
|
||||
@JsonProperty("ts")
|
||||
private Double ts;
|
||||
|
||||
@Schema(description = "事件类型")
|
||||
@JsonProperty("event")
|
||||
private String event;
|
||||
|
||||
@Schema(description = "事件数据")
|
||||
@JsonProperty("data")
|
||||
private Object data;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 3. Agent 会话 (Session) - 对应 RAGFlow_Agent_Dify接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 创建请求")
|
||||
public static class SessionCreateReq implements Serializable {
|
||||
@Schema(description = "用户 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 列表请求")
|
||||
public static class SessionListReq implements Serializable {
|
||||
@Schema(description = "页码", defaultValue = "1")
|
||||
@JsonProperty("page")
|
||||
@Builder.Default
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(description = "每页大小", defaultValue = "10")
|
||||
@JsonProperty("page_size")
|
||||
@Builder.Default
|
||||
private Integer pageSize = 10;
|
||||
|
||||
@Schema(description = "排序字段", defaultValue = "create_time")
|
||||
@JsonProperty("orderby")
|
||||
@Builder.Default
|
||||
private String orderby = "create_time";
|
||||
|
||||
@Schema(description = "是否降序", defaultValue = "true")
|
||||
@JsonProperty("desc")
|
||||
@Builder.Default
|
||||
private Boolean desc = true;
|
||||
|
||||
@Schema(description = "Session ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "用户 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "是否返回 DSL")
|
||||
@JsonProperty("dsl")
|
||||
@Builder.Default
|
||||
private Boolean dsl = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 批量删除请求")
|
||||
public static class SessionBatchDeleteReq implements Serializable {
|
||||
@Schema(description = "会话 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("ids")
|
||||
@NotEmpty(message = "ID列表不能为空")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 响应对象")
|
||||
public static class SessionVO implements Serializable {
|
||||
@Schema(description = "Session ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "Agent ID")
|
||||
@JsonProperty("agent_id")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "用户 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "来源")
|
||||
@JsonProperty("source")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "DSL 定义")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "消息列表")
|
||||
@JsonProperty("messages")
|
||||
private List<Map<String, Object>> messages;
|
||||
}
|
||||
|
||||
// ========== 4. Agent 对话 (Completion) - 对应 RAGFlow_Agent_Dify接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Completion 对话请求")
|
||||
public static class CompletionReq implements Serializable {
|
||||
@Schema(description = "会话 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "会话 ID 不能为空")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "用户问题")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "true")
|
||||
@JsonProperty("stream")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
|
||||
@Schema(description = "是否返回追踪信息", defaultValue = "false")
|
||||
@JsonProperty("return_trace")
|
||||
@Builder.Default
|
||||
private Boolean returnTrace = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Completion 对话响应")
|
||||
public static class CompletionVO implements Serializable {
|
||||
@Schema(description = "会话 ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "回复内容")
|
||||
@JsonProperty("content")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "引用来源")
|
||||
@JsonProperty("reference")
|
||||
private Map<String, Object> reference;
|
||||
|
||||
@Schema(description = "追踪信息")
|
||||
@JsonProperty("trace")
|
||||
private List<Object> trace;
|
||||
}
|
||||
|
||||
// ========== 5. Dify 兼容检索 - 对应 RAGFlow_Agent_Dify接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Dify 兼容检索请求")
|
||||
public static class DifyRetrievalReq implements Serializable {
|
||||
@Schema(description = "知识库 ID")
|
||||
@JsonProperty("knowledge_id")
|
||||
private String knowledgeId;
|
||||
|
||||
@Schema(description = "查询词")
|
||||
@JsonProperty("query")
|
||||
private String query;
|
||||
|
||||
@Schema(description = "检索设置")
|
||||
@JsonProperty("retrieval_setting")
|
||||
private Map<String, Object> retrievalSetting;
|
||||
|
||||
@Schema(description = "元数据过滤条件")
|
||||
@JsonProperty("metadata_condition")
|
||||
private Map<String, Object> metadataCondition;
|
||||
|
||||
@Schema(description = "是否使用知识图谱")
|
||||
@JsonProperty("use_kg")
|
||||
private Boolean useKg;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Dify 兼容检索响应")
|
||||
public static class DifyRetrievalVO implements Serializable {
|
||||
@Schema(description = "检索结果列表")
|
||||
@JsonProperty("records")
|
||||
private List<Record> records;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "检索记录")
|
||||
public static class Record implements Serializable {
|
||||
@Schema(description = "内容")
|
||||
@JsonProperty("content")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "相似度分数")
|
||||
@JsonProperty("score")
|
||||
private Double score;
|
||||
|
||||
@Schema(description = "标题")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "元数据")
|
||||
@JsonProperty("metadata")
|
||||
private Map<String, Object> metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.bot;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "外部机器人 (Bot) 聚合 DTO")
|
||||
public class BotDTO {
|
||||
|
||||
// ========== 1. SearchBot (检索机器人) ==========
|
||||
|
||||
// 对应 /api/v1/searchbots/ask
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "SearchBot 提问请求")
|
||||
public static class SearchAskReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is RAG?")
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否返回引用", defaultValue = "false")
|
||||
@JsonProperty("quote")
|
||||
@Builder.Default
|
||||
private Boolean quote = false;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "true")
|
||||
@JsonProperty("stream")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "SearchBot 提问响应")
|
||||
public static class SearchAskVO implements Serializable {
|
||||
@Schema(description = "回答内容")
|
||||
@JsonProperty("answer")
|
||||
private String answer;
|
||||
|
||||
@Schema(description = "引用来源 (Value 结构通常对应 RetrievalDTO.HitVO)")
|
||||
@JsonProperty("reference")
|
||||
private Map<String, Object> reference;
|
||||
}
|
||||
|
||||
// 对应 /api/v1/searchbots/related_questions
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "相关问题请求")
|
||||
public static class RelatedQuestionReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
}
|
||||
|
||||
// 对应 /api/v1/searchbots/mindmap
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "思维导图请求")
|
||||
public static class MindMapReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
}
|
||||
|
||||
// ========== 2. AgentBot (嵌入式 Agent) ==========
|
||||
|
||||
// 对应 /api/v1/agentbots/{id}/inputs
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AgentBot 输入参数请求")
|
||||
public static class AgentInputsReq implements Serializable {
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AgentBot 输入参数定义响应")
|
||||
public static class AgentInputsVO implements Serializable {
|
||||
@Schema(description = "表单变量定义列表")
|
||||
@JsonProperty("variables")
|
||||
private List<Map<String, Object>> variables;
|
||||
}
|
||||
|
||||
// 对应 /api/v1/agentbots/{id}/completions
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AgentBot 对话请求")
|
||||
public static class AgentCompletionReq implements Serializable {
|
||||
@Schema(description = "输入参数值")
|
||||
@JsonProperty("inputs")
|
||||
private Map<String, Object> inputs;
|
||||
|
||||
@Schema(description = "用户查询", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "查询内容不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "true")
|
||||
@JsonProperty("stream")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
|
||||
@Schema(description = "会话 ID")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.chat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 聊天对话请求 DTO (OpenAI 兼容格式)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "聊天对话请求")
|
||||
public class ChatCompletionRequest implements Serializable {
|
||||
|
||||
@Schema(description = "模型标识 (对应 agent_id 或 bot_id)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("model")
|
||||
private String model;
|
||||
|
||||
@Schema(description = "对话消息列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("messages")
|
||||
private List<Message> messages;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "false")
|
||||
@JsonProperty("stream")
|
||||
private Boolean stream = false;
|
||||
|
||||
@Schema(description = "温度系数 (0-1)", defaultValue = "0.7")
|
||||
@JsonProperty("temperature")
|
||||
private Double temperature;
|
||||
|
||||
@Schema(description = "Session ID (可选,用于延续会话)")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "其他RAGFlow特定参数 (可选)")
|
||||
private Map<String, Object> extra;
|
||||
|
||||
@Data
|
||||
public static class Message implements Serializable {
|
||||
@Schema(description = "角色 (system, user, assistant)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String role;
|
||||
|
||||
@Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String content;
|
||||
}
|
||||
}
|
||||
@@ -1,523 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.chat;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 对话管理聚合 DTO
|
||||
* <p>
|
||||
* 容器类,内含对话助手、会话和消息的所有请求/响应对象。
|
||||
* </p>
|
||||
*/
|
||||
@Schema(description = "对话管理聚合 DTO")
|
||||
public class ChatDTO {
|
||||
|
||||
// ========== 1. 对话助手 (Assistant/Bot) 相关 ==========
|
||||
|
||||
/**
|
||||
* 提示词配置
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "提示词配置")
|
||||
public static class PromptConfig implements Serializable {
|
||||
|
||||
@Schema(description = "系统提示词", example = "你是一个专业的客服助手...")
|
||||
@JsonProperty("prompt")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "开场白", example = "您好,我是您的智能助手,请问有什么可以帮您?")
|
||||
private String opener;
|
||||
|
||||
@Schema(description = "空结果回复", example = "抱歉,我没有找到相关信息。")
|
||||
@JsonProperty("empty_response")
|
||||
private String emptyResponse;
|
||||
|
||||
@Schema(description = "是否展示引用", example = "true")
|
||||
@JsonProperty("show_quote")
|
||||
private Boolean quote;
|
||||
|
||||
@Schema(description = "是否启用 TTS", example = "false")
|
||||
private Boolean tts;
|
||||
|
||||
@Schema(description = "相似度阈值 (0.0 - 1.0)", example = "0.2")
|
||||
@JsonProperty("similarity_threshold")
|
||||
private Float similarityThreshold;
|
||||
|
||||
@Schema(description = "关键词相似度权重 (0.0 - 1.0)", example = "0.7")
|
||||
@JsonProperty("keywords_similarity_weight")
|
||||
private Float vectorSimilarityWeight;
|
||||
|
||||
@Schema(description = "检索 Top N", example = "6")
|
||||
@JsonProperty("top_n")
|
||||
private Integer topK;
|
||||
|
||||
@Schema(description = "Rerank 模型", example = "rerank_model_001")
|
||||
@JsonProperty("rerank_model")
|
||||
private String rerankId;
|
||||
|
||||
@Schema(description = "是否启用多轮对话优化", example = "false")
|
||||
@JsonProperty("refine_multiturn")
|
||||
private Boolean refineMultigraph;
|
||||
|
||||
@Schema(description = "变量列表")
|
||||
private List<Map<String, Object>> variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 配置
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "LLM 模型配置")
|
||||
public static class LLMConfig implements Serializable {
|
||||
|
||||
@NotBlank(message = "模型名称不能为空")
|
||||
@Schema(description = "模型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-4")
|
||||
@JsonProperty("model_name")
|
||||
private String modelName;
|
||||
|
||||
@Schema(description = "温度参数 (0.0 - 2.0)", example = "0.7")
|
||||
private Float temperature;
|
||||
|
||||
@Schema(description = "Top P 采样", example = "0.9")
|
||||
@JsonProperty("top_p")
|
||||
private Float topP;
|
||||
|
||||
@Schema(description = "最大 Token 数", example = "4096")
|
||||
@JsonProperty("max_tokens")
|
||||
private Integer maxTokens;
|
||||
|
||||
@Schema(description = "存在惩罚", example = "0.0")
|
||||
@JsonProperty("presence_penalty")
|
||||
private Float presencePenalty;
|
||||
|
||||
@Schema(description = "频率惩罚", example = "0.0")
|
||||
@JsonProperty("frequency_penalty")
|
||||
private Float frequencyPenalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建助手请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "创建助手请求")
|
||||
public static class AssistantCreateReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "助手名称不能为空")
|
||||
@Schema(description = "助手名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能客服助手")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "助手头像 (Base64 编码)", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "助手描述", example = "这是一个智能客服助手")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "LLM 模型配置")
|
||||
@JsonProperty("llm")
|
||||
private LLMConfig llm;
|
||||
|
||||
@Schema(description = "提示词配置")
|
||||
@JsonProperty("prompt")
|
||||
private PromptConfig promptConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新助手请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "更新助手请求")
|
||||
public static class AssistantUpdateReq implements Serializable {
|
||||
|
||||
@Schema(description = "助手名称", example = "智能客服助手 V2")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "助手头像 (Base64 编码)", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "助手描述", example = "这是一个智能客服助手")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "LLM 模型配置")
|
||||
@JsonProperty("llm")
|
||||
private LLMConfig llm;
|
||||
|
||||
@Schema(description = "提示词配置")
|
||||
@JsonProperty("prompt")
|
||||
private PromptConfig promptConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询助手列表请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "查询助手列表请求")
|
||||
public static class AssistantListReq implements Serializable {
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "按名称过滤 (模糊匹配)", example = "客服")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "排序字段: create_time / update_time", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
|
||||
@Schema(description = "按 ID 精确筛选", example = "assistant_001")
|
||||
private String id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 助手详情 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "助手详情 VO")
|
||||
public static class AssistantVO implements Serializable {
|
||||
|
||||
@Schema(description = "助手 ID", example = "assistant_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "租户 ID", example = "tenant_001")
|
||||
@JsonProperty("tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
@Schema(description = "助手名称", example = "智能客服助手")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "助手头像", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "关联的知识库 ID 列表")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "关联的知识库列表 (详情)")
|
||||
private List<SimpleDatasetVO> datasets;
|
||||
|
||||
@Schema(description = "助手描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "LLM 模型配置")
|
||||
@JsonProperty("llm")
|
||||
private LLMConfig llm;
|
||||
|
||||
@Schema(description = "提示词配置")
|
||||
@JsonProperty("prompt")
|
||||
private PromptConfig promptConfig;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除助手请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "删除助手请求")
|
||||
public static class AssistantDeleteReq implements Serializable {
|
||||
|
||||
@Schema(description = "要删除的助手 ID 列表", example = "[\"assistant_001\", \"assistant_002\"]")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
// ========== 2. 会话 (Session) 相关 ==========
|
||||
|
||||
/**
|
||||
* 创建会话请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "创建会话请求")
|
||||
public static class SessionCreateReq implements Serializable {
|
||||
|
||||
@Schema(description = "会话名称", example = "技术咨询会话")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "用户 ID", example = "user_001")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "更新会话请求")
|
||||
public static class SessionUpdateReq implements Serializable {
|
||||
|
||||
@Schema(description = "会话名称", example = "技术咨询会话 - 更新")
|
||||
private String name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询会话列表请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "查询会话列表请求")
|
||||
public static class SessionListReq implements Serializable {
|
||||
|
||||
@Schema(description = "助手 ID", example = "assistant_001")
|
||||
@JsonProperty("assistant_id")
|
||||
private String assistantId;
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "按名称过滤", example = "技术")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "排序字段", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
|
||||
@Schema(description = "会话 ID 精确筛选", example = "session_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "用户标识筛选", example = "user_001")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话详情 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "会话详情 VO")
|
||||
public static class SessionVO implements Serializable {
|
||||
|
||||
@Schema(description = "会话 ID", example = "session_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "助手 ID", example = "assistant_001")
|
||||
@JsonProperty("chat_id")
|
||||
private String chatId;
|
||||
|
||||
@Schema(description = "助手 ID (兼容旧版)", example = "assistant_001")
|
||||
@JsonProperty("assistant_id")
|
||||
private String assistantId;
|
||||
|
||||
@Schema(description = "会话名称", example = "技术咨询会话")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "创建日期", example = "2024-05-01 10:00:00")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "更新日期", example = "2024-05-01 10:00:00")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
|
||||
@Schema(description = "用户 ID", example = "user_001")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "对话历史消息列表")
|
||||
private List<Map<String, Object>> messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "删除会话请求")
|
||||
public static class SessionDeleteReq implements Serializable {
|
||||
|
||||
@Schema(description = "要删除的会话 ID 列表", example = "[\"session_001\", \"session_002\"]")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
// ========== 3. 消息/对话 (Completion) 相关 ==========
|
||||
|
||||
/**
|
||||
* 发送消息请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "发送消息请求")
|
||||
public static class CompletionReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "问题内容不能为空")
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "请介绍一下你们的产品")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否使用流式响应 (SSE)", example = "true")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
|
||||
@NotBlank(message = "会话 ID 不能为空")
|
||||
@Schema(description = "会话 ID (可选,不传则创建新会话)", example = "session_001")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "是否展示引用", example = "true")
|
||||
private Boolean quote;
|
||||
|
||||
@Schema(description = "指定检索的文档 ID 列表 (逗号分隔)", example = "doc_001,doc_002")
|
||||
@JsonProperty("doc_ids")
|
||||
private String docIds;
|
||||
|
||||
@Schema(description = "元数据过滤条件")
|
||||
@JsonProperty("metadata_condition")
|
||||
private Map<String, Object> metadataCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息响应 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "消息响应 VO")
|
||||
public static class CompletionVO implements Serializable {
|
||||
|
||||
@Schema(description = "AI 回答内容")
|
||||
private String answer;
|
||||
|
||||
@Schema(description = "引用信息")
|
||||
private Reference reference;
|
||||
|
||||
@Schema(description = "会话 ID", example = "session_001")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "任务 ID (用于流式响应追踪)", example = "task_001")
|
||||
@JsonProperty("task_id")
|
||||
private String taskId;
|
||||
|
||||
/**
|
||||
* 引用信息 (检索命中结果)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "引用信息")
|
||||
public static class Reference implements Serializable {
|
||||
|
||||
@Schema(description = "命中的文档块列表")
|
||||
private List<xiaozhi.modules.knowledge.dto.document.RetrievalDTO.HitVO> chunks;
|
||||
|
||||
@Schema(description = "文档聚合信息")
|
||||
@JsonProperty("doc_aggs")
|
||||
private List<DocAgg> docAggs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档聚合信息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文档聚合信息")
|
||||
public static class DocAgg implements Serializable {
|
||||
|
||||
@Schema(description = "文档 ID", example = "doc_001")
|
||||
@JsonProperty("doc_id")
|
||||
private String docId;
|
||||
|
||||
@Schema(description = "文档名称", example = "产品手册.pdf")
|
||||
@JsonProperty("doc_name")
|
||||
private String docName;
|
||||
|
||||
@Schema(description = "命中次数", example = "3")
|
||||
private Integer count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简易知识库 VO (用于 Assistant 列表)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "简易知识库 VO")
|
||||
public static class SimpleDatasetVO implements Serializable {
|
||||
@Schema(description = "知识库 ID")
|
||||
private String id;
|
||||
@Schema(description = "知识库名称")
|
||||
private String name;
|
||||
@Schema(description = "头像")
|
||||
private String avatar;
|
||||
@Schema(description = "分块数量")
|
||||
@JsonProperty("chunk_num")
|
||||
private Integer chunkNum;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.common;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "通用扩展功能 DTO")
|
||||
public class CommonDTO {
|
||||
|
||||
// ========== 1. 引用详情 (detail_share_embedded) ==========
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "引用详情请求")
|
||||
public static class ReferenceDetailReq implements Serializable {
|
||||
@Schema(description = "切片 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "切片 ID 不能为空")
|
||||
@JsonProperty("chunk_id")
|
||||
private String chunkId;
|
||||
|
||||
@Schema(description = "知识库 ID")
|
||||
@JsonProperty("knowledge_id")
|
||||
private String knowledgeId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "引用详情响应")
|
||||
public static class ReferenceDetailVO implements Serializable {
|
||||
@Schema(description = "切片 ID")
|
||||
@JsonProperty("chunk_id")
|
||||
private String chunkId;
|
||||
|
||||
@Schema(description = "完整内容")
|
||||
@JsonProperty("content_with_weight")
|
||||
private String contentWithWeight;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
@JsonProperty("doc_name")
|
||||
private String docName;
|
||||
|
||||
@Schema(description = "图片 ID 列表")
|
||||
@JsonProperty("img_id")
|
||||
private String imageId; // 注意:RAGFlow 有时返回 String 有时返回 List,需根据实际情况确认,暂定 String 用于 ID
|
||||
|
||||
@Schema(description = "文档 ID")
|
||||
@JsonProperty("doc_id")
|
||||
private String docId;
|
||||
}
|
||||
|
||||
// ========== 2. 通用问答 (ask_about) - 调试用 ==========
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "通用问答请求 (调试用)")
|
||||
public static class AskAboutReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is this dataset about?")
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "数据集 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "数据集列表不能为空")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
}
|
||||
|
||||
// 响应通常复用 String 或者简单的 Map 结构,视具体实现而定,暂不定义专用 VO
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.file;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 文件管理聚合 DTO
|
||||
* <p>
|
||||
* 容器类,内含文件模块所有请求/响应对象的静态内部类定义。
|
||||
* </p>
|
||||
*/
|
||||
@Schema(description = "文件管理聚合 DTO")
|
||||
public class FileDTO {
|
||||
|
||||
// ========== 请求类 ==========
|
||||
|
||||
/**
|
||||
* 文件上传请求 (对应接口 1: upload)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件上传请求")
|
||||
public static class UploadReq implements Serializable {
|
||||
|
||||
@NotNull(message = "文件不能为空")
|
||||
@Schema(description = "上传的文件", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private MultipartFile file;
|
||||
|
||||
@Schema(description = "父文件夹 ID (为空则上传到根目录)", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建文件夹请求 (对应接口 2: create)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "新建文件夹请求")
|
||||
public static class CreateReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "文件夹名称不能为空")
|
||||
@Schema(description = "文件夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "新建文件夹")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "父文件夹 ID (为空则创建在根目录)", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
|
||||
@NotBlank(message = "类型不能为空")
|
||||
@Schema(description = "类型: FOLDER", requiredMode = Schema.RequiredMode.REQUIRED, example = "FOLDER")
|
||||
@Builder.Default
|
||||
private String type = "FOLDER";
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名请求 (对应接口 6: rename)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "重命名请求")
|
||||
public static class RenameReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "文件 ID 不能为空")
|
||||
@Schema(description = "文件/文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "file_001")
|
||||
@JsonProperty("file_id")
|
||||
private String fileId;
|
||||
|
||||
@NotBlank(message = "新名称不能为空")
|
||||
@Schema(description = "新名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "重命名后的文件")
|
||||
private String name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动请求 (对应接口 7: move)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "移动请求")
|
||||
public static class MoveReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "源文件 ID 列表不能为空")
|
||||
@Schema(description = "源文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
|
||||
@JsonProperty("src_file_ids")
|
||||
private List<String> srcFileIds;
|
||||
|
||||
@NotBlank(message = "目标文件夹 ID 不能为空")
|
||||
@Schema(description = "目标文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "folder_002")
|
||||
@JsonProperty("dest_file_id")
|
||||
private String destFileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除请求 (对应接口 8: rm)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "批量删除请求")
|
||||
public static class RemoveReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "文件 ID 列表不能为空")
|
||||
@Schema(description = "文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
|
||||
@JsonProperty("file_ids")
|
||||
private List<String> fileIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入知识库请求 (对应接口 9: convert)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "导入知识库请求")
|
||||
public static class ConvertReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "文件 ID 列表不能为空")
|
||||
@Schema(description = "文件 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
|
||||
@JsonProperty("file_ids")
|
||||
private List<String> fileIds;
|
||||
|
||||
@NotEmpty(message = "知识库 ID 列表不能为空")
|
||||
@Schema(description = "目标知识库 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"kb_001\"]")
|
||||
@JsonProperty("kb_ids")
|
||||
private List<String> kbIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表查询请求 (对应接口 3: list_files)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "列表查询请求")
|
||||
public static class ListReq implements Serializable {
|
||||
|
||||
@Schema(description = "父文件夹 ID (为空则查询根目录)", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
|
||||
@Schema(description = "关键词搜索", example = "文档")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "排序字段: create_time / update_time / name / size", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
}
|
||||
|
||||
// ========== 响应类 ==========
|
||||
|
||||
/**
|
||||
* 文件/文件夹基础信息 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件/文件夹基础信息")
|
||||
public static class InfoVO implements Serializable {
|
||||
|
||||
@Schema(description = "文件/文件夹 ID", example = "file_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "父文件夹 ID", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
|
||||
@Schema(description = "租户 ID", example = "tenant_001")
|
||||
@JsonProperty("tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
@Schema(description = "创建者 ID", example = "user_001")
|
||||
@JsonProperty("created_by")
|
||||
private String createdBy;
|
||||
|
||||
@Schema(description = "类型: FOLDER / FILE", example = "FOLDER")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "名称", example = "我的文件夹")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "路径位置", example = "/root/folder")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "文件大小 (字节)", example = "1024")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "来源类型", example = "local")
|
||||
@JsonProperty("source_type")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
|
||||
@Schema(description = "文件扩展名", example = "pdf")
|
||||
private String extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表响应 VO (对应接口 3: list_files)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件列表响应")
|
||||
public static class ListVO implements Serializable {
|
||||
|
||||
@Schema(description = "总记录数", example = "100")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "当前父文件夹信息")
|
||||
@JsonProperty("parent_folder")
|
||||
private InfoVO parentFolder;
|
||||
|
||||
@Schema(description = "文件/文件夹列表")
|
||||
private List<InfoVO> files;
|
||||
|
||||
@Schema(description = "面包屑导航路径")
|
||||
private List<InfoVO> breadcrumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换结果项 VO (对应接口 9: convert)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件转换结果项")
|
||||
public static class ConvertVO implements Serializable {
|
||||
|
||||
@Schema(description = "转换记录 ID", example = "convert_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "源文件 ID", example = "file_001")
|
||||
@JsonProperty("file_id")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "目标文档 ID", example = "doc_001")
|
||||
@JsonProperty("document_id")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换状态 VO (对应接口 10: get_convert_status)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件转换状态")
|
||||
public static class ConvertStatusVO implements Serializable {
|
||||
|
||||
@Schema(description = "转换状态: pending / processing / completed / failed", example = "completed")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "转换进度 (0.0 - 1.0)", example = "1.0")
|
||||
private Float progress;
|
||||
|
||||
@Schema(description = "状态消息", example = "转换完成")
|
||||
private String message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 面包屑 VO (对应接口 12: all_parent_folder)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "面包屑导航 (所有父文件夹)")
|
||||
public static class BreadcrumbVO implements Serializable {
|
||||
|
||||
@Schema(description = "父文件夹列表 (从根到当前的路径)")
|
||||
@JsonProperty("parent_folders")
|
||||
private List<InfoVO> parentFolders;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根目录信息 VO (对应接口 10: get_root_folder)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "根目录信息")
|
||||
public static class RootFolderVO implements Serializable {
|
||||
|
||||
@Schema(description = "根文件夹信息")
|
||||
@JsonProperty("root_folder")
|
||||
private InfoVO rootFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 父目录信息 VO (对应接口 11: get_parent_folder)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "父目录信息")
|
||||
public static class ParentFolderVO implements Serializable {
|
||||
|
||||
@Schema(description = "父文件夹信息")
|
||||
@JsonProperty("parent_folder")
|
||||
private InfoVO parentFolder;
|
||||
}
|
||||
}
|
||||
@@ -485,16 +485,11 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||
|
||||
@Override
|
||||
public Integer getDocumentCount(String datasetId) {
|
||||
try {
|
||||
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
|
||||
if (info != null && info.getDocumentCount() != null) {
|
||||
return info.getDocumentCount().intValue();
|
||||
}
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取文档数量失败: {}", e.getMessage());
|
||||
return 0;
|
||||
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
|
||||
if (info != null && info.getDocumentCount() != null) {
|
||||
return info.getDocumentCount().intValue();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -518,7 +513,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage());
|
||||
return null;
|
||||
throw convertToRenException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,9 +100,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
}
|
||||
|
||||
DatasetDTO.InfoVO datasetInfo = adapter.getDatasetInfo(dto.getDatasetId());
|
||||
|
||||
// getDatasetInfo 正常返回 null 表示远端确实不存在;异常时已抛出 RenException 由外层 catch 接管
|
||||
if (datasetInfo == null) {
|
||||
// RAGFlow 端已删除 → 本地级联清理
|
||||
// RAGFlow 端已确认删除 → 本地级联清理
|
||||
log.info("数据集 {} 在 RAGFlow 端不存在,执行本地清理", dto.getDatasetId());
|
||||
cleanupLocalDataset(dto.getDatasetId(), dto.getId());
|
||||
// 标记为已删除,让上层从列表中移除
|
||||
@@ -145,8 +145,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
|
||||
log.error("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
|
||||
dto.setDocumentCount(0);
|
||||
dto.setErrorMessage(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -529,7 +529,8 @@ public class KnowledgeFilesServiceImpl extends BaseServiceImpl<DocumentDao, Docu
|
||||
adapter.deleteDocument(datasetId, req);
|
||||
log.info("远程批量删除请求成功");
|
||||
} catch (Exception e) {
|
||||
log.warn("远程删除请求部分或全部失败: {}", e.getMessage());
|
||||
log.error("远程删除请求失败,中止本地清理以避免数据不一致: {}", e.getMessage());
|
||||
throw new RenException(e.getMessage());
|
||||
}
|
||||
|
||||
// 4. 原子化清理本地影子记录并同步统计数据
|
||||
|
||||
@@ -87,8 +87,7 @@ public class ShiroConfig {
|
||||
filterMap.put("/user/retrieve-password", "anon");
|
||||
// 将config路径使用server服务过滤器
|
||||
filterMap.put("/config/**", "server");
|
||||
filterMap.put("/device/address-book/lookup", "server");
|
||||
filterMap.put("/device/call/forward", "server");
|
||||
filterMap.put("/device/address-book/call", "server");
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/chat-history/download/**", "anon");
|
||||
filterMap.put("/agent/chat-summary/**", "server");
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 更新EdgeTTS供应器增加语速、音调、音量配置
|
||||
UPDATE `ai_model_provider`
|
||||
SET fields = '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"rate","label":"语速(-100~100)","type":"number"},{"key":"volume","label":"音量(0~100)","type":"number"},{"key":"pitch","label":"音调(-100~100)","type":"number"}]'
|
||||
WHERE id = 'SYSTEM_TTS_edge';
|
||||
|
||||
UPDATE `ai_model_config` SET
|
||||
`remark` = 'EdgeTTS配置说明:
|
||||
1. 使用微软Edge TTS服务
|
||||
2. 支持多种语言和音色
|
||||
3. 免费使用,无需注册
|
||||
4. 需要网络连接
|
||||
5. 输出文件保存在tmp/目录
|
||||
6. 语速:-100~100,0为正常速度
|
||||
7. 音量:0~100,50为正常音量
|
||||
8. 音调:-100~100,0为正常音调' WHERE `id` = 'TTS_EdgeTTS';
|
||||
@@ -0,0 +1,40 @@
|
||||
-- 修正、添加Minimax流式TTS音色
|
||||
delete from `ai_tts_voice` where tts_model_id = 'TTS_MinimaxStreamTTS';
|
||||
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0001', 'TTS_MinimaxStreamTTS', '少女音', 'female-shaonv', '普通话', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0002', 'TTS_MinimaxStreamTTS', '成熟女声', 'female-chengshu', '普通话', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0003', 'TTS_MinimaxStreamTTS', '霸道少爷', 'badao_shaoye', '普通话', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0004', 'TTS_MinimaxStreamTTS', '病娇弟弟', 'bingjiao_didi', '普通话', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0005', 'TTS_MinimaxStreamTTS', '纯真学弟', 'chunzhen_xuedi', '普通话', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0006', 'TTS_MinimaxStreamTTS', '冷淡学长', 'lengdan_xiongzhang', '普通话', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0007', 'TTS_MinimaxStreamTTS', '甜美小玲', 'tianxin_xiaoling', '普通话', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0008', 'TTS_MinimaxStreamTTS', '俏皮萌妹', 'qiaopi_mengmei', '普通话', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0009', 'TTS_MinimaxStreamTTS', '妩媚御姐', 'wumei_yujie', '普通话', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0010', 'TTS_MinimaxStreamTTS', '调教学妹', 'diadia_xuemei', '普通话', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0011', 'TTS_MinimaxStreamTTS', '淡雅学姐', 'danya_xuejie', '普通话', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0012', 'TTS_MinimaxStreamTTS', 'Santa Claus', 'Santa_Claus', '普通话', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0013', 'TTS_MinimaxStreamTTS', 'Grinch', 'Grinch', '普通话', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0014', 'TTS_MinimaxStreamTTS', '专业女主持', 'Cantonese_ProfessionalHost(F)', '粤语', "https://filecdn.minimax.chat/public/84589766-c651-477b-b320-36b89599f953.mp3", NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0015', 'TTS_MinimaxStreamTTS', '温柔女声', 'Cantonese_GentleLady', '粤语', "https://filecdn.minimax.chat/public/d391936d-d586-4dab-886c-a2a084a20920.mp3", NULL, NULL, NULL, 15, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0016', 'TTS_MinimaxStreamTTS', '专业男主持', 'Cantonese_ProfessionalHost(M)', '粤语', NULL, NULL, NULL, NULL, 16, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0017', 'TTS_MinimaxStreamTTS', '活泼男声', 'Cantonese_PlayfulMan', '粤语', NULL, NULL, NULL, NULL, 17, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0018', 'TTS_MinimaxStreamTTS', '可爱女孩', 'Cantonese_CuteGirl', '粤语', "https://filecdn.minimax.chat/public/ff96520a-ac12-491d-945e-d6337467eb7d.mp3", NULL, NULL, NULL, 18, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0019', 'TTS_MinimaxStreamTTS', '善良女声', 'Cantonese_KindWoman', '粤语', "https://filecdn.minimax.chat/public/78239a18-5e9c-4538-94f6-2f504c080374.mp3", NULL, NULL, NULL, 19, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0020', 'TTS_MinimaxStreamTTS', 'Crisp News Anchor', 'Cantonese_crisp_news_anchor_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/741a5a4b-f42a-4a9b-ae83-3954d097ab73.mp3', NULL, NULL, NULL, 20, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0021', 'TTS_MinimaxStreamTTS', 'Pro Reporter', 'Cantonese_professional_reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/77afb86d-2cc4-4149-8c2e-e1174fa849d4.mp3', NULL, NULL, NULL, 21, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0022', 'TTS_MinimaxStreamTTS', 'Crisp Reporter', 'Cantonese_crisp_reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/4ad39a61-628e-481c-9d60-bfb7c36bceb8.mp3', NULL, NULL, NULL, 22, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0023', 'TTS_MinimaxStreamTTS', 'Articulate', 'Cantonese_Articulate_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/9346601d-3035-4f9e-9b1c-0e03f30e5eef.mp3', NULL, NULL, NULL, 23, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0024', 'TTS_MinimaxStreamTTS', 'Warm Reporter', 'Cantonese_Warm_Reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/c651050e-d6a5-4653-b09e-5177bdc8b04c.mp3', NULL, NULL, NULL, 24, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0025', 'TTS_MinimaxStreamTTS', 'Energetic', 'Cantonese_energetic_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/1fd1c461-71da-450d-91bb-45f819249b44.mp3', NULL, NULL, NULL, 25, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0026', 'TTS_MinimaxStreamTTS', 'Objective Narrator', 'Cantonese_objective_narrator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/463c5a68-a7b5-4fe9-85b6-d494dca74abb.mp3', NULL, NULL, NULL, 26, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0027', 'TTS_MinimaxStreamTTS', '沙哑男声', 'Cantonese_Narrator', '粤语', 'https://filecdn.minimax.chat/public/22e29cb0-0f5a-443a-9fa9-a32dabd55cf9.mp3', NULL, NULL, NULL, 27, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0028', 'TTS_MinimaxStreamTTS', 'News Anchor', 'Cantonese_news_anchor_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/59b9ba49-47d4-48f1-b207-09c0e0769a1b.mp3', NULL, NULL, NULL, 28, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0029', 'TTS_MinimaxStreamTTS', '渊博学者', 'Cantonese_WiselProfessor', '粤语', 'https://filecdn.minimax.chat/public/8de234c4-64a7-4539-a316-c3b43ee1d0a9.mp3', NULL, NULL, NULL, 29, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0030', 'TTS_MinimaxStreamTTS', 'Resonant Host', 'Cantonese_resonant_host_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/ec8fc0ce-6657-4c49-9bf0-3953a15046ec.mp3', NULL, NULL, NULL, 30, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0031', 'TTS_MinimaxStreamTTS', '冷漠员工', 'Cantonese_IndifferentStaff', '粤语', 'https://filecdn.minimax.chat/public/9686682c-3fc8-45f0-bd88-2a4692e1f63e.mp3', NULL, NULL, NULL, 31, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0032', 'TTS_MinimaxStreamTTS', 'Male News Anchor', 'Cantonese_Male_news_anchor_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/2055ee8b-fb25-424c-b8cb-e2baf971e520.mp3', NULL, NULL, NULL, 32, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0033', 'TTS_MinimaxStreamTTS', 'History Narrator', 'Cantonese_casual_narrator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/c170f9e4-145d-4331-9910-1c3a0e6bbc3a.mp3', NULL, NULL, NULL, 33, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0034', 'TTS_MinimaxStreamTTS', 'Steady Reporter', 'Cantonese_Steady_reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/381cfee2-772d-4a73-b30c-7914aec05771.mp3', NULL, NULL, NULL, 34, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0035', 'TTS_MinimaxStreamTTS', 'Serious Commentator', 'Cantonese_serious_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/57335c8a-32ea-475f-a198-b0de7fa1b126.mp3', NULL, NULL, NULL, 35, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0036', 'TTS_MinimaxStreamTTS', 'Podcast Host', 'Cantonese_podcast_host_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/048b617f-e1f8-422c-89fb-fdbc6e303675.mp3', NULL, NULL, NULL, 36, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0037', 'TTS_MinimaxStreamTTS', 'Obj commentator', 'Cantonese_Objective_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/a8f55e44-b726-4ba5-815a-f1b515ba57ef.mp3', NULL, NULL, NULL, 37, NULL, NULL, NULL, NULL);
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Add language configuration for local FunASR ASR.
|
||||
UPDATE `ai_model_provider`
|
||||
SET `fields` = '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"language","label":"识别语言","type":"string","default":"auto"}]'
|
||||
WHERE `id` = 'SYSTEM_ASR_FunASR';
|
||||
|
||||
UPDATE `ai_model_config`
|
||||
SET `config_json` = JSON_SET(`config_json`, '$.language', 'auto')
|
||||
WHERE `id` = 'ASR_FunASR'
|
||||
AND JSON_EXTRACT(`config_json`, '$.language') IS NULL;
|
||||
|
||||
-- Update the FunASR local model configuration description to mention the language option.
|
||||
UPDATE `ai_model_config`
|
||||
SET `remark` = 'FunASR本地模型配置说明:
|
||||
1. 需要下载模型文件到xiaozhi-server/models/SenseVoiceSmall目录
|
||||
2. 支持中日韩粤语音识别
|
||||
3. 本地推理,无需网络连接
|
||||
4. 待识别文件保存在tmp/目录
|
||||
5. “识别语言”字段控制识别语种:auto = 自动检测;如需限定只识别中文可设为 zh(en=英语、ja=日语、ko=韩语、yue=粤语)。'
|
||||
WHERE `id` = 'ASR_FunASR';
|
||||
@@ -0,0 +1,20 @@
|
||||
-- liquibase formatted sql
|
||||
|
||||
-- changeset tykechen:202607071530
|
||||
CREATE TABLE IF NOT EXISTS `ai_agent_snapshot` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '快照ID',
|
||||
`agent_id` VARCHAR(32) NOT NULL COMMENT '智能体ID',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '所属用户ID',
|
||||
`version_no` INT UNSIGNED NOT NULL COMMENT '版本号',
|
||||
`snapshot_data` JSON NOT NULL DEFAULT (JSON_OBJECT()) COMMENT '快照数据',
|
||||
`changed_fields` JSON DEFAULT NULL COMMENT '变更字段',
|
||||
`source` VARCHAR(32) DEFAULT 'config' COMMENT '快照来源',
|
||||
`restore_from_snapshot_id` VARCHAR(32) DEFAULT NULL COMMENT '恢复来源快照ID',
|
||||
`restore_from_version_no` INT UNSIGNED DEFAULT NULL COMMENT '恢复来源版本号',
|
||||
`creator` BIGINT DEFAULT NULL COMMENT '创建者',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_agent_version` (`agent_id`, `version_no`),
|
||||
INDEX `idx_agent_created_at` (`agent_id`, `created_at`),
|
||||
INDEX `idx_snapshot_user_created_at` (`user_id`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置快照表';
|
||||
@@ -676,3 +676,31 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202605251426.sql
|
||||
- changeSet:
|
||||
id: 202606231030
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202606231030.sql
|
||||
- changeSet:
|
||||
id: 202606261131
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202606261131.sql
|
||||
- changeSet:
|
||||
id: 202607011405
|
||||
author: LauraGPT
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202607011405.sql
|
||||
- changeSet:
|
||||
id: 202607071530
|
||||
author: tykechen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202607071530.sql
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
<result column="memModelId" property="memModelId"/>
|
||||
<result column="intentModelId" property="intentModelId"/>
|
||||
|
||||
<result column="functions" property="functions"
|
||||
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
|
||||
<result column="chatHistoryConf" property="chatHistoryConf"/>
|
||||
<result column="systemPrompt" property="systemPrompt"/>
|
||||
<result column="summaryMemory" property="summaryMemory"/>
|
||||
@@ -38,6 +35,13 @@
|
||||
<result column="createdAt" property="createdAt"/>
|
||||
<result column="updater" property="updater"/>
|
||||
<result column="updatedAt" property="updatedAt"/>
|
||||
<collection property="functions"
|
||||
ofType="xiaozhi.modules.agent.entity.AgentPluginMapping">
|
||||
<id column="functionId" property="id"/>
|
||||
<result column="functionAgentId" property="agentId"/>
|
||||
<result column="pluginId" property="pluginId"/>
|
||||
<result column="paramInfo" property="paramInfo"/>
|
||||
</collection>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectAgentInfoById" resultMap="AgentInfoMap">
|
||||
@@ -58,19 +62,6 @@
|
||||
a.tts_pitch AS ttsPitch,
|
||||
a.mem_model_id AS memModelId,
|
||||
a.intent_model_id AS intentModelId,
|
||||
COALESCE(
|
||||
(SELECT JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'id', m.id,
|
||||
'agentId', m.agent_id,
|
||||
'pluginId', m.plugin_id,
|
||||
'paramInfo', m.param_info
|
||||
)
|
||||
)
|
||||
FROM ai_agent_plugin_mapping m
|
||||
WHERE m.agent_id = a.id),
|
||||
JSON_ARRAY()
|
||||
) AS functions,
|
||||
a.chat_history_conf AS chatHistoryConf,
|
||||
a.system_prompt AS systemPrompt,
|
||||
a.summary_memory AS summaryMemory,
|
||||
@@ -80,8 +71,22 @@
|
||||
a.creator,
|
||||
a.created_at AS createdAt,
|
||||
a.updater,
|
||||
a.updated_at AS updatedAt
|
||||
a.updated_at AS updatedAt,
|
||||
f.id AS functionId,
|
||||
f.agent_id AS functionAgentId,
|
||||
f.plugin_id AS pluginId,
|
||||
f.param_info AS paramInfo
|
||||
FROM ai_agent a
|
||||
LEFT JOIN ai_agent_plugin_mapping f ON f.agent_id = a.id
|
||||
WHERE a.id = #{agentId}
|
||||
ORDER BY f.id ASC
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
<select id="selectByIdForUpdate" resultType="xiaozhi.modules.agent.entity.AgentEntity">
|
||||
SELECT *
|
||||
FROM ai_agent
|
||||
WHERE id = #{agentId}
|
||||
FOR UPDATE
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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.AgentSnapshotDao">
|
||||
|
||||
<select id="selectMaxVersionNo" resultType="java.lang.Integer">
|
||||
SELECT COALESCE(MAX(version_no), 0)
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
</select>
|
||||
|
||||
<select id="selectLatestSnapshot" resultType="xiaozhi.modules.agent.entity.AgentSnapshotEntity">
|
||||
SELECT id,
|
||||
agent_id AS agentId,
|
||||
user_id AS userId,
|
||||
version_no AS versionNo,
|
||||
snapshot_data AS snapshotData,
|
||||
changed_fields AS changedFields,
|
||||
source,
|
||||
restore_from_snapshot_id AS restoreFromSnapshotId,
|
||||
restore_from_version_no AS restoreFromVersionNo,
|
||||
creator,
|
||||
created_at AS createdAt
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
ORDER BY version_no DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectNextSnapshot" resultType="xiaozhi.modules.agent.entity.AgentSnapshotEntity">
|
||||
SELECT id,
|
||||
agent_id AS agentId,
|
||||
user_id AS userId,
|
||||
version_no AS versionNo,
|
||||
snapshot_data AS snapshotData,
|
||||
changed_fields AS changedFields,
|
||||
source,
|
||||
restore_from_snapshot_id AS restoreFromSnapshotId,
|
||||
restore_from_version_no AS restoreFromVersionNo,
|
||||
creator,
|
||||
created_at AS createdAt
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
AND version_no > #{versionNo}
|
||||
ORDER BY version_no ASC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<insert id="insertWithNextVersion">
|
||||
INSERT INTO ai_agent_snapshot (
|
||||
id,
|
||||
agent_id,
|
||||
user_id,
|
||||
version_no,
|
||||
snapshot_data,
|
||||
changed_fields,
|
||||
source,
|
||||
restore_from_snapshot_id,
|
||||
restore_from_version_no,
|
||||
creator,
|
||||
created_at
|
||||
)
|
||||
SELECT #{snapshot.id},
|
||||
#{snapshot.agentId},
|
||||
#{snapshot.userId},
|
||||
COALESCE(MAX(version_no), 0) + 1,
|
||||
#{snapshot.snapshotData},
|
||||
#{snapshot.changedFields},
|
||||
#{snapshot.source},
|
||||
#{snapshot.restoreFromSnapshotId},
|
||||
#{snapshot.restoreFromVersionNo},
|
||||
#{snapshot.creator},
|
||||
#{snapshot.createdAt}
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{snapshot.agentId}
|
||||
</insert>
|
||||
|
||||
<delete id="deleteOlderThanKeepLimit">
|
||||
DELETE FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
AND id NOT IN (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT id
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
ORDER BY version_no DESC
|
||||
LIMIT #{keepLimit}
|
||||
) retained_snapshots
|
||||
)
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@@ -25,4 +25,15 @@
|
||||
INSERT INTO ai_device_address_book (mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date)
|
||||
VALUES (#{macAddress}, #{targetMac}, #{alias}, #{hasPermission}, #{creator}, NOW(), #{updater}, NOW())
|
||||
</insert>
|
||||
|
||||
<delete id="deleteByMacAddresses">
|
||||
DELETE FROM ai_device_address_book WHERE mac_address IN
|
||||
<foreach collection="macAddresses" item="mac" open="(" separator="," close=")">
|
||||
#{mac}
|
||||
</foreach>
|
||||
OR target_mac IN
|
||||
<foreach collection="macAddresses" item="mac" open="(" separator="," close=")">
|
||||
#{mac}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -0,0 +1,44 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AgentUpdateDTOTest {
|
||||
|
||||
@Test
|
||||
void functionInfoAcceptsJsonStringParamInfo() {
|
||||
AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo();
|
||||
|
||||
info.setParamInfo("{\"api_key\":\"abc\",\"max_results\":5}");
|
||||
|
||||
assertEquals("abc", info.getParamInfo().get("api_key"));
|
||||
assertEquals(5, info.getParamInfo().get("max_results"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionInfoNormalizesMapKeysToStrings() {
|
||||
AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo();
|
||||
Map<Object, Object> params = new LinkedHashMap<>();
|
||||
params.put("api_key", "abc");
|
||||
params.put(42, true);
|
||||
|
||||
info.setParamInfo(params);
|
||||
|
||||
assertEquals("abc", info.getParamInfo().get("api_key"));
|
||||
assertEquals(true, info.getParamInfo().get("42"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionInfoFallsBackToEmptyMapForBlankParamInfo() {
|
||||
AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo();
|
||||
|
||||
info.setParamInfo(" ");
|
||||
|
||||
assertTrue(info.getParamInfo().isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.Enums.AgentSnapshotField;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dao.AgentSnapshotDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTagDao;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotTagDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
|
||||
class AgentSnapshotServiceImplTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void normalizeSortedJsonListTreatsDtoAndMapShapesEqually() throws Exception {
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("normalizeSortedJsonList", List.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
ContextProviderDTO dto = new ContextProviderDTO();
|
||||
dto.setUrl("https://example.com/context");
|
||||
dto.setHeaders(Map.of("Authorization", "Bearer token"));
|
||||
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("headers", Map.of("Authorization", "Bearer token"));
|
||||
map.put("url", "https://example.com/context");
|
||||
|
||||
assertEquals(method.invoke(service, List.of(dto)), method.invoke(service, List.of(map)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void redactSnapshotDataMasksSensitiveFunctionAndHeaderValues() throws Exception {
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("redactSnapshotData",
|
||||
AgentSnapshotDataDTO.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
AgentSnapshotDataDTO data = new AgentSnapshotDataDTO();
|
||||
AgentUpdateDTO.FunctionInfo function = new AgentUpdateDTO.FunctionInfo();
|
||||
function.setPluginId("rag");
|
||||
function.setParamInfo(Map.of("api_key", "secret-key", "max_tokens", 32));
|
||||
ContextProviderDTO provider = new ContextProviderDTO();
|
||||
provider.setUrl("https://example.com/context");
|
||||
provider.setHeaders(Map.of("Authorization", "Bearer secret-token", "Accept", "application/json"));
|
||||
data.setFunctions(List.of(function));
|
||||
data.setContextProviders(List.of(provider));
|
||||
|
||||
AgentSnapshotDataDTO redacted = (AgentSnapshotDataDTO) method.invoke(service, data);
|
||||
String json = JsonUtils.toJsonString(redacted);
|
||||
|
||||
assertFalse(json.contains("secret-key"));
|
||||
assertFalse(json.contains("secret-token"));
|
||||
assertTrue(json.contains("__SNAPSHOT_SECRET_REDACTED__"));
|
||||
assertTrue(json.contains("max_tokens"));
|
||||
assertTrue(json.contains("application/json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preserveCurrentSensitiveValuesKeepsCurrentSecretsWhenRestoringSnapshot() throws Exception {
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("preserveCurrentSensitiveValues",
|
||||
AgentSnapshotDataDTO.class, AgentSnapshotDataDTO.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
AgentSnapshotDataDTO snapshot = buildSnapshot("old-key", "old-token", "old-value");
|
||||
AgentSnapshotDataDTO current = buildSnapshot("current-key", "current-token", "current-value");
|
||||
|
||||
AgentSnapshotDataDTO restored = (AgentSnapshotDataDTO) method.invoke(service, snapshot, current);
|
||||
|
||||
assertEquals("current-key", restored.getFunctions().get(0).getParamInfo().get("api_key"));
|
||||
assertEquals("old-value", restored.getFunctions().get(0).getParamInfo().get("description"));
|
||||
assertEquals("current-token", restored.getContextProviders().get(0).getHeaders().get("Authorization"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void getChangedFieldsFollowsCentralFieldOrder() throws Exception {
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("getChangedFields",
|
||||
AgentSnapshotDataDTO.class, AgentUpdateDTO.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
AgentSnapshotDataDTO current = new AgentSnapshotDataDTO();
|
||||
current.setAgentName("old-name");
|
||||
current.setCorrectWordFileIds(List.of("b", "a"));
|
||||
|
||||
AgentUpdateDTO pendingUpdate = new AgentUpdateDTO();
|
||||
pendingUpdate.setAgentName("new-name");
|
||||
pendingUpdate.setCorrectWordFileIds(List.of("a", "b"));
|
||||
AgentUpdateDTO.FunctionInfo function = new AgentUpdateDTO.FunctionInfo();
|
||||
function.setPluginId("rag");
|
||||
function.setParamInfo(Map.of("api_key", "secret-key"));
|
||||
pendingUpdate.setFunctions(List.of(function));
|
||||
|
||||
List<String> changedFields = (List<String>) method.invoke(service, current, pendingUpdate);
|
||||
|
||||
assertEquals(List.of(AgentSnapshotField.AGENT_NAME.getFieldName(),
|
||||
AgentSnapshotField.FUNCTIONS.getFieldName()), changedFields);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void tagIdsAreNotExposedAsIndependentSnapshotFields() throws Exception {
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("getChangedFields",
|
||||
AgentSnapshotDataDTO.class, AgentUpdateDTO.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
AgentSnapshotDataDTO current = new AgentSnapshotDataDTO();
|
||||
current.setTagNames(List.of("alpha", "beta"));
|
||||
|
||||
AgentUpdateDTO pendingUpdate = new AgentUpdateDTO();
|
||||
pendingUpdate.setTagNames(List.of("beta", "alpha"));
|
||||
pendingUpdate.setTagIds(List.of("new-id"));
|
||||
|
||||
List<String> changedFields = (List<String>) method.invoke(service, current, pendingUpdate);
|
||||
|
||||
assertFalse(AgentSnapshotField.names().contains("tagIds"));
|
||||
assertFalse(changedFields.contains("tagIds"));
|
||||
assertTrue(changedFields.isEmpty());
|
||||
|
||||
pendingUpdate.setTagNames(List.of("alpha", "gamma"));
|
||||
changedFields = (List<String>) method.invoke(service, current, pendingUpdate);
|
||||
|
||||
assertEquals(List.of(AgentSnapshotField.TAG_NAMES.getFieldName()), changedFields);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tagOnlySavePathCreatesSnapshot() throws Exception {
|
||||
String controller = Files.readString(Path.of("src/main/java/xiaozhi/modules/agent/controller/AgentController.java"));
|
||||
String roleConfig = Files.readString(Path.of("../manager-web/src/views/roleConfig.vue"));
|
||||
|
||||
assertTrue(controller.contains("agentService.updateAgentById(id, dto);"));
|
||||
assertFalse(controller.contains("agentService.updateAgentById(id, dto, false);"));
|
||||
assertTrue(roleConfig.contains("configData.tagNames = tagNames;"));
|
||||
assertFalse(roleConfig.contains("this.handleSaveAgentTags(agentId, tagNames)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstAgentSaveKeepsInitialBaselineBeforePersistingNewState() {
|
||||
AgentDao agentDao = mock(AgentDao.class);
|
||||
AgentContextProviderService contextProviderService = mock(AgentContextProviderService.class);
|
||||
CorrectWordFileService correctWordFileService = mock(CorrectWordFileService.class);
|
||||
AgentSnapshotService snapshotService = mock(AgentSnapshotService.class);
|
||||
AgentServiceImpl service = new AgentServiceImpl(agentDao, null, null, null, null, null, null, null,
|
||||
null, null, contextProviderService, null, correctWordFileService, snapshotService);
|
||||
ReflectionTestUtils.setField(service, "baseDao", agentDao);
|
||||
|
||||
String agentId = "agent-id";
|
||||
AgentEntity lockedAgent = new AgentEntity();
|
||||
lockedAgent.setId(agentId);
|
||||
AgentInfoVO currentAgent = new AgentInfoVO();
|
||||
currentAgent.setId(agentId);
|
||||
currentAgent.setAgentName("old-name");
|
||||
AgentUpdateDTO update = new AgentUpdateDTO();
|
||||
update.setAgentName("new-name");
|
||||
|
||||
when(agentDao.selectByIdForUpdate(agentId)).thenReturn(lockedAgent);
|
||||
when(agentDao.selectAgentInfoById(agentId)).thenReturn(currentAgent);
|
||||
when(snapshotService.getCurrentVersionNo(agentId)).thenReturn(0);
|
||||
when(contextProviderService.getByAgentId(agentId)).thenReturn(null);
|
||||
when(correctWordFileService.getAgentCorrectWordFileIds(agentId)).thenReturn(List.of());
|
||||
when(agentDao.updateById(any())).thenReturn(1);
|
||||
|
||||
service.updateAgentById(agentId, update);
|
||||
|
||||
InOrder inOrder = inOrder(agentDao, snapshotService);
|
||||
inOrder.verify(snapshotService).createSnapshot(agentId, "initial");
|
||||
inOrder.verify(agentDao).updateById(argThat(agent -> "new-name".equals(agent.getAgentName())));
|
||||
inOrder.verify(snapshotService).createSnapshot(agentId, "config");
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentSnapshotFieldAppliesRestorableAgentFields() {
|
||||
AgentSnapshotDataDTO data = new AgentSnapshotDataDTO();
|
||||
data.setAgentName("restored-name");
|
||||
data.setLlmModelId("llm-new");
|
||||
data.setSystemPrompt("restored prompt");
|
||||
data.setSort(7);
|
||||
|
||||
AgentEntity agent = new AgentEntity();
|
||||
for (AgentSnapshotField field : AgentSnapshotField.values()) {
|
||||
field.applyTo(agent, data);
|
||||
}
|
||||
|
||||
assertEquals("restored-name", agent.getAgentName());
|
||||
assertEquals("llm-new", agent.getLlmModelId());
|
||||
assertEquals("restored prompt", agent.getSystemPrompt());
|
||||
assertEquals(7, agent.getSort());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreSnapshotRollsBackForAnyException() throws Exception {
|
||||
Method method = AgentSnapshotServiceImpl.class.getMethod("restoreSnapshot", String.class, String.class);
|
||||
|
||||
Transactional transactional = method.getAnnotation(Transactional.class);
|
||||
|
||||
assertNotNull(transactional);
|
||||
assertTrue(List.of(transactional.rollbackFor()).contains(Exception.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteSnapshotRollsBackForAnyException() throws Exception {
|
||||
Method method = AgentSnapshotServiceImpl.class.getMethod("deleteSnapshot", String.class, String.class);
|
||||
|
||||
Transactional transactional = method.getAnnotation(Transactional.class);
|
||||
|
||||
assertNotNull(transactional);
|
||||
assertTrue(List.of(transactional.rollbackFor()).contains(Exception.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void snapshotInsertAllocatesVersionInSingleSqlStatement() throws Exception {
|
||||
String xml = normalizeWhitespace(Files.readString(Path.of("src/main/resources/mapper/agent/AgentSnapshotDao.xml")));
|
||||
String dao = Files.readString(Path.of("src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java"));
|
||||
|
||||
assertTrue(dao.contains("insertWithNextVersion"));
|
||||
assertTrue(xml.contains("<insert id=\"insertWithNextVersion\">"));
|
||||
assertTrue(xml.contains("INSERT INTO ai_agent_snapshot"));
|
||||
assertTrue(xml.contains("COALESCE(MAX(version_no), 0) + 1"));
|
||||
assertFalse(xml.contains("LAST_INSERT_ID"));
|
||||
assertFalse(xml.contains("ai_agent_snapshot_sequence"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void snapshotMigrationIsMergedIntoSingleChangeLog() throws Exception {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/changelog/202607071530.sql"));
|
||||
String master = Files.readString(Path.of("src/main/resources/db/changelog/db.changelog-master.yaml"));
|
||||
|
||||
assertTrue(sql.contains("restore_from_snapshot_id"));
|
||||
assertTrue(sql.contains("restore_from_version_no"));
|
||||
assertTrue(sql.contains("idx_snapshot_user_created_at"));
|
||||
assertTrue(sql.contains("DEFAULT (JSON_OBJECT())"));
|
||||
assertTrue(master.contains("202607071530.sql"));
|
||||
assertFalse(master.contains("202607081150.sql"));
|
||||
assertFalse(master.contains("202607081230.sql"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectNextSnapshotLoadsRestoreTraceColumns() throws Exception {
|
||||
String xml = normalizeWhitespace(Files.readString(Path.of("src/main/resources/mapper/agent/AgentSnapshotDao.xml")));
|
||||
|
||||
assertTrue(xml.contains("restore_from_snapshot_id AS restoreFromSnapshotId"));
|
||||
assertTrue(xml.contains("restore_from_version_no AS restoreFromVersionNo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retentionPruneSqlKeepsLatestVersions() throws Exception {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/agent/AgentSnapshotDao.xml"));
|
||||
|
||||
assertTrue(xml.contains("deleteOlderThanKeepLimit"));
|
||||
assertTrue(xml.contains("ORDER BY version_no DESC"));
|
||||
assertTrue(xml.contains("LIMIT #{keepLimit}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentInfoMapperLoadsFunctionsWithJoinInsteadOfNestedSelect() throws Exception {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/agent/AgentDao.xml"));
|
||||
|
||||
assertTrue(xml.contains("LEFT JOIN ai_agent_plugin_mapping f ON f.agent_id = a.id"));
|
||||
assertFalse(xml.contains("selectAgentFunctionsByAgentId"));
|
||||
assertFalse(xml.contains("select=\"selectAgentFunctions"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toVOIncludesRestoreTraceFields() throws Exception {
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("toVO",
|
||||
AgentSnapshotEntity.class, boolean.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
AgentSnapshotEntity entity = new AgentSnapshotEntity();
|
||||
entity.setId("snapshot-id");
|
||||
entity.setAgentId("agent-id");
|
||||
entity.setVersionNo(3);
|
||||
entity.setChangedFields("[]");
|
||||
entity.setSource("restore");
|
||||
entity.setRestoreFromSnapshotId("target-id");
|
||||
entity.setRestoreFromVersionNo(1);
|
||||
|
||||
AgentSnapshotVO vo = (AgentSnapshotVO) method.invoke(service, entity, false);
|
||||
|
||||
assertEquals("target-id", vo.getRestoreFromSnapshotId());
|
||||
assertEquals(1, vo.getRestoreFromVersionNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageDoesNotCreateInitialSnapshotWhenAgentHasNoHistory() {
|
||||
AgentSnapshotDao snapshotDao = mock(AgentSnapshotDao.class);
|
||||
AgentDao agentDao = mock(AgentDao.class);
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(snapshotDao, agentDao, null, null, null,
|
||||
null, null, null, null, null);
|
||||
|
||||
when(snapshotDao.selectPage(any(), any())).thenReturn(new Page<AgentSnapshotEntity>(1, 10));
|
||||
|
||||
service.page("agent-id", new AgentSnapshotPageDTO());
|
||||
|
||||
verify(agentDao, never()).selectByIdForUpdate(any());
|
||||
verify(snapshotDao, never()).selectMaxVersionNo(any());
|
||||
verify(snapshotDao, never()).insertWithNextVersion(any());
|
||||
verify(snapshotDao, never()).deleteOlderThanKeepLimit(any(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCurrentVersionNoReturnsPersistedMaxVersion() {
|
||||
AgentSnapshotDao snapshotDao = mock(AgentSnapshotDao.class);
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(snapshotDao, null, null, null, null,
|
||||
null, null, null, null, null);
|
||||
|
||||
when(snapshotDao.selectMaxVersionNo("agent-id")).thenReturn(7);
|
||||
|
||||
assertEquals(7, service.getCurrentVersionNo("agent-id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSnapshotStoresCurrentStateAsInitialVersionWhenHistoryIsEmpty() {
|
||||
AgentSnapshotDao snapshotDao = mock(AgentSnapshotDao.class);
|
||||
AgentDao agentDao = mock(AgentDao.class);
|
||||
AgentTagDao tagDao = mock(AgentTagDao.class);
|
||||
AgentContextProviderService contextProviderService = mock(AgentContextProviderService.class);
|
||||
CorrectWordFileService correctWordFileService = mock(CorrectWordFileService.class);
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(snapshotDao, agentDao, tagDao, null, null,
|
||||
contextProviderService, null, null, null, correctWordFileService);
|
||||
|
||||
AgentEntity lockedAgent = new AgentEntity();
|
||||
lockedAgent.setId("agent-id");
|
||||
AgentInfoVO agentInfo = new AgentInfoVO();
|
||||
agentInfo.setId("agent-id");
|
||||
agentInfo.setUserId(7L);
|
||||
agentInfo.setAgentName("agent");
|
||||
when(agentDao.selectByIdForUpdate("agent-id")).thenReturn(lockedAgent);
|
||||
when(agentDao.selectAgentInfoById("agent-id")).thenReturn(agentInfo);
|
||||
when(snapshotDao.selectLatestSnapshot("agent-id")).thenReturn(null);
|
||||
when(snapshotDao.insertWithNextVersion(any())).thenReturn(1);
|
||||
when(correctWordFileService.getAgentCorrectWordFileIds("agent-id")).thenReturn(List.of());
|
||||
when(contextProviderService.getByAgentId("agent-id")).thenReturn(null);
|
||||
when(tagDao.selectByAgentId("agent-id")).thenReturn(List.of());
|
||||
|
||||
service.createSnapshot("agent-id", "config");
|
||||
|
||||
verify(snapshotDao).insertWithNextVersion(argThat(snapshot -> "config".equals(snapshot.getSource())
|
||||
&& snapshot.getVersionNo() == null
|
||||
&& snapshot.getChangedFields().contains("initial")));
|
||||
verify(snapshotDao).deleteOlderThanKeepLimit("agent-id", 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreTagDoesNotReviveSoftDeletedSnapshotTagId() throws Exception {
|
||||
AgentTagDao tagDao = mock(AgentTagDao.class);
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, tagDao, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("restoreTag",
|
||||
AgentSnapshotTagDTO.class, Date.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
AgentTagEntity deletedTag = new AgentTagEntity();
|
||||
deletedTag.setId("deleted-tag-id");
|
||||
deletedTag.setTagName("archived");
|
||||
deletedTag.setDeleted(1);
|
||||
when(tagDao.selectById("deleted-tag-id")).thenReturn(deletedTag);
|
||||
when(tagDao.selectOne(any())).thenReturn(null);
|
||||
AtomicReference<AgentTagEntity> insertedTag = new AtomicReference<>();
|
||||
when(tagDao.insert(any())).thenAnswer(invocation -> {
|
||||
insertedTag.set(invocation.getArgument(0));
|
||||
return 1;
|
||||
});
|
||||
|
||||
AgentSnapshotTagDTO snapshotTag = new AgentSnapshotTagDTO();
|
||||
snapshotTag.setId("deleted-tag-id");
|
||||
snapshotTag.setTagName("archived");
|
||||
|
||||
String restoredTagId = (String) method.invoke(service, snapshotTag, new Date());
|
||||
|
||||
verify(tagDao, never()).updateById(any());
|
||||
assertNotEquals("deleted-tag-id", restoredTagId);
|
||||
assertNotNull(insertedTag.get());
|
||||
assertEquals(restoredTagId, insertedTag.get().getId());
|
||||
assertEquals(0, insertedTag.get().getDeleted());
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO buildSnapshot(String apiKey, String authToken, String description) {
|
||||
AgentSnapshotDataDTO data = new AgentSnapshotDataDTO();
|
||||
|
||||
AgentUpdateDTO.FunctionInfo function = new AgentUpdateDTO.FunctionInfo();
|
||||
function.setPluginId("rag");
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
params.put("api_key", apiKey);
|
||||
params.put("description", description);
|
||||
function.setParamInfo(params);
|
||||
|
||||
ContextProviderDTO provider = new ContextProviderDTO();
|
||||
provider.setUrl("https://example.com/context");
|
||||
provider.setHeaders(Map.of("Authorization", authToken));
|
||||
|
||||
data.setFunctions(List.of(function));
|
||||
data.setContextProviders(List.of(provider));
|
||||
return data;
|
||||
}
|
||||
|
||||
private String normalizeWhitespace(String value) {
|
||||
return value.replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package xiaozhi.modules.agent.typehandler;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.type.TypeHandler;
|
||||
import org.apache.ibatis.type.TypeHandlerRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
|
||||
class ContextProviderListTypeHandlerTest {
|
||||
|
||||
private final ContextProviderListTypeHandler handler = new ContextProviderListTypeHandler();
|
||||
|
||||
@Test
|
||||
void parseKeepsContextProviderDtoElementType() {
|
||||
List<ContextProviderDTO> providers = handler
|
||||
.parse("[{\"url\":\"https://example.com/context\",\"headers\":{\"Authorization\":\"Bearer token\"}}]");
|
||||
|
||||
assertEquals(1, providers.size());
|
||||
assertInstanceOf(ContextProviderDTO.class, providers.get(0));
|
||||
assertEquals("https://example.com/context", providers.get(0).getUrl());
|
||||
assertEquals("Bearer token", providers.get(0).getHeaders().get("Authorization"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBlankJsonAsEmptyList() {
|
||||
assertTrue(handler.parse(" ").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void myBatisCanInstantiateHandlerForListField() {
|
||||
TypeHandler<?> typeHandler = new TypeHandlerRegistry().getInstance(List.class,
|
||||
ContextProviderListTypeHandler.class);
|
||||
|
||||
assertInstanceOf(ContextProviderListTypeHandler.class, typeHandler);
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,9 @@ importers:
|
||||
pinia-plugin-persistedstate:
|
||||
specifier: 3.2.1
|
||||
version: 3.2.1(pinia@2.0.36(typescript@5.7.2)(vue@3.5.15(typescript@5.7.2)))
|
||||
sm-crypto:
|
||||
specifier: ^0.3.13
|
||||
version: 0.3.14
|
||||
vue:
|
||||
specifier: ^3.4.21
|
||||
version: 3.5.15(typescript@5.7.2)
|
||||
@@ -4719,6 +4722,9 @@ packages:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
|
||||
jsbn@1.1.0:
|
||||
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
|
||||
|
||||
jsdoc-type-pratt-parser@4.1.0:
|
||||
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -6020,6 +6026,9 @@ packages:
|
||||
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
sm-crypto@0.3.14:
|
||||
resolution: {integrity: sha512-sR7NuGAJH93Om8keAF2/rj1EqFzEiUey4aKGyDo3nQ2BiVqhgq8UkiIoIp5qhV9w4pG7qArhfqyewcG8AbhXbg==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -12652,6 +12661,8 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsbn@1.1.0: {}
|
||||
|
||||
jsdoc-type-pratt-parser@4.1.0: {}
|
||||
|
||||
jsdom@16.7.0:
|
||||
@@ -14145,6 +14156,10 @@ snapshots:
|
||||
ansi-styles: 6.2.1
|
||||
is-fullwidth-code-point: 5.0.0
|
||||
|
||||
sm-crypto@0.3.14:
|
||||
dependencies:
|
||||
jsbn: 1.1.0
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
|
||||
@@ -71,6 +71,13 @@ export default {
|
||||
'home.inputPlaceholder': 'z.B. Kundenservice-Assistent, Sprachassistent, Wissens-F&A',
|
||||
'home.createError': 'Der Name muss zwischen 1 und 64 Zeichen lang sein.',
|
||||
'home.createNow': 'Jetzt erstellen',
|
||||
'home.deleteConfirmTitle': 'Agent löschen',
|
||||
'home.confirmDeleteAgent': 'Das Löschen kann nicht rückgängig gemacht werden. Geben Sie den Agentennamen "{agentName}" ein, um zu bestätigen.',
|
||||
'home.deleteAgentNamePlaceholder': 'Agentennamen eingeben',
|
||||
'home.deleteAgentNameMismatch': 'Der Agentenname stimmt nicht überein',
|
||||
'home.deleteAgentPasteForbidden': 'Bitte geben Sie den Agentennamen manuell ein. Einfügen ist nicht erlaubt.',
|
||||
'home.deleteAgentMissingInfo': 'Agenteninformationen konnten nicht bestätigt werden. Bitte aktualisieren und erneut versuchen.',
|
||||
'home.deleteAgentSuccess': 'Agent erfolgreich gelöscht',
|
||||
'home.justNow': 'Gerade eben',
|
||||
'home.minutesAgo': 'Minuten her',
|
||||
'home.hoursAgo': 'Stunden her',
|
||||
|
||||
@@ -71,6 +71,13 @@ export default {
|
||||
'home.inputPlaceholder': 'e.g. Customer Service Assistant, Voice Assistant, Knowledge Q&A',
|
||||
'home.createError': 'The name length must be between 1 and 64 characters',
|
||||
'home.createNow': 'Create Now',
|
||||
'home.deleteConfirmTitle': 'Delete Agent',
|
||||
'home.confirmDeleteAgent': 'Deletion cannot be undone. Type the agent name "{agentName}" to confirm.',
|
||||
'home.deleteAgentNamePlaceholder': 'Enter agent name',
|
||||
'home.deleteAgentNameMismatch': 'The agent name does not match',
|
||||
'home.deleteAgentPasteForbidden': 'Please type the agent name manually. Pasting is not allowed.',
|
||||
'home.deleteAgentMissingInfo': 'Unable to confirm agent information. Please refresh and try again.',
|
||||
'home.deleteAgentSuccess': 'Agent deleted successfully',
|
||||
'home.justNow': 'Just now',
|
||||
'home.minutesAgo': 'minutes ago',
|
||||
'home.hoursAgo': 'hours ago',
|
||||
|
||||
@@ -71,6 +71,13 @@ export default {
|
||||
'home.inputPlaceholder': 'ex: Assistente de Atendimento, Assistente de Voz, Perguntas e Respostas',
|
||||
'home.createError': 'O comprimento do nome deve estar entre 1 e 64 caracteres',
|
||||
'home.createNow': 'Criar Agora',
|
||||
'home.deleteConfirmTitle': 'Excluir Agente',
|
||||
'home.confirmDeleteAgent': 'A exclusão não pode ser desfeita. Digite o nome do agente "{agentName}" para confirmar.',
|
||||
'home.deleteAgentNamePlaceholder': 'Digite o nome do agente',
|
||||
'home.deleteAgentNameMismatch': 'O nome do agente não corresponde',
|
||||
'home.deleteAgentPasteForbidden': 'Digite o nome do agente manualmente. Colar não é permitido.',
|
||||
'home.deleteAgentMissingInfo': 'Não foi possível confirmar as informações do agente. Atualize a página e tente novamente.',
|
||||
'home.deleteAgentSuccess': 'Agente excluído com sucesso',
|
||||
'home.justNow': 'Agora mesmo',
|
||||
'home.minutesAgo': 'minutos atrás',
|
||||
'home.hoursAgo': 'horas atrás',
|
||||
|
||||
@@ -71,6 +71,13 @@ export default {
|
||||
'home.inputPlaceholder': 'ví dụ: Trợ lý chăm sóc khách hàng, Trợ lý giọng nói, Hỏi đáp kiến thức',
|
||||
'home.createError': 'Độ dài tên phải từ 1 đến 64 ký tự',
|
||||
'home.createNow': 'Tạo ngay',
|
||||
'home.deleteConfirmTitle': 'Xóa đại lý',
|
||||
'home.confirmDeleteAgent': 'Không thể hoàn tác sau khi xóa. Nhập tên đại lý "{agentName}" để xác nhận.',
|
||||
'home.deleteAgentNamePlaceholder': 'Nhập tên đại lý',
|
||||
'home.deleteAgentNameMismatch': 'Tên đại lý không khớp',
|
||||
'home.deleteAgentPasteForbidden': 'Vui lòng nhập thủ công tên đại lý. Không được dán.',
|
||||
'home.deleteAgentMissingInfo': 'Không thể xác nhận thông tin đại lý. Vui lòng tải lại rồi thử lại.',
|
||||
'home.deleteAgentSuccess': 'Đã xóa đại lý thành công',
|
||||
'home.justNow': 'Vừa xong',
|
||||
'home.minutesAgo': 'phút trước',
|
||||
'home.hoursAgo': 'giờ trước',
|
||||
|
||||
@@ -71,6 +71,13 @@ export default {
|
||||
'home.inputPlaceholder': '例如:客服助手、语音助理、知识问答',
|
||||
'home.createError': '名称长度必须在 1 到 64 个字符之间',
|
||||
'home.createNow': '立即创建',
|
||||
'home.deleteConfirmTitle': '删除智能体',
|
||||
'home.confirmDeleteAgent': '删除后无法恢复。请输入智能体名称“{agentName}”确认删除。',
|
||||
'home.deleteAgentNamePlaceholder': '请输入智能体名称',
|
||||
'home.deleteAgentNameMismatch': '输入的智能体名称不匹配',
|
||||
'home.deleteAgentPasteForbidden': '请手动输入智能体名称,不允许粘贴',
|
||||
'home.deleteAgentMissingInfo': '无法确认智能体信息,请刷新后重试',
|
||||
'home.deleteAgentSuccess': '智能体删除成功',
|
||||
'home.justNow': '刚刚',
|
||||
'home.minutesAgo': '分钟前',
|
||||
'home.hoursAgo': '小时前',
|
||||
|
||||
@@ -92,6 +92,13 @@ export default {
|
||||
'home.inputPlaceholder': '例如:客服助手、語音助理、知識問答',
|
||||
'home.createError': '暱稱長度必須在 1 到 64 個字元之間。',
|
||||
'home.createNow': '立即創建',
|
||||
'home.deleteConfirmTitle': '刪除智能體',
|
||||
'home.confirmDeleteAgent': '刪除後無法復原。請輸入智能體名稱「{agentName}」確認刪除。',
|
||||
'home.deleteAgentNamePlaceholder': '請輸入智能體名稱',
|
||||
'home.deleteAgentNameMismatch': '輸入的智能體名稱不匹配',
|
||||
'home.deleteAgentPasteForbidden': '請手動輸入智能體名稱,不允許貼上',
|
||||
'home.deleteAgentMissingInfo': '無法確認智能體資訊,請重新整理後再試',
|
||||
'home.deleteAgentSuccess': '智能體刪除成功',
|
||||
'home.justNow': '剛剛',
|
||||
'home.minutesAgo': '分鐘前',
|
||||
'home.hoursAgo': '小時前',
|
||||
|
||||
@@ -169,6 +169,28 @@ function handleInputConfirm() {
|
||||
// 是否禁用历史记忆输入框
|
||||
const isMemoryDisabled = computed(() => formData.value.memModelId !== 'Memory_mem_local_short')
|
||||
|
||||
function normalizeFunctionParams(paramInfo: any) {
|
||||
if (!paramInfo)
|
||||
return {}
|
||||
if (typeof paramInfo === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(paramInfo)
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
}
|
||||
catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return typeof paramInfo === 'object' && !Array.isArray(paramInfo) ? { ...paramInfo } : {}
|
||||
}
|
||||
|
||||
function normalizeAgentFunctions(functions: any[] = []) {
|
||||
return functions.map(item => ({
|
||||
...item,
|
||||
paramInfo: normalizeFunctionParams(item.paramInfo),
|
||||
}))
|
||||
}
|
||||
|
||||
// 打开上下文源编辑弹窗
|
||||
function openContextProviderDialog() {
|
||||
uni.navigateTo({
|
||||
@@ -191,11 +213,12 @@ async function loadAgentDetail() {
|
||||
loading.value = true
|
||||
tempSummaryMemory.value = ''
|
||||
const detail = await getAgentDetail(agentId.value)
|
||||
formData.value = { ...detail }
|
||||
const normalizedFunctions = normalizeAgentFunctions(detail.functions || [])
|
||||
formData.value = { ...detail, functions: normalizedFunctions }
|
||||
|
||||
// 更新插件store
|
||||
pluginStore.setCurrentAgentId(agentId.value)
|
||||
pluginStore.setCurrentFunctions(detail.functions || [])
|
||||
pluginStore.setCurrentFunctions(normalizedFunctions)
|
||||
|
||||
// 更新语速音调
|
||||
speedPitchStore.updateSpeedPitch({
|
||||
@@ -626,6 +649,7 @@ async function saveAgent() {
|
||||
...speedPitchStore.speedPitch,
|
||||
ttsLanguage: formData.value.language,
|
||||
contextProviders: providerStore.providers,
|
||||
functions: normalizeAgentFunctions(formData.value.functions || []),
|
||||
}
|
||||
await updateAgent(agentId.value, saveData)
|
||||
loadAgentDetail()
|
||||
@@ -663,7 +687,7 @@ function handleTools() {
|
||||
|
||||
// 确保store中有最新数据
|
||||
pluginStore.setCurrentAgentId(agentId.value)
|
||||
pluginStore.setCurrentFunctions(formData.value.functions || [])
|
||||
pluginStore.setCurrentFunctions(normalizeAgentFunctions(formData.value.functions || []))
|
||||
pluginStore.setAllFunctions(allFunctions.value)
|
||||
|
||||
uni.navigateTo({
|
||||
@@ -688,7 +712,7 @@ async function handleUpdateAgentTags() {
|
||||
|
||||
// 监听store中的插件配置变化
|
||||
watch(() => pluginStore.currentFunctions, (newFunctions) => {
|
||||
formData.value.functions = newFunctions
|
||||
formData.value.functions = normalizeAgentFunctions(newFunctions || [])
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -41,17 +41,32 @@ const tempParams = ref<Record<string, any>>({})
|
||||
const arrayTextCache = ref<Record<string, string>>({})
|
||||
const jsonTextCache = ref<Record<string, string>>({})
|
||||
|
||||
function normalizeFunctionParams(paramInfo: any, fallback: Record<string, any> = {}) {
|
||||
if (!paramInfo)
|
||||
return { ...fallback }
|
||||
if (typeof paramInfo === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(paramInfo)
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : { ...fallback }
|
||||
}
|
||||
catch {
|
||||
return { ...fallback }
|
||||
}
|
||||
}
|
||||
return typeof paramInfo === 'object' && !Array.isArray(paramInfo) ? { ...fallback, ...paramInfo } : { ...fallback }
|
||||
}
|
||||
|
||||
async function mergeFunctions() {
|
||||
selectedList.value = functions.value.map((mapping) => {
|
||||
const meta = allFunctions.value.find(f => f.id === mapping.pluginId)
|
||||
if (!meta) {
|
||||
return { id: mapping.pluginId, name: mapping.pluginId, params: {} }
|
||||
return { id: mapping.pluginId, name: mapping.pluginId, params: normalizeFunctionParams(mapping.paramInfo) }
|
||||
}
|
||||
|
||||
return {
|
||||
id: mapping.pluginId,
|
||||
name: meta.name,
|
||||
params: mapping.paramInfo || { ...meta.params },
|
||||
params: normalizeFunctionParams(mapping.paramInfo, meta.params),
|
||||
fieldsMeta: meta.fieldsMeta,
|
||||
}
|
||||
})
|
||||
@@ -91,7 +106,7 @@ function selectFunction(func: any) {
|
||||
selectedList.value = [...selectedList.value, {
|
||||
id: func.id,
|
||||
name: func.name,
|
||||
params: { ...func.params },
|
||||
params: normalizeFunctionParams(func.params),
|
||||
fieldsMeta: func.fieldsMeta,
|
||||
}]
|
||||
|
||||
@@ -118,7 +133,7 @@ function editFunction(func: any) {
|
||||
currentFunction.value = func
|
||||
|
||||
// 直接使用当前函数的参数
|
||||
tempParams.value = { ...func.params }
|
||||
tempParams.value = normalizeFunctionParams(func.params)
|
||||
|
||||
// 初始化文本缓存
|
||||
if (func.fieldsMeta) {
|
||||
@@ -252,7 +267,7 @@ function getFieldRemark(field: any) {
|
||||
watch(() => selectedList.value, (newSelectedList) => {
|
||||
const finalFunctions = newSelectedList.map(f => ({
|
||||
pluginId: f.id,
|
||||
paramInfo: f.params,
|
||||
paramInfo: normalizeFunctionParams(f.params),
|
||||
}))
|
||||
pluginStore.updateFunctions(finalFunctions)
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Agent } from '@/api/agent/types'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
// 在组件挂载后设置导航栏标题
|
||||
import { useMessage } from 'wot-design-uni'
|
||||
import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging.js'
|
||||
@@ -53,6 +53,14 @@ const pagingRef = ref()
|
||||
useZPaging(pagingRef)
|
||||
// 消息组件
|
||||
const message = useMessage()
|
||||
const deleteDialogVisible = ref(false)
|
||||
const deleteTargetAgent = ref<Agent | null>(null)
|
||||
const deleteConfirmText = ref('')
|
||||
const deleteAgentLoading = ref(false)
|
||||
|
||||
const isDeleteAgentNameMatched = computed(() => {
|
||||
return !!deleteTargetAgent.value?.agentName && deleteConfirmText.value === deleteTargetAgent.value.agentName
|
||||
})
|
||||
|
||||
// z-paging查询列表数据
|
||||
async function queryList(pageNo: number, pageSize: number) {
|
||||
@@ -89,19 +97,53 @@ async function handleCreateAgent(agentName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteAgentDialog(agent: Agent) {
|
||||
if (!agent.id || !agent.agentName) {
|
||||
toast.error(t('home.deleteAgentMissingInfo'))
|
||||
return
|
||||
}
|
||||
|
||||
deleteTargetAgent.value = agent
|
||||
deleteConfirmText.value = ''
|
||||
deleteDialogVisible.value = true
|
||||
}
|
||||
|
||||
function closeDeleteAgentDialog() {
|
||||
if (deleteAgentLoading.value)
|
||||
return
|
||||
|
||||
deleteDialogVisible.value = false
|
||||
deleteTargetAgent.value = null
|
||||
deleteConfirmText.value = ''
|
||||
}
|
||||
|
||||
function handleDeleteAgentPaste() {
|
||||
toast.warning(t('home.deleteAgentPasteForbidden'))
|
||||
}
|
||||
|
||||
// 删除智能体
|
||||
async function handleDeleteAgent(agent: Agent) {
|
||||
async function handleDeleteAgent() {
|
||||
if (!deleteTargetAgent.value || !isDeleteAgentNameMatched.value || deleteAgentLoading.value)
|
||||
return
|
||||
|
||||
try {
|
||||
await deleteAgent(agent.id)
|
||||
deleteAgentLoading.value = true
|
||||
await deleteAgent(deleteTargetAgent.value.id)
|
||||
// 删除成功后刷新列表
|
||||
pagingRef.value.reload()
|
||||
toast.success(`${t('home.agentName')}${t('message.deleteSuccess')}`)
|
||||
toast.success(t('home.deleteAgentSuccess'))
|
||||
deleteDialogVisible.value = false
|
||||
deleteTargetAgent.value = null
|
||||
deleteConfirmText.value = ''
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('删除智能体失败:', error)
|
||||
const errorMessage = error?.message || t('message.deleteFail')
|
||||
toast.error(errorMessage)
|
||||
}
|
||||
finally {
|
||||
deleteAgentLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 进入编辑页面
|
||||
@@ -254,7 +296,7 @@ onMounted(() => {
|
||||
|
||||
<template #right>
|
||||
<view class="swipe-actions">
|
||||
<view class="action-btn delete-btn" @click.stop="handleDeleteAgent(agent)">
|
||||
<view class="action-btn delete-btn" @click.stop="openDeleteAgentDialog(agent)">
|
||||
<wd-icon name="delete" />
|
||||
<text>{{ t('home.delete') }}</text>
|
||||
</view>
|
||||
@@ -283,6 +325,79 @@ onMounted(() => {
|
||||
<!-- MessageBox 组件 -->
|
||||
<wd-message-box />
|
||||
</z-paging>
|
||||
|
||||
<wd-popup
|
||||
v-model="deleteDialogVisible"
|
||||
position="center"
|
||||
custom-style="width: 88%; max-width: 420px; border-radius: 16px;"
|
||||
:close-on-click-modal="!deleteAgentLoading"
|
||||
safe-area-inset-bottom
|
||||
@close="closeDeleteAgentDialog"
|
||||
>
|
||||
<view class="delete-agent-dialog">
|
||||
<view class="delete-agent-header">
|
||||
<text class="delete-agent-title">
|
||||
{{ t('home.deleteConfirmTitle') }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="delete-agent-body">
|
||||
<view class="delete-agent-content">
|
||||
<wd-icon name="warning" custom-class="delete-agent-warning" />
|
||||
<view class="delete-agent-message">
|
||||
<text
|
||||
class="delete-agent-copy-guard delete-agent-tip"
|
||||
@copy.stop.prevent
|
||||
@cut.stop.prevent
|
||||
@contextmenu.stop.prevent
|
||||
@longpress.stop.prevent
|
||||
>
|
||||
{{ t('home.confirmDeleteAgent', { agentName: deleteTargetAgent?.agentName || '' }) }}
|
||||
</text>
|
||||
<view
|
||||
class="delete-agent-copy-guard delete-agent-target"
|
||||
@copy.stop.prevent
|
||||
@cut.stop.prevent
|
||||
@contextmenu.stop.prevent
|
||||
@longpress.stop.prevent
|
||||
>
|
||||
<text>{{ deleteTargetAgent?.agentName || '' }}</text>
|
||||
</view>
|
||||
|
||||
<input
|
||||
v-model="deleteConfirmText"
|
||||
class="delete-agent-input"
|
||||
type="text"
|
||||
:maxlength="deleteTargetAgent?.agentName?.length || 64"
|
||||
:placeholder="t('home.deleteAgentNamePlaceholder')"
|
||||
:disabled="deleteAgentLoading"
|
||||
@paste.stop.prevent="handleDeleteAgentPaste"
|
||||
@drop.stop.prevent="handleDeleteAgentPaste"
|
||||
@contextmenu.stop.prevent
|
||||
>
|
||||
<text v-if="deleteConfirmText && !isDeleteAgentNameMatched" class="delete-agent-helper">
|
||||
{{ t('home.deleteAgentNameMismatch') }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="delete-agent-footer">
|
||||
<wd-button type="info" custom-class="delete-agent-button" :disabled="deleteAgentLoading" @click="closeDeleteAgentDialog">
|
||||
{{ t('common.cancel') }}
|
||||
</wd-button>
|
||||
<wd-button
|
||||
type="primary"
|
||||
custom-class="delete-agent-button"
|
||||
:loading="deleteAgentLoading"
|
||||
:disabled="!isDeleteAgentNameMatched"
|
||||
@click="handleDeleteAgent"
|
||||
>
|
||||
{{ t('common.confirm') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -587,4 +702,103 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
border-top: 1rpx solid #eeeeee;
|
||||
}
|
||||
|
||||
.delete-agent-dialog {
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.delete-agent-header {
|
||||
padding: 32rpx 32rpx 20rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.delete-agent-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #232338;
|
||||
}
|
||||
|
||||
.delete-agent-body {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.delete-agent-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
:deep(.delete-agent-warning) {
|
||||
margin-top: 4rpx;
|
||||
color: #e6a23c;
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.delete-agent-message {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.delete-agent-tip {
|
||||
display: block;
|
||||
color: #606266;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.delete-agent-target {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin-top: 20rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
border: 1rpx solid #ebeef5;
|
||||
border-radius: 12rpx;
|
||||
background: #f5f7fa;
|
||||
color: #303133;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.delete-agent-copy-guard {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.delete-agent-input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
margin-top: 28rpx;
|
||||
padding: 0 20rpx;
|
||||
border: 1rpx solid #eeeeee;
|
||||
border-radius: 12rpx;
|
||||
background: #f5f7fb;
|
||||
color: #232338;
|
||||
font-size: 28rpx;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.delete-agent-helper {
|
||||
display: block;
|
||||
min-height: 32rpx;
|
||||
margin-top: 10rpx;
|
||||
color: #f56c6c;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.delete-agent-footer {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx 32rpx 32rpx;
|
||||
border-top: 1rpx solid #eeeeee;
|
||||
}
|
||||
|
||||
:deep(.delete-agent-button) {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -265,7 +265,7 @@ function showAbout() {
|
||||
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
|
||||
content: t('settings.aboutContent', {
|
||||
appName: import.meta.env.VITE_APP_TITLE,
|
||||
version: '0.9.4'
|
||||
version: '0.9.5'
|
||||
}),
|
||||
showCancel: false,
|
||||
confirmText: t('common.confirm'),
|
||||
|
||||
@@ -116,7 +116,7 @@ export default async ({ command, mode }) => {
|
||||
&& mode === 'production'
|
||||
&& visualizer({
|
||||
filename: './node_modules/.cache/visualizer/stats.html',
|
||||
open: true,
|
||||
open: false,
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
}),
|
||||
|
||||
@@ -11096,6 +11096,16 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"check:i18n": "node scripts/check-i18n.js",
|
||||
"build": "vue-cli-service build",
|
||||
"analyze": "cross-env ANALYZE=true vue-cli-service build"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const locales = [
|
||||
{ name: 'zh_CN', file: 'src/i18n/zh_CN.js' },
|
||||
{ name: 'zh_TW', file: 'src/i18n/zh_TW.js' },
|
||||
{ name: 'en', file: 'src/i18n/en.js' },
|
||||
{ name: 'de', file: 'src/i18n/de.js' },
|
||||
{ name: 'vi', file: 'src/i18n/vi.js' },
|
||||
{ name: 'pt_BR', file: 'src/i18n/pt_BR.js' },
|
||||
];
|
||||
|
||||
function readLocale(locale) {
|
||||
const filePath = path.join(rootDir, locale.file);
|
||||
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
||||
const keys = [];
|
||||
const seen = new Map();
|
||||
const duplicates = [];
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const match = line.match(/^\s*['"]([^'"]+)['"]\s*:/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = match[1];
|
||||
keys.push(key);
|
||||
if (!seen.has(key)) {
|
||||
seen.set(key, []);
|
||||
}
|
||||
seen.get(key).push(index + 1);
|
||||
});
|
||||
|
||||
seen.forEach((lineNumbers, key) => {
|
||||
if (lineNumbers.length > 1) {
|
||||
duplicates.push({ key, lineNumbers });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...locale,
|
||||
filePath,
|
||||
keys,
|
||||
keySet: new Set(keys),
|
||||
duplicates,
|
||||
};
|
||||
}
|
||||
|
||||
function diffKeys(left, right) {
|
||||
return [...left].filter((key) => !right.has(key)).sort();
|
||||
}
|
||||
|
||||
function formatKeyList(keys) {
|
||||
return keys.length ? keys.map((key) => ` - ${key}`).join('\n') : '';
|
||||
}
|
||||
|
||||
function main() {
|
||||
const results = locales.map(readLocale);
|
||||
const base = results.find((locale) => locale.name === 'zh_CN');
|
||||
const errors = [];
|
||||
|
||||
results.forEach((locale) => {
|
||||
locale.duplicates.forEach(({ key, lineNumbers }) => {
|
||||
errors.push(
|
||||
`${locale.file} has duplicate key "${key}" on lines ${lineNumbers.join(', ')}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
results
|
||||
.filter((locale) => locale !== base)
|
||||
.forEach((locale) => {
|
||||
const missing = diffKeys(base.keySet, locale.keySet);
|
||||
const extra = diffKeys(locale.keySet, base.keySet);
|
||||
|
||||
if (missing.length) {
|
||||
errors.push(`${locale.file} is missing keys from ${base.file}:\n${formatKeyList(missing)}`);
|
||||
}
|
||||
|
||||
if (extra.length) {
|
||||
errors.push(`${locale.file} has keys that do not exist in ${base.file}:\n${formatKeyList(extra)}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
console.error(`i18n check failed with ${errors.length} issue(s):`);
|
||||
console.error(errors.join('\n\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const keyCount = base.keySet.size;
|
||||
console.log(`i18n check passed: ${results.length} locale files, ${keyCount} keys each.`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
}).send()
|
||||
},
|
||||
// 保存
|
||||
addParam(data, callback) {
|
||||
addParam(data, callback, failCallback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
.method('POST')
|
||||
@@ -89,6 +89,10 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
RequestService.clearRequestTime()
|
||||
failCallback(err)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('添加参数失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
|
||||
@@ -81,6 +81,67 @@ export default {
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取智能体配置快照列表
|
||||
getAgentSnapshots(agentId, params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/snapshots`)
|
||||
.method('GET')
|
||||
.data(params)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentSnapshots(agentId, params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取智能体配置快照详情
|
||||
getAgentSnapshot(agentId, snapshotId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/snapshots/${snapshotId}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentSnapshot(agentId, snapshotId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 恢复智能体配置快照
|
||||
restoreAgentSnapshot(agentId, snapshotId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/snapshots/${snapshotId}/restore`)
|
||||
.method('POST')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.restoreAgentSnapshot(agentId, snapshotId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 删除智能体配置快照
|
||||
deleteAgentSnapshot(agentId, snapshotId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/snapshots/${snapshotId}`)
|
||||
.method('DELETE')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteAgentSnapshot(agentId, snapshotId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 新增方法:获取智能体模板
|
||||
getAgentTemplate(callback) { // 移除templateName参数
|
||||
RequestService.sendRequest()
|
||||
|
||||
|
After Width: | Height: | Size: 659 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 913 B |
|
After Width: | Height: | Size: 769 B |
|
After Width: | Height: | Size: 889 B |
|
After Width: | Height: | Size: 995 B |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 669 B |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -1,39 +1,38 @@
|
||||
<template>
|
||||
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="57%" center
|
||||
custom-class="custom-dialog" :show-close="false" class="center-dialog">
|
||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||
{{ $t('modelConfigDialog.addModel') }}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="handleClose">
|
||||
×
|
||||
</button>
|
||||
|
||||
<CustomDialog
|
||||
:visible.sync="dialogVisible"
|
||||
:title="$t('modelConfigDialog.addModel')"
|
||||
width="57%"
|
||||
class="add-model-dialog"
|
||||
:confirmLoading="saving"
|
||||
@close="handleClose"
|
||||
@confirm="confirm"
|
||||
>
|
||||
<div class="dialog-content">
|
||||
<!-- 模型信息部分 -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">{{ $t('modelConfigDialog.modelInfo') }}</div>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.enable') }}</span>
|
||||
<div class="section-header">
|
||||
<div class="section-title">{{ $t('modelConfigDialog.modelInfo') }}</div>
|
||||
<div class="switch-group">
|
||||
<div class="switch-item">
|
||||
<span>{{ $t('modelConfigDialog.enable') }}</span>
|
||||
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div style="display: none; align-items: center;">
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.setDefault') }}</span>
|
||||
<div class="switch-item" style="display: none;">
|
||||
<span>{{ $t('modelConfigDialog.setDefault') }}</span>
|
||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<div class="divider"></div>
|
||||
<el-form :model="formData" label-width="auto" label-position="left" class="custom-form">
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('modelConfigDialog.modelId')" prop="id" style="flex: 1;">
|
||||
<el-input v-model="formData.id" :placeholder="$t('modelConfigDialog.enterModelId')" class="custom-input-bg"
|
||||
maxlength="32"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
|
||||
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
|
||||
class="custom-input-bg"></el-input>
|
||||
@@ -44,7 +43,7 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
||||
<el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')"
|
||||
class="custom-select custom-input-bg" style="width: 100%;" @focus="loadProviders" filterable>
|
||||
@@ -57,7 +56,6 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
|
||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
|
||||
<el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
|
||||
class="custom-input-bg"></el-input>
|
||||
@@ -70,13 +68,13 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{
|
||||
$t('modelConfigDialog.callInfo') }}</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
<teleport v-if="chunkedCallInfoFields.length">
|
||||
<div class="section-title">{{ $t('modelConfigDialog.callInfo') }}</div>
|
||||
<div class="divider"></div>
|
||||
</teleport>
|
||||
|
||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex"
|
||||
style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex" class="form-row">
|
||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop" style="flex: 1;">
|
||||
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
|
||||
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
|
||||
@@ -85,19 +83,17 @@
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<el-button type="primary" @click="confirm" class="save-btn" :loading="saving" :disabled="saving">
|
||||
{{ $t('modelConfigDialog.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
export default {
|
||||
name: 'AddModelDialog',
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
props: {
|
||||
visible: { type: Boolean, required: true },
|
||||
modelType: { type: String, required: true }
|
||||
@@ -178,16 +174,10 @@ export default {
|
||||
});
|
||||
this.formData.configJson = { ...defaultConfig };
|
||||
},
|
||||
handleVisibleChange(val) {
|
||||
this.dialogVisible = val;
|
||||
this.$emit('update:visible', val);
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.saving = false;
|
||||
this.dialogVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
initDynamicConfig() {
|
||||
@@ -232,7 +222,7 @@ export default {
|
||||
|
||||
try {
|
||||
this.$emit('confirm', submitData);
|
||||
this.$emit('update:visible', false);
|
||||
this.handleClose();
|
||||
this.resetForm();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -297,134 +287,71 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-dialog {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
padding-bottom: 17px;
|
||||
}
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/global.scss';
|
||||
|
||||
.custom-dialog .el-dialog__header {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
::v-deep .el-dialog {
|
||||
margin-top: 6vh !important;
|
||||
}
|
||||
|
||||
.center-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
::v-deep .el-dialog__body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
@include scrollbar-style;
|
||||
}
|
||||
.add-model-dialog {
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.center-dialog .el-dialog {
|
||||
margin: 0 0 auto !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.section-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #3d4566;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
.switch-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
.switch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.custom-select .el-input__suffix {
|
||||
background: #e6e8ea;
|
||||
right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 9px;
|
||||
}
|
||||
span {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #e9e9e9;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.custom-select .el-icon-arrow-up:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 7px solid #c0c4cc;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
transform: rotate(180deg);
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-form .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item__label {
|
||||
color: #3d4566;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
|
||||
}
|
||||
|
||||
.custom-form .el-form-item.prop-remark .el-form-item__label {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
|
||||
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
|
||||
color: #9c9f9e;
|
||||
}
|
||||
|
||||
|
||||
.custom-input-bg .el-input__inner,
|
||||
.custom-input-bg .el-textarea__inner {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
.save-btn {
|
||||
background: #e6f0fd;
|
||||
color: #237ff4;
|
||||
border: 1px solid #b3d1ff;
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: linear-gradient(to right, #237ff4, #9c40d5);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.custom-switch .el-switch__core {
|
||||
border-radius: 20px;
|
||||
height: 23px;
|
||||
@@ -439,7 +366,7 @@ export default {
|
||||
background-color: white;
|
||||
top: 3px;
|
||||
left: 4px;
|
||||
transition: all .3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.custom-switch.is-checked .el-switch__core {
|
||||
@@ -454,12 +381,10 @@ export default {
|
||||
background-color: #1b47ee;
|
||||
}
|
||||
|
||||
|
||||
[style*="display: flex"] {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner {
|
||||
height: 32px;
|
||||
}
|
||||
::v-deep .el-input__inner {
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +1,42 @@
|
||||
<template>
|
||||
<form>
|
||||
<el-dialog :visible.sync="dialogVisible" width="24%" center>
|
||||
<div
|
||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||
<div
|
||||
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img loading="lazy" src="@/assets/login/shield.png" alt=""
|
||||
style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||
</div>
|
||||
{{ $t('changePassword.title') }}
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
<CustomDialog
|
||||
:title="$t('changePassword.title')"
|
||||
:visible.sync="dialogVisible"
|
||||
width="600px"
|
||||
@cancel="cancel"
|
||||
@confirm="confirm">
|
||||
<div class="password-form">
|
||||
<div class="form-label">
|
||||
<span class="required">*</span>
|
||||
{{ $t('changePassword.oldPasswordLabel') }}
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input :placeholder="$t('changePassword.oldPasswordPlaceholder')" v-model="oldPassword" type="password" show-password />
|
||||
</div>
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
<div class="form-label" style="margin-top: 12px;">
|
||||
<span class="required">*</span>
|
||||
{{ $t('changePassword.newPasswordLabel') }}
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input :placeholder="$t('changePassword.newPasswordPlaceholder')" v-model="newPassword" type="password" show-password />
|
||||
</div>
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
<div class="form-label" style="margin-top: 12px;">
|
||||
<span class="required">*</span>
|
||||
{{ $t('changePassword.confirmPasswordLabel') }}
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input :placeholder="$t('changePassword.confirmPasswordPlaceholder')" v-model="confirmNewPassword" type="password" show-password />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
{{ $t('changePassword.confirmButton') }}
|
||||
</div>
|
||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||
{{ $t('changePassword.cancelButton') }}
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
import { mapActions } from 'vuex';
|
||||
import { changeLanguage } from '@/i18n';
|
||||
import CustomDialog from '@/components/CustomDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'ChangePasswordDialog',
|
||||
@@ -67,6 +54,9 @@ export default {
|
||||
confirmNewPassword: ""
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.dialogVisible = val;
|
||||
@@ -100,7 +90,6 @@ export default {
|
||||
});
|
||||
this.logout().then(() => {
|
||||
this.$router.push('/login');
|
||||
this.$emit('update:visible', false);
|
||||
});
|
||||
} else {
|
||||
this.$message.error(res.data.msg || this.$t('changePassword.changeFailed'));
|
||||
@@ -108,10 +97,9 @@ export default {
|
||||
}, (err) => {
|
||||
this.$message.error(err.msg || this.$t('changePassword.changeFailed'));
|
||||
});
|
||||
this.$emit('input', false);
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
cancel() {
|
||||
this.dialogVisible = false;
|
||||
this.resetForm();
|
||||
},
|
||||
resetForm() {
|
||||
@@ -124,38 +112,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.form-label {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.input-46 {
|
||||
background: #f6f8fb;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
border-radius: 23px;
|
||||
background: #5778ff;
|
||||
height: 40px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__headerbtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||