Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7782b1f6e |
@@ -53,98 +53,113 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
<img alt="响应速度感受" src="docs/images/demo9.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
||||||
</a>
|
</picture>
|
||||||
</td>
|
</a>
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
<img alt="多指令任务" src="docs/images/demo11.png" />
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
<img alt="实时打断" src="docs/images/demo10.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
<img alt="拍照识物品" src="docs/images/demo12.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ VAD:
|
|||||||
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||||
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||||
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||||
6、[修改代码后怎么编译自己的Docker镜像](./docker-build.md)<br/>
|
|
||||||
|
|
||||||
### 8、编译固件相关教程
|
### 8、编译固件相关教程
|
||||||
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
||||||
@@ -85,8 +84,6 @@ VAD:
|
|||||||
10、[如何部署上下文源](./context-provider-integration.md)<br/>
|
10、[如何部署上下文源](./context-provider-integration.md)<br/>
|
||||||
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
|
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
|
||||||
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
|
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
|
||||||
13、[如何开启设备呼叫插件](./device-call-guide.md)<br/>
|
|
||||||
14、[如何开启联网搜索功能](./web-search-integration.md)<br/>
|
|
||||||
|
|
||||||
### 10、数字人相关教程
|
### 10、数字人相关教程
|
||||||
1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/>
|
1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/>
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
# 设备间相互呼叫插件使用指南
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
设备呼叫功能允许两个已配置设备之间通过语音/数据通道进行双向通信。设备A呼叫设备B时,系统通过以下流程实现:
|
|
||||||
|
|
||||||
```
|
|
||||||
设备A → 授权校验 → MQTT网关 → 设备B远程唤醒 → 建立连接 → 通话建立
|
|
||||||
```
|
|
||||||
## 使用这个功能的前提条件
|
|
||||||
1. 你必须要有至少两个设备,每个设备型号必须是`ESP32-S3`,因为只有`ESP32-S3`才支持远程唤醒功能。
|
|
||||||
2. 你的设备必须要有`两个麦克风`。但是如果你的设备只有`单个麦克风`,你只是想体验一下这个功能,也是可以的,但是会有强烈的卡顿感。
|
|
||||||
3. 你必须使用[全模块部署](Deployment_all.md)本项目,因为你需要`智控台`来管理设备的权限和通信。
|
|
||||||
4. 你必须安装并配置好`2026年5月27日`以后的[MQTT网关服务](mqtt-gateway-integration.md),如果你已经部署了MQTT网关服务,请确认代码的版本是`2026年5月27日`之后的版本。
|
|
||||||
|
|
||||||
以上是使用这个功能的硬性条件,接下来会详细介绍。
|
|
||||||
|
|
||||||
## 配置步骤
|
|
||||||
|
|
||||||
### 第一步:开启通讯录功能
|
|
||||||
|
|
||||||
1. 确认你的智控台版本是`0.9.4`或以上版本。
|
|
||||||
2. 登录智控台后台
|
|
||||||
3. 进入 **系统功能配置**
|
|
||||||
4. 在左侧功能列表中勾选 **通讯录**
|
|
||||||
5. 点击 **保存配置** 确认
|
|
||||||
|
|
||||||
### 第二步:配置设备间呼叫权限
|
|
||||||
|
|
||||||
1. 在智控台顶部菜单点击 **通讯录**
|
|
||||||
2. 在左侧智能体下,设备列表中选择你的设备A(支持按 MAC地址 或 备注名 搜索)
|
|
||||||
3. 在右侧详情面板中,找到目标设备B的称呼设置,例如 **"小王"**
|
|
||||||
4. 勾选设备B的 **呼叫权限** 复选框
|
|
||||||
5. 点击 **保存**
|
|
||||||
|
|
||||||
**双向授权说明:** 如需设备A和设备B互相通信,必须在两侧智控台分别配置对方权限。例如:
|
|
||||||
|
|
||||||
- 在设备A的配置中勾选设备B → 设备A可与设备B通信
|
|
||||||
- 在设备B的配置中勾选设备A → 设备B可与设备A通信
|
|
||||||
|
|
||||||
### 第三步:在智能体配置添加呼叫工具
|
|
||||||
|
|
||||||
1. 在智控台顶部菜单点击 **智能体管理**
|
|
||||||
2. 在刚刚配置设备联系人的相关智能体中点击 **编辑角色**
|
|
||||||
3. 在右侧详情面板中,点击 **编辑功能**
|
|
||||||
4. 勾选 **设备呼叫设备** 工具
|
|
||||||
5. 点击 **保存配置** 确认
|
|
||||||
6. 在外侧再次点击 **保存配置** ,随即重启设备
|
|
||||||
|
|
||||||
### 第四步:固件端添加远程唤醒工具
|
|
||||||
|
|
||||||
1. 在[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32) 代码的基础上增加远程唤醒工具MCP,版本支持为2.1.0至2.2.6(2026年5月29日的版本)
|
|
||||||
2. 在application.h文件中添加远程唤醒函数声明
|
|
||||||
```cpp
|
|
||||||
void RemoteWakeup(const std::string& reason);
|
|
||||||
```
|
|
||||||
3. 在application.cc文件中添加远程唤醒函数
|
|
||||||
```cpp
|
|
||||||
void Application::RemoteWakeup(const std::string& reason){
|
|
||||||
if (!protocol_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto state = GetDeviceState();
|
|
||||||
|
|
||||||
if (state == kDeviceStateIdle) {
|
|
||||||
audio_service_.EncodeWakeWord();
|
|
||||||
|
|
||||||
if (!protocol_->IsAudioChannelOpened()) {
|
|
||||||
SetDeviceState(kDeviceStateConnecting);
|
|
||||||
if (!protocol_->OpenAudioChannel()) {
|
|
||||||
audio_service_.EnableWakeWordDetection(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::string wake_word = reason;
|
|
||||||
#if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD
|
|
||||||
// Encode and send the wake word data to the server
|
|
||||||
while (auto packet = audio_service_.PopWakeWordPacket()) {
|
|
||||||
protocol_->SendAudio(std::move(packet));
|
|
||||||
}
|
|
||||||
// Set the chat state to wake word detected
|
|
||||||
protocol_->SendWakeWordDetected(wake_word);
|
|
||||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
|
||||||
#else
|
|
||||||
// Set flag to play popup sound after state changes to listening
|
|
||||||
// (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing)
|
|
||||||
play_popup_on_listening_ = true;
|
|
||||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
|
||||||
#endif
|
|
||||||
} else if (state == kDeviceStateSpeaking) {
|
|
||||||
AbortSpeaking(kAbortReasonWakeWordDetected);
|
|
||||||
SetDeviceState(kDeviceStateIdle);
|
|
||||||
} else if (state == kDeviceStateActivating) {
|
|
||||||
SetDeviceState(kDeviceStateIdle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
4. 在mcp_server.cc文件中添加远程唤醒工具
|
|
||||||
```cpp
|
|
||||||
AddUserOnlyTool("self.remote_wakeup", "Remote wakeup function with configurable parameters",
|
|
||||||
PropertyList({
|
|
||||||
Property("reason", kPropertyTypeString, "Wakeup reason"),
|
|
||||||
}),
|
|
||||||
[this](const PropertyList& properties) -> ReturnValue {
|
|
||||||
std::string reason = properties["reason"].value<std::string>();
|
|
||||||
ESP_LOGI(TAG, "Wakeup reason=%s", reason.c_str());
|
|
||||||
auto& app = Application::GetInstance();
|
|
||||||
app.RemoteWakeup(reason);
|
|
||||||
return true;
|
|
||||||
```
|
|
||||||
5. 按照 [固件编译烧录指南](firmware-build.md) 完成固件烧录
|
|
||||||
6. 无论你的设备是单麦还是双麦,请在编译环节,勾选开启AEC功能!
|
|
||||||
7. 无论你的设备是单麦还是双麦,请在编译环节,勾选开启AEC功能!
|
|
||||||
8. 无论你的设备是单麦还是双麦,请在编译环节,勾选开启AEC功能!
|
|
||||||
|
|
||||||
### 第五步:配置MQTT网关服务
|
|
||||||
|
|
||||||
1. 部署MQTT网关服务,参考 [MQTT网关集成文档](mqtt-gateway-integration.md)
|
|
||||||
2. 如果已经部署请确认代码的版本是2026年5月27日的版本
|
|
||||||
|
|
||||||
## 呼叫流程说明
|
|
||||||
|
|
||||||
准备两个设备,在智控台上面配置好通讯权限和在智能体中添加呼叫工具之后,在其中一个小智对话那里对他说:”呼叫XXX“,观察设备B是否响应。
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q: 设备B没有响应呼叫?
|
|
||||||
|
|
||||||
- 检查设备B是否在线(智控台设备状态)
|
|
||||||
- 确认设备B的固件已正确集成远程唤醒工具
|
|
||||||
- 检查MQTT网关连接是否正常
|
|
||||||
- 验证双向权限配置是否完整
|
|
||||||
|
|
||||||
### Q: 提示"无呼叫权限"?
|
|
||||||
|
|
||||||
- 在智控台确认设备A已勾选设备B的呼叫权限
|
|
||||||
- 确认配置已保存(非仅修改未保存)
|
|
||||||
|
|
||||||
### Q: 如何确认通讯录功能已开启?
|
|
||||||
|
|
||||||
- 智控台顶部菜单如显示"通讯录"入口,则表示已开启
|
|
||||||
|
|
||||||
### Q: 我叫他呼叫"张山",但是他老是识别成"张三",怎么办?
|
|
||||||
- 可以查阅你使用的asr服务的文档,确认是否支持热词识别。
|
|
||||||
- 如果你用的是`FunASRServer`,可以在容器里的`热词文件`里添加"张山",然后重启容器。
|
|
||||||
- 如果你用的是`火山引擎`的服务,可以在`火山引擎的控制台`里添加`热词文件`,然后回到智控台的`模型配置页面`,把`热词文件名称`配置在`火山引擎的tts`上去。
|
|
||||||
|
|
||||||
@@ -1,74 +1,21 @@
|
|||||||
# 本地编译docker镜像方法
|
# 本地编译docker镜像方法
|
||||||
|
|
||||||
现在本项目已经使用`github`的`自动编译docker镜像`功能,如果您拉取的是项目发行的镜像,您没有自己编译镜像的需求,那就忽略本文档。
|
现在本项目已经使用github自动编译docker功能,本文档是提供给有本地编译docker镜像需求的朋友准备的。
|
||||||
|
|
||||||
如果您修改了源码,然后想采用`docker`的方式部署运行,可以参照以下步骤操作:
|
1、安装docker
|
||||||
|
```
|
||||||
## 1、环境准备
|
|
||||||
|
|
||||||
安装docker:
|
|
||||||
```bash
|
|
||||||
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
```
|
```
|
||||||
|
2、编译docker镜像
|
||||||
## 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:新的版本号 .
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
#进入项目根目录
|
||||||
|
# 编译server
|
||||||
|
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
|
||||||
|
# 编译web
|
||||||
|
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
|
||||||
|
|
||||||
## 3、修改docker-compose配置
|
# 编译完成后,可以使用docker-compose启动项目
|
||||||
|
# docker-compose.yml你需要修改成自己编译的镜像版本
|
||||||
```bash
|
|
||||||
cd main/xiaozhi-server
|
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: 152 KiB After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 280 KiB |
@@ -52,98 +52,113 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
<img alt="声纹识别" src="../images/demo14.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||||
</a>
|
</picture>
|
||||||
</td>
|
</a>
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
<img alt="多指令任务" src="../images/demo11.png" />
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
<img alt="实时打断" src="../images/demo10.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
<img alt="自定义音色" src="../images/demo2.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
<img alt="播报新闻" src="../images/demo0.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -52,98 +52,113 @@ Want to see the usage effects? Click the videos below 🎥
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
<img alt="声纹识别" src="../images/demo14.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||||
</a>
|
</picture>
|
||||||
</td>
|
</a>
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
<img alt="多指令任务" src="../images/demo11.png" />
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
<img alt="实时打断" src="../images/demo10.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
<img alt="自定义音色" src="../images/demo2.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
<img alt="播报新闻" src="../images/demo0.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -52,98 +52,113 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
<img alt="Experiência de velocidade de resposta" src="../images/demo9.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
<img alt="Segredo da otimização de velocidade" src="../images/demo6.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
<img alt="Cenário médico complexo" src="../images/demo1.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
<img alt="Envio de comandos MQTT" src="../images/demo4.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
<img alt="Reconhecimento de impressão vocal" src="../images/demo14.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
<img alt="Controle de interruptores de eletrodomésticos" src="../images/demo5.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
<img alt="Ponto de acesso MCP" src="../images/demo13.png" />
|
||||||
</a>
|
</picture>
|
||||||
</td>
|
</a>
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
<img alt="Tarefas com múltiplos comandos" src="../images/demo11.png" />
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
<img alt="Interrupção em tempo real" src="../images/demo10.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
<img alt="Fotografar e identificar objetos" src="../images/demo12.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
<img alt="Timbre de voz personalizado" src="../images/demo2.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
<img alt="Comunicação em cantonês" src="../images/demo3.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
<img alt="Transmissão de notícias" src="../images/demo0.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -52,98 +52,113 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
<img alt="声纹识别" src="../images/demo14.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||||
</a>
|
</picture>
|
||||||
</td>
|
</a>
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
<img alt="多指令任务" src="../images/demo11.png" />
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
<img alt="实时打断" src="../images/demo10.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
<img alt="自定义音色" src="../images/demo2.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
<img alt="播报新闻" src="../images/demo0.png" />
|
||||||
</a>
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
# 联网搜索插件使用指南
|
|
||||||
|
|
||||||
## 功能简介
|
|
||||||
|
|
||||||
联网搜索插件 `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
|
|
||||||
```
|
|
||||||
|
|
||||||
配置完成后重启服务即可生效。
|
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
<label for="emojiEnabled">情绪表情:</label>
|
<label for="emojiEnabled">情绪表情:</label>
|
||||||
<select id="emojiEnabled" class="model-select">
|
<select id="emojiEnabled" class="model-select">
|
||||||
<option value="true">启用</option>
|
<option value="true">启用</option>
|
||||||
<option value="false" selected>禁用</option>
|
<option value="false">禁用</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -146,11 +146,6 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String SERVER_AUTH_ENABLED = "server.auth.enabled";
|
String SERVER_AUTH_ENABLED = "server.auth.enabled";
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统功能菜单配置
|
|
||||||
*/
|
|
||||||
String SYSTEM_WEB_MENU = "system-web.menu";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无记忆
|
* 无记忆
|
||||||
*/
|
*/
|
||||||
@@ -324,7 +319,7 @@ public interface Constant {
|
|||||||
/**
|
/**
|
||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.9.5";
|
public static final String VERSION = "0.9.3";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无效固件URL
|
* 无效固件URL
|
||||||
|
|||||||
@@ -188,11 +188,4 @@ public class RedisKeys {
|
|||||||
return "ota:upload:count:" + username;
|
return "ota:upload:count:" + username;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备通讯录缓存Key
|
|
||||||
*/
|
|
||||||
public static String getAddressBookKey() {
|
|
||||||
return "device:address_book:all";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,11 +26,4 @@ public interface AgentPluginMappingService extends IService<AgentPluginMapping>
|
|||||||
* @param agentId
|
* @param agentId
|
||||||
*/
|
*/
|
||||||
void deleteByAgentId(String agentId);
|
void deleteByAgentId(String agentId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据插件ID删除所有智能体的插件映射
|
|
||||||
*
|
|
||||||
* @param pluginId 插件ID
|
|
||||||
*/
|
|
||||||
void deleteByPluginId(String pluginId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,11 +104,4 @@ public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappin
|
|||||||
agentPluginMappingMapper.delete(updateWrapper);
|
agentPluginMappingMapper.delete(updateWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteByPluginId(String pluginId) {
|
|
||||||
UpdateWrapper<AgentPluginMapping> updateWrapper = new UpdateWrapper<>();
|
|
||||||
updateWrapper.eq("plugin_id", pluginId);
|
|
||||||
agentPluginMappingMapper.delete(updateWrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import xiaozhi.common.constant.Constant;
|
|||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.modules.config.service.ConfigService;
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.device.service.DeviceAddressBookService;
|
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -25,9 +24,6 @@ public class SystemInitConfig {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RedisUtils redisUtils;
|
private RedisUtils redisUtils;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private DeviceAddressBookService deviceAddressBookService;
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
// 检查版本号
|
// 检查版本号
|
||||||
@@ -41,8 +37,5 @@ public class SystemInitConfig {
|
|||||||
|
|
||||||
sysParamsService.initServerSecret();
|
sysParamsService.initServerSecret();
|
||||||
configService.getConfig(false);
|
configService.getConfig(false);
|
||||||
|
|
||||||
// 初始化设备通讯录缓存
|
|
||||||
deviceAddressBookService.refreshCache();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,6 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
|
|||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@@ -23,17 +22,13 @@ import xiaozhi.common.redis.RedisKeys;
|
|||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.device.dto.DeviceAddressBookAliasDTO;
|
|
||||||
import xiaozhi.modules.device.dto.DeviceAddressBookPermissionDTO;
|
|
||||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO;
|
import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceAddressBookService;
|
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@@ -42,14 +37,11 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
@RequestMapping("/device")
|
@RequestMapping("/device")
|
||||||
public class DeviceController {
|
public class DeviceController {
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
private final DeviceAddressBookService deviceAddressBookService;
|
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
private final SysParamsService sysParamsService;
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
public DeviceController(DeviceService deviceService, DeviceAddressBookService deviceAddressBookService,
|
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService) {
|
||||||
RedisUtils redisUtils, SysParamsService sysParamsService) {
|
|
||||||
this.deviceService = deviceService;
|
this.deviceService = deviceService;
|
||||||
this.deviceAddressBookService = deviceAddressBookService;
|
|
||||||
this.redisUtils = redisUtils;
|
this.redisUtils = redisUtils;
|
||||||
this.sysParamsService = sysParamsService;
|
this.sysParamsService = sysParamsService;
|
||||||
}
|
}
|
||||||
@@ -86,10 +78,10 @@ public class DeviceController {
|
|||||||
@GetMapping("/bind/{agentId}")
|
@GetMapping("/bind/{agentId}")
|
||||||
@Operation(summary = "获取已绑定设备")
|
@Operation(summary = "获取已绑定设备")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<List<UserShowDeviceListVO>> getUserDevices(@PathVariable String agentId) {
|
public Result<List<DeviceEntity>> getUserDevices(@PathVariable String agentId) {
|
||||||
UserDetail user = SecurityUser.getUser();
|
UserDetail user = SecurityUser.getUser();
|
||||||
List<UserShowDeviceListVO> devices = deviceService.getUserDeviceList(user.getId(), agentId);
|
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
|
||||||
return new Result<List<UserShowDeviceListVO>>().ok(devices);
|
return new Result<List<DeviceEntity>>().ok(devices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/bind/{agentId}")
|
@PostMapping("/bind/{agentId}")
|
||||||
@@ -167,48 +159,4 @@ public class DeviceController {
|
|||||||
response.setMsg("Tools called successfully");
|
response.setMsg("Tools called successfully");
|
||||||
return response.ok(result);
|
return response.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/address-book/{macAddress}")
|
|
||||||
@Operation(summary = "获取设备通讯录")
|
|
||||||
@RequiresPermissions("sys:role:normal")
|
|
||||||
public Result<Object> getAddressBook(@PathVariable String macAddress) {
|
|
||||||
return new Result<Object>().ok(deviceAddressBookService.getAddressBookList(macAddress));
|
|
||||||
}
|
|
||||||
|
|
||||||
@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, Object>>().error("未找到对应设备");
|
|
||||||
}
|
|
||||||
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>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/address-book/permission")
|
|
||||||
@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>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package xiaozhi.modules.device.dao;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
|
|
||||||
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取设备通讯录列表
|
|
||||||
*/
|
|
||||||
List<DeviceAddressBookEntity> getAddressBookList(@Param("macAddress") String macAddress);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新别名
|
|
||||||
*/
|
|
||||||
void updateAlias(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("alias") String alias);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新权限
|
|
||||||
*/
|
|
||||||
void updatePermission(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("hasPermission") Boolean hasPermission);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量删除设备相关的通讯录记录
|
|
||||||
*/
|
|
||||||
void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package xiaozhi.modules.device.dto;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Schema(description = "更新设备通讯录别名")
|
|
||||||
public class DeviceAddressBookAliasDTO {
|
|
||||||
|
|
||||||
@NotBlank(message = "MAC地址不能为空")
|
|
||||||
@Schema(description = "本设备MAC地址")
|
|
||||||
private String macAddress;
|
|
||||||
|
|
||||||
@NotBlank(message = "目标MAC地址不能为空")
|
|
||||||
@Schema(description = "对方设备MAC地址")
|
|
||||||
private String targetMac;
|
|
||||||
|
|
||||||
@Schema(description = "我对对方的称呼")
|
|
||||||
private String alias;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package xiaozhi.modules.device.dto;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Schema(description = "更新设备通讯录权限")
|
|
||||||
public class DeviceAddressBookPermissionDTO {
|
|
||||||
|
|
||||||
@NotBlank(message = "MAC地址不能为空")
|
|
||||||
@Schema(description = "本设备MAC地址")
|
|
||||||
private String macAddress;
|
|
||||||
|
|
||||||
@NotBlank(message = "目标MAC地址不能为空")
|
|
||||||
@Schema(description = "对方设备MAC地址")
|
|
||||||
private String targetMac;
|
|
||||||
|
|
||||||
@Schema(description = "是否有权限呼叫")
|
|
||||||
private Boolean hasPermission;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package xiaozhi.modules.device.entity;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = false)
|
|
||||||
@TableName("ai_device_address_book")
|
|
||||||
@Schema(description = "设备通讯录")
|
|
||||||
public class DeviceAddressBookEntity {
|
|
||||||
|
|
||||||
@TableId(type = IdType.INPUT)
|
|
||||||
@Schema(description = "本设备MAC地址")
|
|
||||||
private String macAddress;
|
|
||||||
|
|
||||||
@Schema(description = "对方设备MAC地址")
|
|
||||||
private String targetMac;
|
|
||||||
|
|
||||||
@Schema(description = "我对对方的称呼")
|
|
||||||
private String alias;
|
|
||||||
|
|
||||||
@Schema(description = "是否有权限呼叫")
|
|
||||||
private Boolean hasPermission;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
|
||||||
@Schema(description = "创建人")
|
|
||||||
private Long creator;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
|
||||||
@Schema(description = "创建时间")
|
|
||||||
private Date createDate;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
|
||||||
@Schema(description = "更新人")
|
|
||||||
private Long updater;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
|
||||||
@Schema(description = "更新时间")
|
|
||||||
private Date updateDate;
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package xiaozhi.modules.device.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
|
|
||||||
|
|
||||||
public interface DeviceAddressBookService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取设备通讯录列表
|
|
||||||
*/
|
|
||||||
List<DeviceAddressBookEntity> getAddressBookList(String macAddress);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有设备的通讯录(全局缓存用)
|
|
||||||
*/
|
|
||||||
Map<String, Map<String, String>> getAllAddressBooks();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新别名
|
|
||||||
*/
|
|
||||||
void updateAlias(String macAddress, String targetMac, String alias);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新权限
|
|
||||||
*/
|
|
||||||
void updatePermission(String macAddress, String targetMac, Boolean hasPermission);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加或更新通讯录记录
|
|
||||||
*/
|
|
||||||
void saveOrUpdate(String macAddress, String targetMac, String alias, Boolean hasPermission);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新通讯录缓存
|
|
||||||
*/
|
|
||||||
void refreshCache();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据昵称发起呼叫
|
|
||||||
* @param callerMac 主叫方MAC地址
|
|
||||||
* @param nickname 被叫方昵称
|
|
||||||
* @param isAnswer 是否为接听模式(跳过权限检查)
|
|
||||||
*/
|
|
||||||
Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量删除设备相关的通讯录记录
|
|
||||||
*/
|
|
||||||
void deleteByMacAddresses(List<String> macAddresses);
|
|
||||||
}
|
|
||||||
@@ -30,11 +30,6 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
|||||||
*/
|
*/
|
||||||
List<DeviceEntity> getUserDevices(Long userId, String agentId);
|
List<DeviceEntity> getUserDevices(Long userId, String agentId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户指定智能体的设备列表(带时区处理),
|
|
||||||
*/
|
|
||||||
List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解绑设备
|
* 解绑设备
|
||||||
*/
|
*/
|
||||||
@@ -138,4 +133,4 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
|||||||
*/
|
*/
|
||||||
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
|
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
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 {
|
|
||||||
|
|
||||||
private final DeviceAddressBookDao deviceAddressBookDao;
|
|
||||||
private final RedisUtils redisUtils;
|
|
||||||
private final DeviceService deviceService;
|
|
||||||
private final SysParamsService sysParamsService;
|
|
||||||
|
|
||||||
public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils,
|
|
||||||
@Lazy DeviceService deviceService, SysParamsService sysParamsService) {
|
|
||||||
this.deviceAddressBookDao = deviceAddressBookDao;
|
|
||||||
this.redisUtils = redisUtils;
|
|
||||||
this.deviceService = deviceService;
|
|
||||||
this.sysParamsService = sysParamsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<DeviceAddressBookEntity> getAddressBookList(String macAddress) {
|
|
||||||
return deviceAddressBookDao.getAddressBookList(macAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public Map<String, Map<String, String>> getAllAddressBooks() {
|
|
||||||
Object cached = redisUtils.get(RedisKeys.getAddressBookKey());
|
|
||||||
if (cached != null) {
|
|
||||||
return (Map<String, Map<String, String>>) cached;
|
|
||||||
}
|
|
||||||
refreshCache();
|
|
||||||
return (Map<String, Map<String, String>>) redisUtils.get(RedisKeys.getAddressBookKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
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 errorResult("未找到备注为'" + nickname + "'的设备");
|
|
||||||
}
|
|
||||||
String targetMacWithPerm = callerBook.get(nickname);
|
|
||||||
if (targetMacWithPerm == null) {
|
|
||||||
return errorResult("未找到备注为'" + nickname + "'的设备");
|
|
||||||
}
|
|
||||||
String[] parts = targetMacWithPerm.split("\\|");
|
|
||||||
String targetMac = parts[0];
|
|
||||||
boolean hasPermission = parts.length > 1 && "1".equals(parts[1]);
|
|
||||||
|
|
||||||
if (!hasPermission) {
|
|
||||||
return errorResult("呼叫失败,您没有权限呼叫该设备");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取目标设备如何称呼主叫方
|
|
||||||
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
|
|
||||||
public void refreshCache() {
|
|
||||||
Map<String, Map<String, String>> result = new HashMap<>();
|
|
||||||
List<DeviceAddressBookEntity> allRecords = deviceAddressBookDao.selectList(null);
|
|
||||||
Map<String, String> reverseMap = 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()) {
|
|
||||||
reverseMap.put(macB + ":" + macA, alias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建反向映射: B对A的映射 macA -> nickname
|
|
||||||
for (DeviceAddressBookEntity entity : allRecords) {
|
|
||||||
String macA = entity.getMacAddress().toLowerCase();
|
|
||||||
String macB = entity.getTargetMac().toLowerCase();
|
|
||||||
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) {
|
|
||||||
String finalAlias = generateUniqueAlias(macAddress, targetMac, alias);
|
|
||||||
deviceAddressBookDao.updateAlias(macAddress, targetMac, finalAlias);
|
|
||||||
refreshCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updatePermission(String macAddress, String targetMac, Boolean hasPermission) {
|
|
||||||
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
|
|
||||||
refreshCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void saveOrUpdate(String macAddress, String targetMac, String alias, Boolean hasPermission) {
|
|
||||||
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 为空,就默认使用设备名称
|
|
||||||
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) {
|
|
||||||
updateAlias(macAddress, targetMac, alias);
|
|
||||||
}
|
|
||||||
if (hasPermission != null) {
|
|
||||||
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,7 +6,6 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -64,7 +63,6 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
|||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.entity.OtaEntity;
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceAddressBookService;
|
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.device.service.OtaService;
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
@@ -82,7 +80,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
private final SysParamsService sysParamsService;
|
private final SysParamsService sysParamsService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
private final OtaService otaService;
|
private final OtaService otaService;
|
||||||
private final DeviceAddressBookService deviceAddressBookService;
|
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
|
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
|
||||||
@@ -301,28 +298,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
return baseDao.selectList(wrapper);
|
return baseDao.selectList(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId) {
|
|
||||||
List<DeviceEntity> devices = getUserDevices(userId, agentId);
|
|
||||||
return devices.stream().map(device -> {
|
|
||||||
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
|
||||||
vo.setDeviceType(device.getBoard());
|
|
||||||
// 设置UTC时间戳供前端使用时区转换
|
|
||||||
if (device.getLastConnectedAt() != null) {
|
|
||||||
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
|
|
||||||
}
|
|
||||||
return vo;
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unbindDevice(Long userId, String deviceId) {
|
public void unbindDevice(Long userId, String deviceId) {
|
||||||
// 先查询设备信息,获取agentId和macAddress
|
// 先查询设备信息,获取agentId
|
||||||
DeviceEntity device = baseDao.selectById(deviceId);
|
DeviceEntity device = baseDao.selectById(deviceId);
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String macAddress = device.getMacAddress();
|
|
||||||
if (StringUtils.isNotBlank(device.getAgentId())) {
|
if (StringUtils.isNotBlank(device.getAgentId())) {
|
||||||
// 清除智能体设备数量缓存
|
// 清除智能体设备数量缓存
|
||||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
|
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
|
||||||
@@ -332,9 +314,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
wrapper.eq("user_id", userId);
|
wrapper.eq("user_id", userId);
|
||||||
wrapper.eq("id", deviceId);
|
wrapper.eq("id", deviceId);
|
||||||
baseDao.delete(wrapper);
|
baseDao.delete(wrapper);
|
||||||
|
|
||||||
// 删除设备相关的通讯录权限记录
|
|
||||||
deviceAddressBookService.deleteByMacAddresses(Collections.singletonList(macAddress));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -353,23 +332,9 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteByAgentId(String agentId) {
|
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<>();
|
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||||
wrapper.eq("agent_id", agentId);
|
wrapper.eq("agent_id", agentId);
|
||||||
baseDao.delete(wrapper);
|
baseDao.delete(wrapper);
|
||||||
|
|
||||||
// 批量删除这些设备相关的所有通讯录权限记录
|
|
||||||
if (!devices.isEmpty()) {
|
|
||||||
List<String> macAddresses = devices.stream()
|
|
||||||
.map(DeviceEntity::getMacAddress)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
deviceAddressBookService.deleteByMacAddresses(macAddresses);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -391,11 +356,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
sysUserUtilService.assignUsername(device.getUserId(),
|
sysUserUtilService.assignUsername(device.getUserId(),
|
||||||
vo::setBindUserName);
|
vo::setBindUserName);
|
||||||
vo.setDeviceType(device.getBoard());
|
vo.setDeviceType(device.getBoard());
|
||||||
vo.setBoard(device.getBoard());
|
|
||||||
// 设置UTC时间戳供前端使用时区转换
|
|
||||||
if (device.getLastConnectedAt() != null) {
|
|
||||||
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
|
|
||||||
}
|
|
||||||
return vo;
|
return vo;
|
||||||
}).toList();
|
}).toList();
|
||||||
// 计算页数
|
// 计算页数
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package xiaozhi.modules.device.vo;
|
package xiaozhi.modules.device.vo;
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -20,29 +16,16 @@ public class UserShowDeviceListVO {
|
|||||||
@Schema(description = "设备型号")
|
@Schema(description = "设备型号")
|
||||||
private String deviceType;
|
private String deviceType;
|
||||||
|
|
||||||
@Schema(description = "设备型号(board)")
|
|
||||||
private String board;
|
|
||||||
|
|
||||||
@Schema(description = "设备唯一标识符")
|
@Schema(description = "设备唯一标识符")
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
@Schema(description = "mac地址")
|
@Schema(description = "mac地址")
|
||||||
private String macAddress;
|
private String macAddress;
|
||||||
|
|
||||||
@Schema(description = "设备别名")
|
|
||||||
private String alias;
|
|
||||||
|
|
||||||
@Schema(description = "开启OTA")
|
@Schema(description = "开启OTA")
|
||||||
private Integer otaUpgrade;
|
private Integer otaUpgrade;
|
||||||
|
|
||||||
@Schema(description = "最近对话时间")
|
@Schema(description = "最近对话时间")
|
||||||
private String recentChatTime;
|
private String recentChatTime;
|
||||||
|
|
||||||
@Schema(description = "最后连接时间(UTC毫秒)")
|
|
||||||
private Long lastConnectedAtTimestamp;
|
|
||||||
|
|
||||||
@Schema(description = "绑定时间")
|
|
||||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
|
|
||||||
private Date createDate;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -167,21 +167,16 @@ public class KnowledgeFilesController {
|
|||||||
public Result<ChunkDTO.ListVO> listChunks(
|
public Result<ChunkDTO.ListVO> listChunks(
|
||||||
@PathVariable("dataset_id") String datasetId,
|
@PathVariable("dataset_id") String datasetId,
|
||||||
@PathVariable("document_id") String documentId,
|
@PathVariable("document_id") String documentId,
|
||||||
@RequestParam(name = "page", required = false, defaultValue = "1") Integer page,
|
@ParameterObject ChunkDTO.ListReq req) {
|
||||||
@RequestParam(name = "page_size", required = false, defaultValue = "10") Integer pageSize,
|
|
||||||
@RequestParam(required = false) String keywords,
|
|
||||||
@RequestParam(required = false) String id) {
|
|
||||||
|
|
||||||
// 验证权限 (内部已包含知识库存在性校验与归属权校验)
|
// 验证权限 (内部已包含知识库存在性校验与归属权校验)
|
||||||
validateKnowledgeBasePermission(datasetId);
|
validateKnowledgeBasePermission(datasetId);
|
||||||
|
|
||||||
// 构建请求对象
|
// 设置默认值
|
||||||
ChunkDTO.ListReq req = ChunkDTO.ListReq.builder()
|
if (req.getPage() == null)
|
||||||
.page(page)
|
req.setPage(1);
|
||||||
.pageSize(pageSize)
|
if (req.getPageSize() == null)
|
||||||
.keywords(keywords)
|
req.setPageSize(50);
|
||||||
.id(id)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 调用服务层获取强类型切片列表
|
// 调用服务层获取强类型切片列表
|
||||||
ChunkDTO.ListVO result = knowledgeFilesService.listChunks(datasetId, documentId, req);
|
ChunkDTO.ListVO result = knowledgeFilesService.listChunks(datasetId, documentId, req);
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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,7 +67,4 @@ public class KnowledgeBaseDTO implements Serializable {
|
|||||||
|
|
||||||
@Schema(description = "文档数量")
|
@Schema(description = "文档数量")
|
||||||
private Integer documentCount;
|
private Integer documentCount;
|
||||||
|
|
||||||
@Schema(description = "异常提示")
|
|
||||||
private String errorMessage;
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
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,11 +485,16 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer getDocumentCount(String datasetId) {
|
public Integer getDocumentCount(String datasetId) {
|
||||||
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
|
try {
|
||||||
if (info != null && info.getDocumentCount() != null) {
|
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
|
||||||
return info.getDocumentCount().intValue();
|
if (info != null && info.getDocumentCount() != null) {
|
||||||
|
return info.getDocumentCount().intValue();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取文档数量失败: {}", e.getMessage());
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -513,7 +518,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
|||||||
return null;
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage());
|
log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage());
|
||||||
throw convertToRenException(e);
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
|||||||
}
|
}
|
||||||
|
|
||||||
DatasetDTO.InfoVO datasetInfo = adapter.getDatasetInfo(dto.getDatasetId());
|
DatasetDTO.InfoVO datasetInfo = adapter.getDatasetInfo(dto.getDatasetId());
|
||||||
// getDatasetInfo 正常返回 null 表示远端确实不存在;异常时已抛出 RenException 由外层 catch 接管
|
|
||||||
if (datasetInfo == null) {
|
if (datasetInfo == null) {
|
||||||
// RAGFlow 端已确认删除 → 本地级联清理
|
// RAGFlow 端已删除 → 本地级联清理
|
||||||
log.info("数据集 {} 在 RAGFlow 端不存在,执行本地清理", dto.getDatasetId());
|
log.info("数据集 {} 在 RAGFlow 端不存在,执行本地清理", dto.getDatasetId());
|
||||||
cleanupLocalDataset(dto.getDatasetId(), dto.getId());
|
cleanupLocalDataset(dto.getDatasetId(), dto.getId());
|
||||||
// 标记为已删除,让上层从列表中移除
|
// 标记为已删除,让上层从列表中移除
|
||||||
@@ -145,9 +145,8 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
|
log.warn("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
|
||||||
dto.setDocumentCount(0);
|
dto.setDocumentCount(0);
|
||||||
dto.setErrorMessage(e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -529,8 +529,7 @@ public class KnowledgeFilesServiceImpl extends BaseServiceImpl<DocumentDao, Docu
|
|||||||
adapter.deleteDocument(datasetId, req);
|
adapter.deleteDocument(datasetId, req);
|
||||||
log.info("远程批量删除请求成功");
|
log.info("远程批量删除请求成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("远程删除请求失败,中止本地清理以避免数据不一致: {}", e.getMessage());
|
log.warn("远程删除请求部分或全部失败: {}", e.getMessage());
|
||||||
throw new RenException(e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 原子化清理本地影子记录并同步统计数据
|
// 4. 原子化清理本地影子记录并同步统计数据
|
||||||
|
|||||||
@@ -39,11 +39,9 @@ import xiaozhi.modules.model.dto.ModelProviderDTO;
|
|||||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
import xiaozhi.modules.model.service.ModelConfigService;
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
import xiaozhi.modules.model.service.ModelProviderService;
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Slf4j
|
|
||||||
public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, ModelConfigEntity>
|
public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, ModelConfigEntity>
|
||||||
implements ModelConfigService {
|
implements ModelConfigService {
|
||||||
|
|
||||||
@@ -375,8 +373,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
|
|
||||||
// 删除在新JSON中不存在的非敏感字段
|
// 删除在新JSON中不存在的非敏感字段
|
||||||
for (String oldKey : originalJson.keySet().toArray(new String[0])) {
|
for (String oldKey : originalJson.keySet().toArray(new String[0])) {
|
||||||
if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey)
|
if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey) && !SensitiveDataUtils.isSensitiveField(oldKey)) {
|
||||||
&& !SensitiveDataUtils.isSensitiveField(oldKey)) {
|
|
||||||
updatedJson.remove(oldKey);
|
updatedJson.remove(oldKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,33 +392,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
return value.contains("***");
|
return value.contains("***");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助方法:递归合并 JSON,保留原始敏感字段
|
// 辅助方法:递归合并JSON,保留原始敏感字段
|
||||||
private void mergeJson(JSONObject original, String key, JSONObject updated) {
|
private void mergeJson(JSONObject original, String key, JSONObject updated) {
|
||||||
// 空值检查
|
|
||||||
if (original == null || updated == null) {
|
|
||||||
log.warn("mergeJson: original 或 updated 为 null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果 original 中不存在 key,创建一个新的 JSON 对象
|
|
||||||
if (!original.containsKey(key)) {
|
if (!original.containsKey(key)) {
|
||||||
original.put(key, new JSONObject());
|
original.put(key, new JSONObject());
|
||||||
}
|
}
|
||||||
|
JSONObject originalChild = original.getJSONObject(key);
|
||||||
// 获取 original 中的子对象
|
|
||||||
Object originalValue = original.get(key);
|
|
||||||
JSONObject originalChild;
|
|
||||||
|
|
||||||
// 检查 originalValue 是否是 JSONObject 类型
|
|
||||||
if (originalValue instanceof JSONObject) {
|
|
||||||
originalChild = (JSONObject) originalValue;
|
|
||||||
} else {
|
|
||||||
// 如果不是 JSONObject 类型,记录警告并创建新的 JSON 对象
|
|
||||||
log.warn("mergeJson: key '{}' 的值不是 JSONObject 类型 (实际类型:{}),将创建新对象",
|
|
||||||
key, originalValue != null ? originalValue.getClass().getSimpleName() : "null");
|
|
||||||
originalChild = new JSONObject();
|
|
||||||
original.put(key, originalChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String childKey : updated.keySet()) {
|
for (String childKey : updated.keySet()) {
|
||||||
Object childValue = updated.get(childKey);
|
Object childValue = updated.get(childKey);
|
||||||
@@ -435,7 +411,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除在新 JSON 中不存在的非敏感子字段
|
// 删除在新JSON中不存在的非敏感子字段
|
||||||
for (String oldChildKey : originalChild.keySet().toArray(new String[0])) {
|
for (String oldChildKey : originalChild.keySet().toArray(new String[0])) {
|
||||||
if (!updated.containsKey(oldChildKey) && !SensitiveDataUtils.isSensitiveField(oldChildKey)) {
|
if (!updated.containsKey(oldChildKey) && !SensitiveDataUtils.isSensitiveField(oldChildKey)) {
|
||||||
originalChild.remove(oldChildKey);
|
originalChild.remove(oldChildKey);
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ public class ShiroConfig {
|
|||||||
filterMap.put("/user/retrieve-password", "anon");
|
filterMap.put("/user/retrieve-password", "anon");
|
||||||
// 将config路径使用server服务过滤器
|
// 将config路径使用server服务过滤器
|
||||||
filterMap.put("/config/**", "server");
|
filterMap.put("/config/**", "server");
|
||||||
filterMap.put("/device/address-book/call", "server");
|
|
||||||
filterMap.put("/agent/chat-history/report", "server");
|
filterMap.put("/agent/chat-history/report", "server");
|
||||||
filterMap.put("/agent/chat-history/download/**", "anon");
|
filterMap.put("/agent/chat-history/download/**", "anon");
|
||||||
filterMap.put("/agent/chat-summary/**", "server");
|
filterMap.put("/agent/chat-summary/**", "server");
|
||||||
|
|||||||
@@ -114,12 +114,7 @@ public class SysParamsController {
|
|||||||
// 校验mqtt密钥长度
|
// 校验mqtt密钥长度
|
||||||
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
|
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
// 如果是系统功能菜单配置,使用特殊处理
|
sysParamsService.update(dto);
|
||||||
if (Constant.SYSTEM_WEB_MENU.equals(dto.getParamCode())) {
|
|
||||||
sysParamsService.updateSystemWebMenu(dto.getParamValue());
|
|
||||||
} else {
|
|
||||||
sysParamsService.update(dto);
|
|
||||||
}
|
|
||||||
configService.getConfig(false);
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,19 +53,4 @@ public interface SysParamsService extends BaseService<SysParamsEntity> {
|
|||||||
* 初始化服务器密钥
|
* 初始化服务器密钥
|
||||||
*/
|
*/
|
||||||
void initServerSecret();
|
void initServerSecret();
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取系统功能菜单配置
|
|
||||||
*
|
|
||||||
* @param fromCache 是否从缓存获取
|
|
||||||
* @return 系统功能菜单配置JSON字符串
|
|
||||||
*/
|
|
||||||
String getSystemWebMenu(boolean fromCache);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新系统功能菜单配置(自动处理功能相关的插件清理)
|
|
||||||
*
|
|
||||||
* @param configJson 新的系统功能菜单配置JSON
|
|
||||||
*/
|
|
||||||
void updateSystemWebMenu(String configJson);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
|
|||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.JsonUtils;
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
import xiaozhi.common.utils.SM2Utils;
|
import xiaozhi.common.utils.SM2Utils;
|
||||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
|
||||||
import xiaozhi.modules.sys.dao.SysParamsDao;
|
import xiaozhi.modules.sys.dao.SysParamsDao;
|
||||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysParamsEntity;
|
import xiaozhi.modules.sys.entity.SysParamsEntity;
|
||||||
@@ -36,7 +35,6 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
@Service
|
@Service
|
||||||
public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParamsEntity> implements SysParamsService {
|
public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParamsEntity> implements SysParamsService {
|
||||||
private final SysParamsRedis sysParamsRedis;
|
private final SysParamsRedis sysParamsRedis;
|
||||||
private final AgentPluginMappingService agentPluginMappingService;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<SysParamsDTO> page(Map<String, Object> params) {
|
public PageData<SysParamsDTO> page(Map<String, Object> params) {
|
||||||
@@ -272,62 +270,4 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@Override
|
|
||||||
public String getSystemWebMenu(boolean fromCache) {
|
|
||||||
return getValue(Constant.SYSTEM_WEB_MENU, fromCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void updateSystemWebMenu(String configJson) {
|
|
||||||
// 获取当前配置
|
|
||||||
String currentConfig = getSystemWebMenu(false);
|
|
||||||
Map<String, Object> currentMap = null;
|
|
||||||
Map<String, Object> newMap = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (StringUtils.isNotBlank(currentConfig)) {
|
|
||||||
currentMap = JsonUtils.parseObject(currentConfig, Map.class);
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(configJson)) {
|
|
||||||
newMap = JsonUtils.parseObject(configJson, Map.class);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查addressBook功能是否被关闭
|
|
||||||
if (currentMap != null && newMap != null) {
|
|
||||||
Map<String, Object> currentFeatures = (Map<String, Object>) currentMap.get("features");
|
|
||||||
Map<String, Object> newFeatures = (Map<String, Object>) newMap.get("features");
|
|
||||||
|
|
||||||
if (currentFeatures != null && newFeatures != null) {
|
|
||||||
Object currentAddressBookObj = currentFeatures.get("addressBook");
|
|
||||||
Object newAddressBookObj = newFeatures.get("addressBook");
|
|
||||||
|
|
||||||
Boolean currentEnabled = false;
|
|
||||||
Boolean newEnabled = false;
|
|
||||||
|
|
||||||
if (currentAddressBookObj instanceof Map) {
|
|
||||||
Map<String, Object> currentAddressBook = (Map<String, Object>) currentAddressBookObj;
|
|
||||||
currentEnabled = currentAddressBook.get("enabled") != null
|
|
||||||
? (Boolean) currentAddressBook.get("enabled") : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newAddressBookObj instanceof Map) {
|
|
||||||
Map<String, Object> newAddressBook = (Map<String, Object>) newAddressBookObj;
|
|
||||||
newEnabled = newAddressBook.get("enabled") != null
|
|
||||||
? (Boolean) newAddressBook.get("enabled") : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果之前是启用状态,现在被禁用,删除所有call_device插件
|
|
||||||
if (Boolean.TRUE.equals(currentEnabled) && !Boolean.TRUE.equals(newEnabled)) {
|
|
||||||
agentPluginMappingService.deleteByPluginId("SYSTEM_PLUGIN_CALL_DEVICE");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新配置
|
|
||||||
updateValueByCode(Constant.SYSTEM_WEB_MENU, configJson);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ spring:
|
|||||||
druid:
|
druid:
|
||||||
#MySQL
|
#MySQL
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
url: jdbc:mysql://127.0.0.1:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&nullCatalogMeansCurrent=true
|
url: jdbc:mysql://127.0.0.1:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
|
||||||
username: root
|
username: root
|
||||||
password: 123456
|
password: 123456
|
||||||
initial-size: 10
|
initial-size: 10
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
-- 新增呼叫设备工具配置
|
|
||||||
SET @data_exists = (SELECT COUNT(*) FROM ai_model_provider WHERE id = 'SYSTEM_PLUGIN_CALL_DEVICE');
|
|
||||||
SET @sql = IF(@data_exists = 0,
|
|
||||||
'INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`,
|
|
||||||
`update_date`) VALUES (''SYSTEM_PLUGIN_CALL_DEVICE'', ''Plugin'', ''call_device'', ''设备呼叫设备'', ''[]'', 85, 1988490863118454785, ''2026-05-18
|
|
||||||
12:00:00'', 1988490863118454785, ''2026-05-18 12:00:00'')',
|
|
||||||
'SELECT ''data already exists, skip'' AS msg');
|
|
||||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- 更新系统菜单配置,增加联系人管理菜单
|
|
||||||
UPDATE sys_params
|
|
||||||
SET param_value = CAST(
|
|
||||||
JSON_SET(
|
|
||||||
CAST(param_value AS JSON),
|
|
||||||
'$.addressBook',
|
|
||||||
JSON_OBJECT(
|
|
||||||
'name', 'feature.addressBook.name',
|
|
||||||
'enabled', FALSE,
|
|
||||||
'description', 'feature.addressBook.description'
|
|
||||||
)
|
|
||||||
) AS CHAR
|
|
||||||
)
|
|
||||||
WHERE param_code = 'system-web.menu'
|
|
||||||
AND NOT JSON_CONTAINS_PATH(CAST(param_value AS JSON), 'one', '$.addressBook');
|
|
||||||
|
|
||||||
-- 创建设备通讯录表
|
|
||||||
CREATE TABLE IF NOT EXISTS `ai_device_address_book` (
|
|
||||||
`mac_address` VARCHAR(64) NOT NULL COMMENT '本设备MAC地址',
|
|
||||||
`target_mac` VARCHAR(64) NOT NULL COMMENT '对方设备MAC地址',
|
|
||||||
`alias` VARCHAR(64) DEFAULT NULL COMMENT '别名',
|
|
||||||
`has_permission` TINYINT(1) DEFAULT TRUE COMMENT '是否有权限呼叫',
|
|
||||||
`creator` BIGINT DEFAULT NULL COMMENT '创建人',
|
|
||||||
`create_date` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
`updater` BIGINT DEFAULT NULL COMMENT '更新人',
|
|
||||||
`update_date` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (`mac_address`, `target_mac`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备通讯录表';
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
-- 更新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';
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
-- 修正、添加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);
|
|
||||||
@@ -669,24 +669,3 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202605121509.sql
|
path: classpath:db/changelog/202605121509.sql
|
||||||
- changeSet:
|
|
||||||
id: 202605251426
|
|
||||||
author: RanChen
|
|
||||||
changes:
|
|
||||||
- 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
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="xiaozhi.modules.device.dao.DeviceAddressBookDao">
|
|
||||||
|
|
||||||
<select id="getAddressBookList" resultType="xiaozhi.modules.device.entity.DeviceAddressBookEntity">
|
|
||||||
SELECT mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date
|
|
||||||
FROM ai_device_address_book
|
|
||||||
WHERE mac_address = #{macAddress}
|
|
||||||
ORDER BY update_date DESC
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<update id="updateAlias">
|
|
||||||
UPDATE ai_device_address_book
|
|
||||||
SET alias = #{alias}, update_date = NOW()
|
|
||||||
WHERE mac_address = #{macAddress} AND target_mac = #{targetMac}
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<update id="updatePermission">
|
|
||||||
UPDATE ai_device_address_book
|
|
||||||
SET has_permission = #{hasPermission}, update_date = NOW()
|
|
||||||
WHERE mac_address = #{macAddress} AND target_mac = #{targetMac}
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<insert id="insert">
|
|
||||||
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>
|
|
||||||
@@ -265,7 +265,7 @@ function showAbout() {
|
|||||||
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
|
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
|
||||||
content: t('settings.aboutContent', {
|
content: t('settings.aboutContent', {
|
||||||
appName: import.meta.env.VITE_APP_TITLE,
|
appName: import.meta.env.VITE_APP_TITLE,
|
||||||
version: '0.9.5'
|
version: '0.9.3'
|
||||||
}),
|
}),
|
||||||
showCancel: false,
|
showCancel: false,
|
||||||
confirmText: t('common.confirm'),
|
confirmText: t('common.confirm'),
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import voiceClone from './module/voiceClone.js'
|
|||||||
import voiceResource from './module/voiceResource.js'
|
import voiceResource from './module/voiceResource.js'
|
||||||
import knowledgeBase from './module/knowledgeBase.js'
|
import knowledgeBase from './module/knowledgeBase.js'
|
||||||
import correctWord from './module/correctWord.js'
|
import correctWord from './module/correctWord.js'
|
||||||
import addressBook from './module/addressBook.js'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +43,5 @@ export default {
|
|||||||
voiceResource,
|
voiceResource,
|
||||||
voiceClone,
|
voiceClone,
|
||||||
knowledgeBase,
|
knowledgeBase,
|
||||||
correctWord,
|
correctWord
|
||||||
addressBook
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { getServiceUrl } from '../api';
|
|
||||||
import RequestService from '../httpRequest';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
/**
|
|
||||||
* 获取设备通讯录列表
|
|
||||||
*/
|
|
||||||
getAddressBookList(macAddress, callback) {
|
|
||||||
RequestService.sendRequest()
|
|
||||||
.url(`${getServiceUrl()}/device/address-book/${macAddress}`)
|
|
||||||
.method('GET')
|
|
||||||
.success((res) => {
|
|
||||||
RequestService.clearRequestTime();
|
|
||||||
callback(res);
|
|
||||||
})
|
|
||||||
.networkFail(() => {
|
|
||||||
RequestService.reAjaxFun(() => {
|
|
||||||
this.getAddressBookList(macAddress, callback);
|
|
||||||
});
|
|
||||||
}).send();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新设备通讯录别名
|
|
||||||
*/
|
|
||||||
updateAlias(data, callback) {
|
|
||||||
RequestService.sendRequest()
|
|
||||||
.url(`${getServiceUrl()}/device/address-book/alias`)
|
|
||||||
.method('PUT')
|
|
||||||
.data(data)
|
|
||||||
.success((res) => {
|
|
||||||
RequestService.clearRequestTime();
|
|
||||||
callback(res);
|
|
||||||
})
|
|
||||||
.networkFail(() => {
|
|
||||||
RequestService.reAjaxFun(() => {
|
|
||||||
this.updateAlias(data, callback);
|
|
||||||
});
|
|
||||||
}).send();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新设备通讯录权限
|
|
||||||
*/
|
|
||||||
updatePermission(data, callback) {
|
|
||||||
RequestService.sendRequest()
|
|
||||||
.url(`${getServiceUrl()}/device/address-book/permission`)
|
|
||||||
.method('PUT')
|
|
||||||
.data(data)
|
|
||||||
.success((res) => {
|
|
||||||
RequestService.clearRequestTime();
|
|
||||||
callback(res);
|
|
||||||
})
|
|
||||||
.networkFail(() => {
|
|
||||||
RequestService.reAjaxFun(() => {
|
|
||||||
this.updatePermission(data, callback);
|
|
||||||
});
|
|
||||||
}).send();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -80,7 +80,7 @@ export default {
|
|||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
// 保存
|
// 保存
|
||||||
addParam(data, callback, failCallback) {
|
addParam(data, callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/admin/params`)
|
.url(`${getServiceUrl()}/admin/params`)
|
||||||
.method('POST')
|
.method('POST')
|
||||||
@@ -89,10 +89,6 @@ export default {
|
|||||||
RequestService.clearRequestTime()
|
RequestService.clearRequestTime()
|
||||||
callback(res)
|
callback(res)
|
||||||
})
|
})
|
||||||
.fail((err) => {
|
|
||||||
RequestService.clearRequestTime()
|
|
||||||
failCallback(err)
|
|
||||||
})
|
|
||||||
.networkFail((err) => {
|
.networkFail((err) => {
|
||||||
console.error('添加参数失败:', err)
|
console.error('添加参数失败:', err)
|
||||||
RequestService.reAjaxFun(() => {
|
RequestService.reAjaxFun(() => {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 659 B |
|
Before Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 913 B |
|
Before Width: | Height: | Size: 769 B |
|
Before Width: | Height: | Size: 889 B |
|
Before Width: | Height: | Size: 995 B |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 669 B |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 682 B |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB |
@@ -1,38 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<CustomDialog
|
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="57%" center
|
||||||
:visible.sync="dialogVisible"
|
custom-class="custom-dialog" :show-close="false" class="center-dialog">
|
||||||
:title="$t('modelConfigDialog.addModel')"
|
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||||
width="57%"
|
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||||
class="add-model-dialog"
|
{{ $t('modelConfigDialog.addModel') }}
|
||||||
:confirmLoading="saving"
|
</div>
|
||||||
@close="handleClose"
|
|
||||||
@confirm="confirm"
|
<button class="custom-close-btn" @click="handleClose">
|
||||||
>
|
×
|
||||||
<div class="dialog-content">
|
</button>
|
||||||
|
|
||||||
<!-- 模型信息部分 -->
|
<!-- 模型信息部分 -->
|
||||||
<div class="section-header">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<div class="section-title">{{ $t('modelConfigDialog.modelInfo') }}</div>
|
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">{{ $t('modelConfigDialog.modelInfo') }}</div>
|
||||||
<div class="switch-group">
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
<div class="switch-item">
|
<div style="display: flex; align-items: center;">
|
||||||
<span>{{ $t('modelConfigDialog.enable') }}</span>
|
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.enable') }}</span>
|
||||||
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="switch-item" style="display: none;">
|
<div style="display: none; align-items: center;">
|
||||||
<span>{{ $t('modelConfigDialog.setDefault') }}</span>
|
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.setDefault') }}</span>
|
||||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||||
<el-form :model="formData" label-width="auto" label-position="left" class="custom-form">
|
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||||
<div class="form-row">
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item :label="$t('modelConfigDialog.modelId')" prop="id" style="flex: 1;">
|
<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"
|
<el-input v-model="formData.id" :placeholder="$t('modelConfigDialog.enterModelId')" class="custom-input-bg"
|
||||||
maxlength="32"></el-input>
|
maxlength="32"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
|
||||||
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
|
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
|
||||||
class="custom-input-bg"></el-input>
|
class="custom-input-bg"></el-input>
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
||||||
<el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')"
|
<el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')"
|
||||||
class="custom-select custom-input-bg" style="width: 100%;" @focus="loadProviders" filterable>
|
class="custom-select custom-input-bg" style="width: 100%;" @focus="loadProviders" filterable>
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
|
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
|
||||||
<el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
|
<el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
|
||||||
class="custom-input-bg"></el-input>
|
class="custom-input-bg"></el-input>
|
||||||
@@ -68,13 +70,13 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<teleport v-if="chunkedCallInfoFields.length">
|
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{
|
||||||
<div class="section-title">{{ $t('modelConfigDialog.callInfo') }}</div>
|
$t('modelConfigDialog.callInfo') }}</div>
|
||||||
<div class="divider"></div>
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||||
</teleport>
|
|
||||||
|
|
||||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||||
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex" class="form-row">
|
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex"
|
||||||
|
style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop" style="flex: 1;">
|
<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"
|
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
|
||||||
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
|
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
|
||||||
@@ -83,17 +85,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</CustomDialog>
|
|
||||||
|
<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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import CustomDialog from './CustomDialog.vue';
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AddModelDialog',
|
name: 'AddModelDialog',
|
||||||
components: {
|
|
||||||
CustomDialog
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
visible: { type: Boolean, required: true },
|
visible: { type: Boolean, required: true },
|
||||||
modelType: { type: String, required: true }
|
modelType: { type: String, required: true }
|
||||||
@@ -174,10 +178,16 @@ export default {
|
|||||||
});
|
});
|
||||||
this.formData.configJson = { ...defaultConfig };
|
this.formData.configJson = { ...defaultConfig };
|
||||||
},
|
},
|
||||||
|
handleVisibleChange(val) {
|
||||||
|
this.dialogVisible = val;
|
||||||
|
this.$emit('update:visible', val);
|
||||||
|
if (!val) {
|
||||||
|
this.resetForm();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
handleClose() {
|
handleClose() {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.dialogVisible = false;
|
|
||||||
this.$emit('update:visible', false);
|
this.$emit('update:visible', false);
|
||||||
},
|
},
|
||||||
initDynamicConfig() {
|
initDynamicConfig() {
|
||||||
@@ -222,7 +232,7 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.$emit('confirm', submitData);
|
this.$emit('confirm', submitData);
|
||||||
this.handleClose();
|
this.$emit('update:visible', false);
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -287,71 +297,134 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style>
|
||||||
@import '@/styles/global.scss';
|
.custom-dialog {
|
||||||
|
position: relative;
|
||||||
::v-deep .el-dialog {
|
border-radius: 20px;
|
||||||
margin-top: 6vh !important;
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
padding-bottom: 17px;
|
||||||
}
|
}
|
||||||
::v-deep .el-dialog__body {
|
|
||||||
max-height: 60vh;
|
.custom-dialog .el-dialog__header {
|
||||||
overflow-y: auto;
|
padding: 0;
|
||||||
@include scrollbar-style;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.add-model-dialog {
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
.center-dialog {
|
||||||
margin-bottom: 10px;
|
display: flex;
|
||||||
font-size: 20px;
|
align-items: center;
|
||||||
font-weight: bold;
|
justify-content: center;
|
||||||
color: #3d4566;
|
}
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-group {
|
.center-dialog .el-dialog {
|
||||||
display: flex;
|
margin: 0 0 auto !important;
|
||||||
align-items: center;
|
display: flex;
|
||||||
gap: 20px;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-item {
|
.custom-close-btn {
|
||||||
display: flex;
|
position: absolute;
|
||||||
align-items: center;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
.custom-close-btn:hover {
|
||||||
margin-right: 8px;
|
color: #409EFF;
|
||||||
}
|
border-color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.custom-select .el-input__suffix {
|
||||||
height: 1px;
|
background: #e6e8ea;
|
||||||
background: #e9e9e9;
|
right: 6px;
|
||||||
margin-bottom: 16px;
|
width: 20px;
|
||||||
}
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-row {
|
.custom-select .el-input__suffix-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
align-items: center;
|
||||||
margin-bottom: 0;
|
justify-content: center;
|
||||||
}
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.custom-select .el-icon-arrow-up:before {
|
||||||
display: flex;
|
content: "";
|
||||||
justify-content: center;
|
display: inline-block;
|
||||||
gap: 20px;
|
width: 0;
|
||||||
padding: 16px 20px;
|
height: 0;
|
||||||
}
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
border-top: 7px solid #c0c4cc;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-form .el-form-item {
|
.custom-form .el-form-item {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.custom-switch .el-switch__core {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
height: 23px;
|
height: 23px;
|
||||||
@@ -366,7 +439,7 @@ export default {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
left: 4px;
|
left: 4px;
|
||||||
transition: all 0.3s;
|
transition: all .3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-switch.is-checked .el-switch__core {
|
.custom-switch.is-checked .el-switch__core {
|
||||||
@@ -381,10 +454,12 @@ export default {
|
|||||||
background-color: #1b47ee;
|
background-color: #1b47ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[style*="display: flex"] {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.custom-input-bg .el-input__inner {
|
.custom-input-bg .el-input__inner {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
::v-deep .el-input__inner {
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog :title="title" :visible="dialogVisible" width="600px" class="address-book-dialog" @close="handleClose">
|
|
||||||
<el-form ref="addressBookForm" :model="form" :rules="rules" label-width="100px" size="medium">
|
|
||||||
<el-form-item :label="$t('addressBookDialog.name')" prop="name">
|
|
||||||
<el-input v-model="form.name" :placeholder="$t('addressBookDialog.namePlaceholder')" clearable></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('addressBookDialog.description')" prop="description">
|
|
||||||
<el-input v-model="form.description" :placeholder="$t('addressBookDialog.descriptionPlaceholder')"
|
|
||||||
type="textarea" :rows="4" maxlength="300" show-word-limit></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('addressBookDialog.status')" prop="status">
|
|
||||||
<el-switch
|
|
||||||
v-model="form.status"
|
|
||||||
:active-value="1"
|
|
||||||
:inactive-value="0"
|
|
||||||
active-color="#13ce66"
|
|
||||||
inactive-color="#909399"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<div slot="footer" class="dialog-footer">
|
|
||||||
<el-button @click="handleClose">{{ $t('addressBookDialog.cancel') }}</el-button>
|
|
||||||
<el-button type="primary" @click="handleSubmit">{{ $t('addressBookDialog.confirm') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "AddressBookDialog",
|
|
||||||
props: {
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: ""
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
id: null,
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
status: 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
dialogVisible: this.visible,
|
|
||||||
rules: {
|
|
||||||
name: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: this.$t("addressBookDialog.nameRequired"),
|
|
||||||
trigger: "blur"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
min: 1,
|
|
||||||
max: 50,
|
|
||||||
message: this.$t("addressBookDialog.nameLength"),
|
|
||||||
trigger: "blur"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: /^[一-龥a-zA-Z0-9\s-_]+$/,
|
|
||||||
message: this.$t("addressBookDialog.namePattern"),
|
|
||||||
trigger: "blur"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
description: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: this.$t("addressBookDialog.descriptionRequired"),
|
|
||||||
trigger: "blur"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
max: 300,
|
|
||||||
message: this.$t("addressBookDialog.descriptionLength"),
|
|
||||||
trigger: "blur"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
visible(val) {
|
|
||||||
this.dialogVisible = val;
|
|
||||||
if (val) {
|
|
||||||
if (this.$refs.addressBookForm) {
|
|
||||||
this.$refs.addressBookForm.clearValidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleClose() {
|
|
||||||
if (this.$refs.addressBookForm) {
|
|
||||||
this.$refs.addressBookForm.clearValidate();
|
|
||||||
}
|
|
||||||
this.dialogVisible = false;
|
|
||||||
this.$emit("update:visible", false);
|
|
||||||
},
|
|
||||||
handleSubmit() {
|
|
||||||
this.$refs.addressBookForm.validate(valid => {
|
|
||||||
if (valid) {
|
|
||||||
this.$emit("submit", {
|
|
||||||
...this.form
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.address-book-dialog {
|
|
||||||
::v-deep .el-dialog {
|
|
||||||
border-radius: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-dialog__body {
|
|
||||||
padding: 20px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-form-item {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-form-item__label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #34495e;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-input {
|
|
||||||
.el-input__inner {
|
|
||||||
height: 36px;
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: #5f70f3;
|
|
||||||
box-shadow: 0 0 0 2px rgba(95, 112, 243, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-textarea {
|
|
||||||
.el-textarea__inner {
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: #5f70f3;
|
|
||||||
box-shadow: 0 0 0 2px rgba(95, 112, 243, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,42 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<form>
|
<form>
|
||||||
<CustomDialog
|
<el-dialog :visible.sync="dialogVisible" width="24%" center>
|
||||||
:title="$t('changePassword.title')"
|
<div
|
||||||
:visible.sync="dialogVisible"
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
width="600px"
|
<div
|
||||||
@cancel="cancel"
|
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||||
@confirm="confirm">
|
<img loading="lazy" src="@/assets/login/shield.png" alt=""
|
||||||
<div class="password-form">
|
style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||||
<div class="form-label">
|
</div>
|
||||||
<span class="required">*</span>
|
{{ $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>
|
||||||
{{ $t('changePassword.oldPasswordLabel') }}
|
{{ $t('changePassword.oldPasswordLabel') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input :placeholder="$t('changePassword.oldPasswordPlaceholder')" v-model="oldPassword" type="password" show-password />
|
<el-input :placeholder="$t('changePassword.oldPasswordPlaceholder')" v-model="oldPassword" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-label" style="margin-top: 12px;">
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||||
<span class="required">*</span>
|
<div style="color: red;display: inline-block;">*</div>
|
||||||
{{ $t('changePassword.newPasswordLabel') }}
|
{{ $t('changePassword.newPasswordLabel') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input :placeholder="$t('changePassword.newPasswordPlaceholder')" v-model="newPassword" type="password" show-password />
|
<el-input :placeholder="$t('changePassword.newPasswordPlaceholder')" v-model="newPassword" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-label" style="margin-top: 12px;">
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||||
<span class="required">*</span>
|
<div style="color: red;display: inline-block;">*</div>
|
||||||
{{ $t('changePassword.confirmPasswordLabel') }}
|
{{ $t('changePassword.confirmPasswordLabel') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input :placeholder="$t('changePassword.confirmPasswordPlaceholder')" v-model="confirmNewPassword" type="password" show-password />
|
<el-input :placeholder="$t('changePassword.confirmPasswordPlaceholder')" v-model="confirmNewPassword" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CustomDialog>
|
<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>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import userApi from '@/apis/module/user';
|
import userApi from '@/apis/module/user';
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import CustomDialog from '@/components/CustomDialog.vue';
|
import { changeLanguage } from '@/i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChangePasswordDialog',
|
name: 'ChangePasswordDialog',
|
||||||
@@ -54,9 +67,6 @@ export default {
|
|||||||
confirmNewPassword: ""
|
confirmNewPassword: ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
CustomDialog
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
value(val) {
|
value(val) {
|
||||||
this.dialogVisible = val;
|
this.dialogVisible = val;
|
||||||
@@ -90,6 +100,7 @@ export default {
|
|||||||
});
|
});
|
||||||
this.logout().then(() => {
|
this.logout().then(() => {
|
||||||
this.$router.push('/login');
|
this.$router.push('/login');
|
||||||
|
this.$emit('update:visible', false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(res.data.msg || this.$t('changePassword.changeFailed'));
|
this.$message.error(res.data.msg || this.$t('changePassword.changeFailed'));
|
||||||
@@ -97,9 +108,10 @@ export default {
|
|||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.$message.error(err.msg || this.$t('changePassword.changeFailed'));
|
this.$message.error(err.msg || this.$t('changePassword.changeFailed'));
|
||||||
});
|
});
|
||||||
this.dialogVisible = false;
|
this.$emit('input', false);
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
|
this.dialogVisible = false;
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
@@ -112,18 +124,38 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.form-label {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required {
|
|
||||||
color: red;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-46 {
|
.input-46 {
|
||||||
background: #f6f8fb;
|
background: #f6f8fb;
|
||||||
border-radius: 15px;
|
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>
|
</style>
|
||||||
@@ -1,19 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<CustomDialog
|
<el-dialog
|
||||||
:title="$t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle')"
|
:title="$t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle') + (currentMacAddress ? '[' + currentMacAddress + ']' : '')"
|
||||||
:visible.sync="dialogVisible"
|
:visible.sync="dialogVisible" width="80%" :before-close="handleClose" custom-class="chat-history-dialog">
|
||||||
width="80%"
|
|
||||||
:footer="false"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
custom-class="chat-history-dialog">
|
|
||||||
<template v-slot:title>
|
|
||||||
<span style="font-size: 18px;">
|
|
||||||
{{ $t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle') }}
|
|
||||||
<template v-if="currentMacAddress">
|
|
||||||
[<MacAddressMask :macAddress="currentMacAddress" />]
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="session-list" @scroll="handleScroll">
|
<div class="session-list" @scroll="handleScroll">
|
||||||
<div v-for="session in sessions" :key="session.sessionId" class="session-item"
|
<div v-for="session in sessions" :key="session.sessionId" class="session-item"
|
||||||
@@ -81,14 +69,12 @@
|
|||||||
{{ $t('chatHistory.downloadCurrentSession') }}
|
{{ $t('chatHistory.downloadCurrentSession') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</CustomDialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { debounce } from '@/utils'
|
import { debounce } from '@/utils'
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import CustomDialog from '@/components/CustomDialog.vue';
|
|
||||||
import MacAddressMask from '@/components/MacAddressMask.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChatHistoryDialog',
|
name: 'ChatHistoryDialog',
|
||||||
@@ -124,10 +110,6 @@ export default {
|
|||||||
expandedToolResults: {} // 跟踪工具结果的展开状态
|
expandedToolResults: {} // 跟踪工具结果的展开状态
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
CustomDialog,
|
|
||||||
MacAddressMask,
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
visible(val) {
|
visible(val) {
|
||||||
this.dialogVisible = val;
|
this.dialogVisible = val;
|
||||||
@@ -247,6 +229,9 @@ export default {
|
|||||||
this.isFirstLoad = true;
|
this.isFirstLoad = true;
|
||||||
this.expandedToolResults = {};
|
this.expandedToolResults = {};
|
||||||
},
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.dialogVisible = false;
|
||||||
|
},
|
||||||
loadSessions() {
|
loadSessions() {
|
||||||
if (this.loading || (!this.isFirstLoad && !this.hasMore)) {
|
if (this.loading || (!this.isFirstLoad && !this.hasMore)) {
|
||||||
return;
|
return;
|
||||||
@@ -638,6 +623,11 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-history-dialog .el-dialog__header {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-history-dialog .el-dialog__body {
|
.chat-history-dialog .el-dialog__body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-button
|
|
||||||
:class="['custom-button', `custom-button--${type}`]"
|
|
||||||
v-bind="$attrs"
|
|
||||||
@click="$emit('click', $event)"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'CustomButton',
|
|
||||||
props: {
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: 'default',
|
|
||||||
validator: (val) => ['default', 'confirm', 'delete', 'add'].includes(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.custom-button--default {
|
|
||||||
width: fit-content;
|
|
||||||
background: #4998ff;
|
|
||||||
color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&.el-button--small {
|
|
||||||
padding: 9px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
&.el-button--medium {
|
|
||||||
padding: 12px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #62a4fc;
|
|
||||||
color: #ffffff;
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: 0 2px 8px rgba(74, 124, 253, 0.2);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-disabled,
|
|
||||||
&.is-disabled:hover {
|
|
||||||
background: linear-gradient(to right, #f0f3fe, #f5f6fe);
|
|
||||||
color: rgba(74, 124, 253, 0.5);
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
transform: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-button--confirm {
|
|
||||||
width: fit-content;
|
|
||||||
background: linear-gradient(to right, #4a7cfd, #8154fc);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&.el-button--small {
|
|
||||||
padding: 9px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
&.el-button--medium {
|
|
||||||
padding: 12px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: 0 2px 8px rgba(74, 124, 253, 0.3);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-disabled,
|
|
||||||
&.is-disabled:hover {
|
|
||||||
background: linear-gradient(to right, #a0b4fd, #b8a4fd);
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
transform: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-button--delete {
|
|
||||||
width: fit-content;
|
|
||||||
background: #ff6b6b;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&.el-button--small {
|
|
||||||
padding: 9px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
&.el-button--medium {
|
|
||||||
padding: 12px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-disabled,
|
|
||||||
&.is-disabled:hover {
|
|
||||||
background: linear-gradient(to right, #ffaaaa, #ffcaca);
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
transform: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-button--add {
|
|
||||||
width: fit-content;
|
|
||||||
background: #52c41a;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&.el-button--small {
|
|
||||||
padding: 9px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
&.el-button--medium {
|
|
||||||
padding: 12px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-disabled,
|
|
||||||
&.is-disabled:hover {
|
|
||||||
background: linear-gradient(to right, #a0e89a, #c4f0c0);
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
transform: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
:title="title"
|
|
||||||
:visible.sync="dialogVisible"
|
|
||||||
:width="width"
|
|
||||||
:close-on-click-modal="closeOnClickModal"
|
|
||||||
:close-on-press-escape="closeOnPressEscape"
|
|
||||||
:show-close="showClose"
|
|
||||||
:destroy-on-close="destroyOnClose"
|
|
||||||
:custom-class="customClass"
|
|
||||||
class="custom-dialog"
|
|
||||||
@close="handleClose"
|
|
||||||
@open="handleOpen"
|
|
||||||
>
|
|
||||||
<template slot="title">
|
|
||||||
<slot name="title">
|
|
||||||
<div class="dialog-title">
|
|
||||||
<img src="@/assets/knowledge-base/level.png" class="title-icon" />
|
|
||||||
<span>{{ title }}</span>
|
|
||||||
</div>
|
|
||||||
</slot>
|
|
||||||
</template>
|
|
||||||
<slot></slot>
|
|
||||||
<template slot="footer">
|
|
||||||
<div v-if="footer" class="dialog-footer">
|
|
||||||
<CustomButton @click="handleCancel">{{ cancelText }}</CustomButton>
|
|
||||||
<CustomButton :loading="confirmLoading" type="confirm" @click="handleConfirm">
|
|
||||||
<span class="confirm-inner">
|
|
||||||
<img src="@/assets/knowledge-base/star.png" class="confirm-icon" />
|
|
||||||
{{ confirmText }}
|
|
||||||
</span>
|
|
||||||
</CustomButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import CustomButton from './CustomButton.vue';
|
|
||||||
export default {
|
|
||||||
name: "CustomDialog",
|
|
||||||
props: {
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: ""
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
confirmLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: String,
|
|
||||||
default: "600px"
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
closeOnClickModal: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
closeOnPressEscape: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showClose: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
destroyOnClose: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
customClass: {
|
|
||||||
type: String,
|
|
||||||
default: ""
|
|
||||||
},
|
|
||||||
cancelText: {
|
|
||||||
type: String,
|
|
||||||
default: "取消"
|
|
||||||
},
|
|
||||||
confirmText: {
|
|
||||||
type: String,
|
|
||||||
default: "确认保存"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
dialogVisible: this.visible
|
|
||||||
};
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
CustomButton
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
visible(val) {
|
|
||||||
this.dialogVisible = val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleClose() {
|
|
||||||
this.dialogVisible = false;
|
|
||||||
this.$emit("update:visible", false);
|
|
||||||
this.$emit("close");
|
|
||||||
},
|
|
||||||
handleOpen() {
|
|
||||||
this.$emit("open");
|
|
||||||
},
|
|
||||||
handleCancel() {
|
|
||||||
this.dialogVisible = false;
|
|
||||||
this.$emit("update:visible", false);
|
|
||||||
this.$emit("cancel");
|
|
||||||
},
|
|
||||||
handleConfirm() {
|
|
||||||
this.$emit("confirm");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.custom-dialog {
|
|
||||||
::v-deep .el-dialog {
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-dialog__header {
|
|
||||||
padding: 16px 20px 12px;
|
|
||||||
background: linear-gradient(135deg, #e2eeff, #edeafe);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-dialog__title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-title {
|
|
||||||
font-size: 18px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
> span {
|
|
||||||
line-height: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-dialog__headerbtn {
|
|
||||||
top: 12px;
|
|
||||||
right: 16px;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.el-dialog__close {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #666;
|
|
||||||
position: static;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
|
||||||
|
|
||||||
.el-dialog__close {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-dialog__body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-dialog__footer {
|
|
||||||
padding: 12px 20px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--primary {
|
|
||||||
background: linear-gradient(to right, #4a7cfd, #8154fc);
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background: linear-gradient(to right, #4a7cfd, #8154fc);
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-inner {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-icon {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="pagination-wrapper">
|
|
||||||
<el-pagination
|
|
||||||
:current-page="currentPage"
|
|
||||||
:page-size="pageSize"
|
|
||||||
:page-sizes="pageSizeOptions"
|
|
||||||
:total="total"
|
|
||||||
layout="sizes, prev, pager, next, total"
|
|
||||||
background
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'Pagination',
|
|
||||||
props: {
|
|
||||||
total: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
currentPage: {
|
|
||||||
type: Number,
|
|
||||||
default: 1
|
|
||||||
},
|
|
||||||
pageSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 10
|
|
||||||
},
|
|
||||||
pageSizeOptions: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [10, 20, 50, 100]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleSizeChange(val) {
|
|
||||||
this.$emit('size-change', val);
|
|
||||||
},
|
|
||||||
handlePageChange(page) {
|
|
||||||
this.$emit('page-change', page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
::v-deep .el-pagination {
|
|
||||||
.btn-next,
|
|
||||||
.btn-prev,
|
|
||||||
.el-pager li {
|
|
||||||
background: transparent !important;
|
|
||||||
min-width: 36px !important;
|
|
||||||
}
|
|
||||||
.el-pager li:not(.disabled).active {
|
|
||||||
background: linear-gradient(135deg, #2a6dfe 0%, #6b8cff 50%, #2a6dfe 100%) !important;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 1px 3px rgba(45, 111, 254, 0.3);
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="custom-table-wrapper">
|
|
||||||
<div class="table-container" :style="{ height: tableContainerHeight }">
|
|
||||||
<el-table
|
|
||||||
ref="tableRef"
|
|
||||||
:data="data"
|
|
||||||
:class="['custom-table', tableClass]"
|
|
||||||
height="100%"
|
|
||||||
v-loading="loading"
|
|
||||||
:element-loading-text="loadingText"
|
|
||||||
:element-loading-spinner="loadingSpinner"
|
|
||||||
:element-loading-background="loadingBackground"
|
|
||||||
:header-cell-class-name="headerCellClassName"
|
|
||||||
:row-class-name="rowClassName"
|
|
||||||
@selection-change="handleSelectionChange"
|
|
||||||
@row-click="handleRowClick"
|
|
||||||
>
|
|
||||||
<!-- 选择列 -->
|
|
||||||
<el-table-column
|
|
||||||
v-if="showSelection"
|
|
||||||
width="55"
|
|
||||||
align="center"
|
|
||||||
label="选择"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<slot
|
|
||||||
v-if="$scopedSlots.selection"
|
|
||||||
name="selection"
|
|
||||||
:row="scope.row"
|
|
||||||
:$index="scope.$index"
|
|
||||||
/>
|
|
||||||
<el-checkbox
|
|
||||||
v-else
|
|
||||||
:value="scope.row.selected"
|
|
||||||
@change="handleCheckboxChange(scope.row)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<!-- 动态列 -->
|
|
||||||
<el-table-column
|
|
||||||
v-for="column in columns"
|
|
||||||
:key="column.prop"
|
|
||||||
:prop="column.prop"
|
|
||||||
:label="column.label"
|
|
||||||
:width="column.width"
|
|
||||||
:min-width="column.minWidth"
|
|
||||||
:align="column.align || 'center'"
|
|
||||||
:show-overflow-tooltip="column.showOverflowTooltip !== false"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<!-- 自定义插槽:优先使用 column.slot 指定的插槽名,否则用 column.prop 作为插槽名 -->
|
|
||||||
<slot
|
|
||||||
v-if="$scopedSlots[column.slot] || $scopedSlots[column.prop]"
|
|
||||||
:name="column.slot || column.prop"
|
|
||||||
:row="scope.row"
|
|
||||||
:$index="scope.$index"
|
|
||||||
:column="column"
|
|
||||||
/>
|
|
||||||
<!-- 默认显示 -->
|
|
||||||
<template v-else>
|
|
||||||
{{ scope.row[column.prop] }}
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<!-- 操作列 -->
|
|
||||||
<el-table-column
|
|
||||||
v-if="showOperations"
|
|
||||||
:label="operationsLabel"
|
|
||||||
align="center"
|
|
||||||
:width="operationsWidth"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<slot name="operations" :row="scope.row" :$index="scope.$index" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div class="table-footer">
|
|
||||||
<slot name="footer-btns"></slot>
|
|
||||||
<CustomPagination
|
|
||||||
v-if="showPagination"
|
|
||||||
:total="total"
|
|
||||||
:current-page="currentPage"
|
|
||||||
:page-size="pageSize"
|
|
||||||
:page-size-options="pageSizeOptions"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@page-change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import CustomPagination from './CustomPagination.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'CustomTable',
|
|
||||||
components: {
|
|
||||||
CustomPagination
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
// 表格数据
|
|
||||||
data: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
// 列配置
|
|
||||||
columns: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
// 是否显示选择框
|
|
||||||
showSelection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
// 是否显示操作列
|
|
||||||
showOperations: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
operationsLabel: {
|
|
||||||
type: String,
|
|
||||||
default: '操作'
|
|
||||||
},
|
|
||||||
operationsWidth: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: 180
|
|
||||||
},
|
|
||||||
// 分页相关
|
|
||||||
showPagination: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
total: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
currentPage: {
|
|
||||||
type: Number,
|
|
||||||
default: 1
|
|
||||||
},
|
|
||||||
pageSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 10
|
|
||||||
},
|
|
||||||
pageSizeOptions: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [10, 20, 50, 100]
|
|
||||||
},
|
|
||||||
// 加载状态
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
loadingText: {
|
|
||||||
type: String,
|
|
||||||
default: 'Loading'
|
|
||||||
},
|
|
||||||
loadingSpinner: {
|
|
||||||
type: String,
|
|
||||||
default: 'el-icon-loading'
|
|
||||||
},
|
|
||||||
loadingBackground: {
|
|
||||||
type: String,
|
|
||||||
default: 'rgba(255, 255, 255, 0.7)'
|
|
||||||
},
|
|
||||||
// 自定义类名
|
|
||||||
tableClass: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
headerCellClassName: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
rowClassName: {
|
|
||||||
type: [String, Function],
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
tableContainerHeight() {
|
|
||||||
return this.showPagination ? 'calc(100% - 48px)' : '100%';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// 复选框变化
|
|
||||||
handleCheckboxChange(row) {
|
|
||||||
this.$set(row, 'selected', !row.selected);
|
|
||||||
},
|
|
||||||
// 分页事件
|
|
||||||
handleSizeChange(val) {
|
|
||||||
this.$emit('size-change', val);
|
|
||||||
},
|
|
||||||
handlePageChange(page) {
|
|
||||||
this.$emit('page-change', page);
|
|
||||||
},
|
|
||||||
// 选择事件
|
|
||||||
handleSelectionChange(selection) {
|
|
||||||
this.$emit('selection-change', selection);
|
|
||||||
},
|
|
||||||
// 行点击事件
|
|
||||||
handleRowClick(row, column, event) {
|
|
||||||
this.$emit('row-click', row, column, event);
|
|
||||||
},
|
|
||||||
// 清除选择
|
|
||||||
clearSelection() {
|
|
||||||
this.$refs.tableRef && this.$refs.tableRef.clearSelection();
|
|
||||||
},
|
|
||||||
// 切换选择
|
|
||||||
toggleRowSelection(row, selected) {
|
|
||||||
this.$refs.tableRef && this.$refs.tableRef.toggleRowSelection(row, selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.custom-table-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
.table-container {
|
|
||||||
width: 100%;
|
|
||||||
box-shadow: 0 2px 12px rgba(74, 124, 253, 0.12);
|
|
||||||
border-radius: 6px;
|
|
||||||
.custom-table {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #eef3fd;
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
.el-table__body-wrapper {
|
|
||||||
overflow-y: auto;
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: #a1c9fd;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: #f0f3fe;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-table__header {
|
|
||||||
th {
|
|
||||||
color: #342f45;
|
|
||||||
background: #edf2fc !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.el-table) {
|
|
||||||
.el-table__body-wrapper {
|
|
||||||
overflow-y: auto;
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: #a1c9fd;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: #f0f3fe;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-table__header {
|
|
||||||
th {
|
|
||||||
color: #342f45;
|
|
||||||
background: #edf2fc !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.table-footer {
|
|
||||||
padding: 16px 0px 0px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-loading-mask) {
|
|
||||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-loading-spinner .circular) {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-loading-spinner .path) {
|
|
||||||
stroke: #6b8cff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-loading-text) {
|
|
||||||
color: #6b8cff !important;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,117 +1,116 @@
|
|||||||
<template>
|
<template>
|
||||||
<CustomDialog
|
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||||
:title="title"
|
<el-form :model="form" :rules="rules" ref="form" label-width="auto">
|
||||||
:visible.sync="dialogVisible"
|
<el-form-item :label="$t('dictDataDialog.dictLabel')" prop="dictLabel">
|
||||||
width="600px"
|
<el-input v-model="form.dictLabel" :placeholder="$t('dictDataDialog.dictLabelPlaceholder')"></el-input>
|
||||||
@confirm="submit"
|
</el-form-item>
|
||||||
@close="cancel"
|
<el-form-item :label="$t('dictDataDialog.dictValue')" prop="dictValue">
|
||||||
:confirmLoading="saving"
|
<el-input v-model="form.dictValue" :placeholder="$t('dictDataDialog.dictValuePlaceholder')"></el-input>
|
||||||
>
|
</el-form-item>
|
||||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left">
|
<el-form-item :label="$t('dictDataDialog.sort')" prop="sort">
|
||||||
<el-form-item :label="$t('dictDataDialog.dictLabel')" prop="dictLabel" class="form-item">
|
<el-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%;"></el-input-number>
|
||||||
<el-input v-model="form.dictLabel" :placeholder="$t('dictDataDialog.dictLabelPlaceholder')" />
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-form>
|
||||||
<el-form-item :label="$t('dictDataDialog.dictValue')" prop="dictValue" class="form-item">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-input v-model="form.dictValue" :placeholder="$t('dictDataDialog.dictValuePlaceholder')" />
|
<el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
|
||||||
</el-form-item>
|
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
|
||||||
<el-form-item :label="$t('dictDataDialog.sort')" prop="sort" class="form-item">
|
</div>
|
||||||
<el-input-number v-model="form.sort" :min="0" :max="999" class="custom-input-number"></el-input-number>
|
</el-dialog>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</CustomDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CustomDialog from './CustomDialog.vue';
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DictDataDialog',
|
name: 'DictDataDialog',
|
||||||
props: {
|
props: {
|
||||||
visible: {
|
visible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => this.$t('dictDataDialog.addDictData')
|
default: () => this.$t('dictDataDialog.addDictData')
|
||||||
},
|
},
|
||||||
dictData: {
|
dictData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
dictTypeId: {
|
dictTypeId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: null
|
default: null
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
CustomDialog
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
dialogVisible: this.visible,
|
|
||||||
saving: false,
|
|
||||||
form: {
|
|
||||||
id: null,
|
|
||||||
dictTypeId: null,
|
|
||||||
dictLabel: '',
|
|
||||||
dictValue: '',
|
|
||||||
sort: 0
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
dictLabel: [{ required: true, message: this.$t('dictDataDialog.requiredDictLabel'), trigger: 'blur' }],
|
|
||||||
dictValue: [{ required: true, message: this.$t('dictDataDialog.requiredDictValue'), trigger: 'blur' }]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
visible(val) {
|
|
||||||
this.dialogVisible = val;
|
|
||||||
},
|
|
||||||
dialogVisible(val) {
|
|
||||||
this.$emit('update:visible', val);
|
|
||||||
if (!val) {
|
|
||||||
this.saving = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dictData: {
|
|
||||||
handler(val) {
|
|
||||||
if (val) {
|
|
||||||
this.form = { ...val };
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
},
|
},
|
||||||
dictTypeId: {
|
data() {
|
||||||
handler(val) {
|
return {
|
||||||
if (val) {
|
dialogVisible: this.visible,
|
||||||
this.form.dictTypeId = val;
|
form: {
|
||||||
|
id: null,
|
||||||
|
dictTypeId: null,
|
||||||
|
dictLabel: '',
|
||||||
|
dictValue: '',
|
||||||
|
sort: 0
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
dictLabel: [{ required: true, message: this.$t('dictDataDialog.requiredDictLabel'), trigger: 'blur' }],
|
||||||
|
dictValue: [{ required: true, message: this.$t('dictDataDialog.requiredDictValue'), trigger: 'blur' }]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
immediate: true
|
watch: {
|
||||||
}
|
dictData: {
|
||||||
},
|
handler(val) {
|
||||||
methods: {
|
if (val) {
|
||||||
submit() {
|
this.form = { ...val }
|
||||||
this.$refs.form.validate((valid) => {
|
}
|
||||||
if (valid) {
|
},
|
||||||
this.saving = true;
|
immediate: true
|
||||||
this.$emit('save', this.form);
|
},
|
||||||
|
dictTypeId: {
|
||||||
|
handler(val) {
|
||||||
|
if (val) {
|
||||||
|
this.form.dictTypeId = val
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
visible(val) {
|
||||||
|
this.dialogVisible = val;
|
||||||
|
},
|
||||||
|
dialogVisible(val) {
|
||||||
|
this.$emit('update:visible', val);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
},
|
},
|
||||||
cancel() {
|
methods: {
|
||||||
this.saving = false;
|
handleClose() {
|
||||||
this.$emit('cancel');
|
this.dialogVisible = false;
|
||||||
},
|
this.resetForm();
|
||||||
resetSaving() {
|
},
|
||||||
this.saving = false;
|
resetForm() {
|
||||||
|
this.form = {
|
||||||
|
id: null,
|
||||||
|
dictTypeId: this.dictTypeId,
|
||||||
|
dictLabel: '',
|
||||||
|
dictValue: '',
|
||||||
|
sort: 0
|
||||||
|
}
|
||||||
|
this.$refs.form?.resetFields()
|
||||||
|
},
|
||||||
|
handleSave() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.$emit('save', this.form)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped>
|
||||||
.custom-input-number {
|
.dialog-footer {
|
||||||
width: 100%;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||