Compare commits

..
Author SHA1 Message Date
FAN-yeB f7782b1f6e update:新增SLM唤醒词回复 2026-05-15 10:50:52 +08:00
403 changed files with 16622 additions and 44968 deletions
-5
View File
@@ -112,17 +112,12 @@ celerybeat.pid
# Environments
.env
.venv
/.venv-*/
env/
venv/
ENV/
env.bak/
venv.bak/
# Repository-local runtimes and package-manager caches
/.runtime/
/main/manager-web/.npm-cache/
# Spyder project settings
.spyderproject
.spyproject
+74 -59
View File
@@ -55,95 +55,110 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
<img alt="响应速度感受" src="docs/images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<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/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
<img alt="声纹识别" src="docs/images/demo14.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="docs/images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="docs/images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="docs/images/demo10.png" />
</picture>
</a>
</td>
<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/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
<img alt="自定义音色" src="docs/images/demo2.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="docs/images/demo0.png" />
</picture>
</a>
</td>
</tr>
-3
View File
@@ -65,7 +65,6 @@ VAD:
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
6、[修改代码后怎么编译自己的Docker镜像](./docker-build.md)<br/>
### 8、编译固件相关教程
1、[如何自己编译小智固件](./firmware-build.md)<br/>
@@ -85,8 +84,6 @@ VAD:
10、[如何部署上下文源](./context-provider-integration.md)<br/>
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
13、[如何开启设备呼叫插件](./device-call-guide.md)<br/>
14、[如何开启联网搜索功能](./web-search-integration.md)<br/>
### 10、数字人相关教程
1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/>
-148
View File
@@ -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.62026年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`上去。
+12 -65
View File
@@ -1,74 +1,21 @@
# 本地编译docker镜像方法
现在本项目已经使用`github``自动编译docker镜像`功能,如果您拉取的是项目发行的镜像,您没有自己编译镜像需求,那就忽略本文档
现在本项目已经使用github自动编译docker功能,本文档是提供给有本地编译docker镜像需求的朋友准备的
如果您修改了源码,然后想采用`docker`的方式部署运行,可以参照以下步骤操作:
## 1、环境准备
安装docker
```bash
1、安装docker
```
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
## 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:新的版本号 .
2、编译docker镜像
```
#进入项目根目录
# 编译server
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
# 编译web
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
## 3、修改docker-compose配置
```bash
# 编译完成后,可以使用docker-compose启动项目
# docker-compose.yml你需要修改成自己编译的镜像版本
cd main/xiaozhi-server
```
编辑 `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
docker compose up -d
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 280 KiB

-161
View File
@@ -1,161 +0,0 @@
# Native MQTT/UDP integration
The server can accept device MQTT control messages and encrypted UDP audio
without running `xiaozhi-mqtt-gateway`. Native MQTT is optional. The existing
Gateway path and direct WebSocket path remain supported.
## Compatibility modes
| Mode | Device control | Device audio | Server business runtime |
| --- | --- | --- | --- |
| Direct WebSocket | WebSocket JSON | WebSocket binary | Shared runtime |
| MQTT Gateway | MQTT via Gateway WebSocket | UDP via Gateway WebSocket | Shared runtime |
| Native MQTT | MQTT 3.1.1 QoS 0 | AES-128-CTR UDP | Shared runtime |
Native MQTT keeps the physical MQTT connection alive between conversations.
Each device `hello` creates a new logical conversation and UDP encryption
session. `goodbye` ends only that logical conversation; MQTT is closed only by
MQTT disconnect, duplicate client takeover, keepAlive expiry, server shutdown,
or a protocol/authentication error.
## Enable Native MQTT
Native MQTT is disabled by default. Both the protocol and server switches must
be enabled:
```yaml
protocols:
enabled_protocols: [websocket, mqtt]
websocket_enabled: true
mqtt_enabled: true
mqtt_server:
enabled: true
host: 0.0.0.0
port: 1883
udp_port: 1883
udp_bind_host: ""
public_endpoint: mqtt.example.com
signature_key: replace-with-a-strong-secret
max_connections: 1000
max_pending_connections: 128
heartbeat_interval: 30
max_payload_size: 8192
message_queue_size: 128
business_ready_timeout: 30
goodbye_timeout: 1
close_timeout: 2
# Includes the common model and any Agent-specific local ASR variants.
shared_asr_max_models: 3
```
`host` is the local MQTT listen address. `public_endpoint` must be reachable by
the device and may be either `host` or `host:port`; manager-api normalizes it
and does not append the port twice. When `host` is a wildcard and
`public_endpoint` is a local IPv4 address, the UDP socket binds that address so
reply datagrams use the same source IP advertised to the device. Set
`udp_bind_host` only when the local UDP bind address must differ from the
advertised endpoint (for example, behind NAT); leaving it empty enables the
automatic behavior. Open the configured MQTT TCP and UDP ports in the firewall.
When configuration is read from manager-api, configure the equivalent
`protocols.*` and `mqtt_server.*` system parameters. The OTA response prefers a
valid Native endpoint when Native is enabled. If Native is disabled or invalid,
manager-api falls back to `server.mqtt_gateway`. If neither is configured, OTA
returns only WebSocket information.
The current firmware persists the OTA `mqtt` object. The UDP server, key and
nonce used by Native mode are negotiated in the MQTT `hello` response, so
manager-api does not send a separate top-level OTA `udp` object.
Long-lived Native connections refresh Agent-private components at logical
`hello` boundaries. Local ASR variants are shared by effective configuration
instead of loaded once per device. `shared_asr_max_models` bounds resident
models; when capacity is exhausted, the replacement runtime is rejected and
the previous healthy runtime remains active.
The default capacity reserves one transition slot so a single Agent can replace
an active private local model without dropping its healthy runtime first. Idle
variants are cached and evicted before another model is loaded; resident models
never exceed this limit.
MQTT application packets are dispatched in order outside the socket read loop,
so PINGREQ remains responsive while a logical Hello waits for private runtime
refresh. `business_ready_timeout` bounds that wait and closes an unusable
connection instead of leaving the device in a half-negotiated session.
`max_connections` limits authenticated client identities, while
`max_pending_connections` separately bounds sockets that have not completed
CONNECT. A connection using an existing client id may therefore replace its
previous owner even when active capacity is full. The server sends the success
CONNACK only after the previous owner has been reclaimed. `goodbye_timeout` and
`close_timeout` bound best-effort device reset and physical resource cleanup so
a stalled peer cannot block heartbeat or shutdown indefinitely.
## Authentication and topics
manager-api generates the device credentials and topics:
- client id: `<group>@@@<mac_with_underscores>@@@<mac_with_underscores>`
- publish topic: `device-server`
- subscribe topic: `devices/p2p/<mac_with_underscores>`
- username: Base64-encoded JSON metadata
- password: Base64(HMAC-SHA256(`client_id + "|" + username`, signature key))
Use the same secret for manager-api `mqtt_server.signature_key` and
xiaozhi-server `mqtt_server.signature_key`. The legacy
`server.mqtt_signature_key` remains a fallback for Gateway compatibility.
Native MQTT accepts MQTT 3.1.1 CONNECT, SUBSCRIBE, PUBLISH QoS 0, PING and
DISCONNECT. Unsupported protocol versions, QoS levels, malformed packets, or
invalid credentials close the physical connection.
## Session and UDP flow
1. Device connects and subscribes over MQTT.
2. Server validates CONNECT credentials and replaces any older connection with
the same client id.
3. Device publishes protocol version 3 `hello`.
4. Server waits until private configuration and business components are ready.
5. Server creates a logical session and returns the UDP server, AES key, nonce,
output audio parameters and session id.
6. The first valid UDP packet from the MQTT peer binds the UDP source tuple for
that logical session. Source changes are rejected until the next `hello`.
7. MQTT JSON and decrypted Opus frames enter the same message processors used
by WebSocket and Gateway connections.
8. Server `goodbye` returns the device to Idle and finalizes conversation tasks
while preserving the MQTT connection.
The 16-byte UDP header is also the AES-CTR nonce:
| Bytes | Field |
| --- | --- |
| 0 | packet type (`1`) |
| 1 | reserved |
| 2..3 | encrypted payload length, big endian |
| 4..7 | connection id, big endian |
| 8..11 | timestamp, big endian |
| 12..15 | sequence, big endian |
Datagrams with an invalid type, header, payload length, connection id, source
address or stale sequence are discarded. Sequence gaps are tolerated because
UDP can lose packets; late and duplicate packets are discarded. AES-CTR
provides confidentiality but not integrity, so Native MQTT and UDP should be
exposed only with a strong signature key and appropriate network controls.
## Verification
From `main/xiaozhi-server`:
```bash
python -m compileall -q .
```
From `main/manager-api` with JDK 21:
```bash
mvn clean package
```
Before deployment, verify Native and Gateway separately with real hardware:
multiple conversations, TTS interruption, exit to Idle, duplicate reconnect,
server restart, short network loss and a 30-60 minute keepAlive soak.
+74 -59
View File
@@ -54,95 +54,110 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
<img alt="响应速度感受" src="../images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
<img alt="速度优化秘诀" src="../images/demo6.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<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/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
<img alt="复杂医疗场景" src="../images/demo1.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="../images/demo10.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="../images/demo12.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
<img alt="自定义音色" src="../images/demo2.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
<img alt="使用粤语交流" src="../images/demo3.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a>
</td>
</tr>
+74 -59
View File
@@ -54,95 +54,110 @@ Want to see the usage effects? Click the videos below 🎥
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
<img alt="响应速度感受" src="../images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
<img alt="速度优化秘诀" src="../images/demo6.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<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/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
<img alt="复杂医疗场景" src="../images/demo1.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="../images/demo10.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="../images/demo12.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
<img alt="自定义音色" src="../images/demo2.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
<img alt="使用粤语交流" src="../images/demo3.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a>
</td>
</tr>
+74 -59
View File
@@ -54,95 +54,110 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
<img alt="Experiência de velocidade de resposta" src="../images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
<img alt="Segredo da otimização de velocidade" src="../images/demo6.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<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/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
<img alt="Cenário médico complexo" src="../images/demo1.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
<img alt="Envio de comandos MQTT" src="../images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
<img alt="Reconhecimento de impressão vocal" src="../images/demo14.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="Controle de interruptores de eletrodomésticos" src="../images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="Ponto de acesso MCP" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="Tarefas com múltiplos comandos" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="Reproduzir música" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="Plugin de clima" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="Interrupção em tempo real" src="../images/demo10.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="Fotografar e identificar objetos" src="../images/demo12.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
<img alt="Timbre de voz personalizado" src="../images/demo2.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
<img alt="Comunicação em cantonês" src="../images/demo3.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="Transmissão de notícias" src="../images/demo0.png" />
</picture>
</a>
</td>
</tr>
+74 -59
View File
@@ -54,95 +54,110 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
<img alt="响应速度感受" src="../images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
<img alt="速度优化秘诀" src="../images/demo6.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<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/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
<img alt="复杂医疗场景" src="../images/demo1.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="../images/demo10.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="../images/demo12.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
<img alt="自定义音色" src="../images/demo2.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
<img alt="使用粤语交流" src="../images/demo3.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a>
</td>
</tr>
-71
View File
@@ -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
```
配置完成后重启服务即可生效。
+1 -1
View File
@@ -168,7 +168,7 @@
<label for="emojiEnabled">情绪表情:</label>
<select id="emojiEnabled" class="model-select">
<option value="true">启用</option>
<option value="false" selected>禁用</option>
<option value="false">禁用</option>
</select>
</div>
</div>
+6 -19
View File
@@ -20,7 +20,7 @@
<java.version>21</java.version>
<junit.version>5.10.1</junit.version>
<druid.version>1.2.20</druid.version>
<mybatisplus.version>3.5.17</mybatisplus.version>
<mybatisplus.version>3.5.5</mybatisplus.version>
<hutool.version>5.8.24</hutool.version>
<jsoup.version>1.19.1</jsoup.version>
<knife4j.version>4.6.0</knife4j.version>
@@ -32,8 +32,6 @@
<liquibase-core.version>4.20.0</liquibase-core.version>
<aliyun-sms-version>4.1.0</aliyun-sms-version>
<okio-version>3.4.0</okio-version>
<skipTests>true</skipTests>
<argLine></argLine>
</properties>
<dependencies>
@@ -177,13 +175,13 @@
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>${mybatisplus.version}</version>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
@@ -273,22 +271,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<proc>full</proc>
<compilerArgs>
<arg>-Xlint:deprecation,unchecked</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${skipTests}</skipTests>
<argLine>@{argLine} -Xshare:off -javaagent:"${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar"</argLine>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
@@ -106,32 +106,6 @@ public interface Constant {
*/
String SERVER_MQTT_GATEWAY = "server.mqtt_gateway";
/**
* MQTT原生服务开关
*/
String SERVER_MQTT_ENABLED = "server.mqtt_enabled";
/**
* 新版协议配置
*/
String PROTOCOLS_ENABLED = "protocols.enabled_protocols";
String PROTOCOLS_WEBSOCKET_ENABLED = "protocols.websocket_enabled";
String PROTOCOLS_MQTT_ENABLED = "protocols.mqtt_enabled";
/**
* 新版原生MQTT配置
*/
String MQTT_SERVER_ENABLED = "mqtt_server.enabled";
String MQTT_SERVER_HOST = "mqtt_server.host";
String MQTT_SERVER_PORT = "mqtt_server.port";
String MQTT_SERVER_UDP_PORT = "mqtt_server.udp_port";
String MQTT_SERVER_UDP_BIND_HOST = "mqtt_server.udp_bind_host";
String MQTT_SERVER_PUBLIC_ENDPOINT = "mqtt_server.public_endpoint";
String MQTT_SERVER_SIGNATURE_KEY = "mqtt_server.signature_key";
String MQTT_SERVER_MANAGER_API = "mqtt_server.manager_api";
String MQTT_SERVER_MANAGER_API_SECRET = "mqtt_server.manager_api_secret";
String SERVER_MQTT_MANAGER_API = "server.mqtt_manager_api";
/**
* ota地址
*/
@@ -172,11 +146,6 @@ public interface Constant {
*/
String SERVER_AUTH_ENABLED = "server.auth.enabled";
/**
* 系统功能菜单配置
*/
String SYSTEM_WEB_MENU = "system-web.menu";
/**
* 无记忆
*/
@@ -350,7 +319,7 @@ public interface Constant {
/**
* 版本号
*/
public static final String VERSION = "0.9.6";
public static final String VERSION = "0.9.3";
/**
* 无效固件URL
@@ -67,7 +67,7 @@ public class DataFilterInterceptor implements InnerInterceptor {
private String getSelect(String buildSql, DataScope scope) {
try {
Select select = (Select) CCJSqlParserUtil.parse(buildSql);
PlainSelect plainSelect = select.getPlainSelect();
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
Expression expression = plainSelect.getWhere();
if (expression == null) {
@@ -188,11 +188,4 @@ public class RedisKeys {
return "ota:upload:count:" + username;
}
/**
* 设备通讯录缓存Key
*/
public static String getAddressBookKey() {
return "device:address_book:all";
}
}
@@ -5,7 +5,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.BiConsumer;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.logging.Log;
@@ -81,8 +81,8 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
// 处理排序字段
if (orderField instanceof String) {
orderFields.add((String) orderField);
} else if (orderField instanceof List<?> fields) {
fields.forEach(field -> orderFields.add(String.class.cast(field)));
} else if (orderField instanceof List) {
orderFields.addAll((List<String>) orderField);
}
// 有排序字段则排序
@@ -142,12 +142,11 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
return SqlHelper.retBool(result);
}
protected Class<?> currentMapperClass() {
return ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
protected Class<M> currentMapperClass() {
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
}
@Override
@SuppressWarnings("unchecked")
public Class<T> currentModelClass() {
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
}
@@ -182,9 +181,8 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
* 执行批量操作
*/
@SuppressWarnings("deprecation")
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiFunction<SqlSession, E, Integer> operation) {
return SqlHelper.executeBatch(SqlHelper.sqlSessionFactory(this.currentModelClass()), this.log, list, batchSize,
operation);
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
}
@Override
@@ -211,7 +209,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
param.put(Constants.ENTITY, entity);
return sqlSession.update(sqlStatement, param);
sqlSession.update(sqlStatement, param);
});
}
@@ -227,6 +225,6 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
@Override
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
return SqlHelper.retBool(baseDao.deleteByIds(idList));
return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
}
}
@@ -24,7 +24,6 @@ import xiaozhi.common.utils.ConvertUtils;
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
implements CrudService<T, D> {
@SuppressWarnings("unchecked")
protected Class<D> currentDtoClass() {
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
}
@@ -71,6 +70,6 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends Bas
@Override
public void delete(Serializable[] ids) {
baseDao.deleteByIds(Arrays.asList(ids));
baseDao.deleteBatchIds(Arrays.asList(ids));
}
}
@@ -1,9 +1,7 @@
package xiaozhi.common.utils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -18,10 +16,6 @@ import cn.hutool.core.util.StrUtil;
*/
public class JsonUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<>() {
};
private static final TypeReference<List<Map<String, Object>>> STRING_OBJECT_MAP_LIST = new TypeReference<>() {
};
public static String toJsonString(Object object) {
try {
@@ -73,59 +67,4 @@ public class JsonUtils {
}
}
public static Map<String, Object> parseMap(String text) {
if (StrUtil.isEmpty(text)) {
return null;
}
return parseObject(text, STRING_OBJECT_MAP);
}
public static List<Map<String, Object>> parseMapList(String text) {
if (StrUtil.isEmpty(text)) {
return null;
}
return parseObject(text, STRING_OBJECT_MAP_LIST);
}
public static Map<String, Object> toStringObjectMap(Object value) {
if (value == null) {
return null;
}
if (!(value instanceof Map<?, ?> map)) {
throw new ClassCastException("Expected Map but got " + value.getClass().getName());
}
Map<String, Object> result = new LinkedHashMap<>(map.size());
for (Map.Entry<?, ?> entry : map.entrySet()) {
result.put(String.class.cast(entry.getKey()), entry.getValue());
}
return result;
}
public static List<Map<String, Object>> toStringObjectMapList(Object value) {
if (value == null) {
return null;
}
List<?> list = List.class.cast(value);
List<Map<String, Object>> result = new ArrayList<>(list.size());
for (Object item : list) {
result.add(toStringObjectMap(item));
}
return result;
}
public static <T> List<T> toList(Object value, Class<T> elementType) {
if (value == null) {
return null;
}
List<?> list = List.class.cast(value);
List<T> result = new ArrayList<>(list.size());
for (Object item : list) {
result.add(elementType.cast(item));
}
return result;
}
}
@@ -75,11 +75,11 @@ public class SensitiveDataUtils {
Object value = jsonObject.get(key);
if (SENSITIVE_FIELDS.contains(key.toLowerCase()) && value instanceof String) {
result.set(key, maskMiddle((String) value));
result.put(key, maskMiddle((String) value));
} else if (value instanceof JSONObject) {
result.set(key, maskSensitiveFields((JSONObject) value));
result.put(key, maskSensitiveFields((JSONObject) value));
} else {
result.set(key, value);
result.put(key, value);
}
}
@@ -22,10 +22,10 @@ public class SqlFilter {
return null;
}
// 去掉'|"|;|\字符
str = str.replace("'", "");
str = str.replace("\"", "");
str = str.replace(";", "");
str = str.replace("\\", "");
str = StringUtils.replace(str, "'", "");
str = StringUtils.replace(str, "\"", "");
str = StringUtils.replace(str, ";", "");
str = StringUtils.replace(str, "\\", "");
// 转换成小写
str = str.toLowerCase();
@@ -1,103 +0,0 @@
package xiaozhi.modules.agent.Enums;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import lombok.Getter;
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
@Getter
public enum AgentSnapshotField {
AGENT_CODE("agentCode", AgentSnapshotDataDTO::getAgentCode, AgentUpdateDTO::getAgentCode,
(agent, data) -> agent.setAgentCode(data.getAgentCode())),
AGENT_NAME("agentName", AgentSnapshotDataDTO::getAgentName, AgentUpdateDTO::getAgentName,
(agent, data) -> agent.setAgentName(data.getAgentName())),
ASR_MODEL_ID("asrModelId", AgentSnapshotDataDTO::getAsrModelId, AgentUpdateDTO::getAsrModelId,
(agent, data) -> agent.setAsrModelId(data.getAsrModelId())),
VAD_MODEL_ID("vadModelId", AgentSnapshotDataDTO::getVadModelId, AgentUpdateDTO::getVadModelId,
(agent, data) -> agent.setVadModelId(data.getVadModelId())),
LLM_MODEL_ID("llmModelId", AgentSnapshotDataDTO::getLlmModelId, AgentUpdateDTO::getLlmModelId,
(agent, data) -> agent.setLlmModelId(data.getLlmModelId())),
SLM_MODEL_ID("slmModelId", AgentSnapshotDataDTO::getSlmModelId, AgentUpdateDTO::getSlmModelId,
(agent, data) -> agent.setSlmModelId(data.getSlmModelId())),
VLLM_MODEL_ID("vllmModelId", AgentSnapshotDataDTO::getVllmModelId, AgentUpdateDTO::getVllmModelId,
(agent, data) -> agent.setVllmModelId(data.getVllmModelId())),
TTS_MODEL_ID("ttsModelId", AgentSnapshotDataDTO::getTtsModelId, AgentUpdateDTO::getTtsModelId,
(agent, data) -> agent.setTtsModelId(data.getTtsModelId())),
TTS_VOICE_ID("ttsVoiceId", AgentSnapshotDataDTO::getTtsVoiceId, AgentUpdateDTO::getTtsVoiceId,
(agent, data) -> agent.setTtsVoiceId(data.getTtsVoiceId())),
TTS_LANGUAGE("ttsLanguage", AgentSnapshotDataDTO::getTtsLanguage, AgentUpdateDTO::getTtsLanguage,
(agent, data) -> agent.setTtsLanguage(data.getTtsLanguage())),
TTS_VOLUME("ttsVolume", AgentSnapshotDataDTO::getTtsVolume, AgentUpdateDTO::getTtsVolume,
(agent, data) -> agent.setTtsVolume(data.getTtsVolume())),
TTS_RATE("ttsRate", AgentSnapshotDataDTO::getTtsRate, AgentUpdateDTO::getTtsRate,
(agent, data) -> agent.setTtsRate(data.getTtsRate())),
TTS_PITCH("ttsPitch", AgentSnapshotDataDTO::getTtsPitch, AgentUpdateDTO::getTtsPitch,
(agent, data) -> agent.setTtsPitch(data.getTtsPitch())),
MEM_MODEL_ID("memModelId", AgentSnapshotDataDTO::getMemModelId, AgentUpdateDTO::getMemModelId,
(agent, data) -> agent.setMemModelId(data.getMemModelId())),
INTENT_MODEL_ID("intentModelId", AgentSnapshotDataDTO::getIntentModelId, AgentUpdateDTO::getIntentModelId,
(agent, data) -> agent.setIntentModelId(data.getIntentModelId())),
CHAT_HISTORY_CONF("chatHistoryConf", AgentSnapshotDataDTO::getChatHistoryConf, AgentUpdateDTO::getChatHistoryConf,
(agent, data) -> agent.setChatHistoryConf(data.getChatHistoryConf())),
SYSTEM_PROMPT("systemPrompt", AgentSnapshotDataDTO::getSystemPrompt, AgentUpdateDTO::getSystemPrompt,
(agent, data) -> agent.setSystemPrompt(data.getSystemPrompt())),
SUMMARY_MEMORY("summaryMemory", AgentSnapshotDataDTO::getSummaryMemory, AgentUpdateDTO::getSummaryMemory,
(agent, data) -> agent.setSummaryMemory(data.getSummaryMemory())),
LANG_CODE("langCode", AgentSnapshotDataDTO::getLangCode, AgentUpdateDTO::getLangCode,
(agent, data) -> agent.setLangCode(data.getLangCode())),
LANGUAGE("language", AgentSnapshotDataDTO::getLanguage, AgentUpdateDTO::getLanguage,
(agent, data) -> agent.setLanguage(data.getLanguage())),
SORT("sort", AgentSnapshotDataDTO::getSort, AgentUpdateDTO::getSort,
(agent, data) -> agent.setSort(data.getSort())),
FUNCTIONS("functions", AgentSnapshotDataDTO::getFunctions, AgentUpdateDTO::getFunctions, null),
CONTEXT_PROVIDERS("contextProviders", AgentSnapshotDataDTO::getContextProviders,
AgentUpdateDTO::getContextProviders, null),
CORRECT_WORD_FILE_IDS("correctWordFileIds", AgentSnapshotDataDTO::getCorrectWordFileIds,
AgentUpdateDTO::getCorrectWordFileIds, null),
TAG_NAMES("tagNames", AgentSnapshotDataDTO::getTagNames, AgentUpdateDTO::getTagNames, null);
private final String fieldName;
private final Function<AgentSnapshotDataDTO, Object> snapshotGetter;
private final Function<AgentUpdateDTO, Object> updateGetter;
private final BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier;
AgentSnapshotField(String fieldName, Function<AgentSnapshotDataDTO, Object> snapshotGetter,
Function<AgentUpdateDTO, Object> updateGetter,
BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier) {
this.fieldName = fieldName;
this.snapshotGetter = snapshotGetter;
this.updateGetter = updateGetter;
this.restoreApplier = restoreApplier;
}
public static List<String> names() {
return Arrays.stream(values()).map(AgentSnapshotField::getFieldName).toList();
}
public static String canonicalName(String fieldName) {
return "tags".equals(fieldName) ? TAG_NAMES.getFieldName() : fieldName;
}
public Object snapshotValue(AgentSnapshotDataDTO data) {
return data == null ? null : snapshotGetter.apply(data);
}
public Object updateValue(AgentUpdateDTO data) {
return data == null ? null : updateGetter.apply(data);
}
public boolean isRestorableAgentField() {
return restoreApplier != null;
}
public void applyTo(AgentEntity agent, AgentSnapshotDataDTO data) {
if (restoreApplier != null) {
restoreApplier.accept(agent, data);
}
}
}
@@ -28,13 +28,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
@@ -51,10 +48,15 @@ import xiaozhi.modules.agent.service.AgentTagService;
import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentChatSummaryService;
import xiaozhi.modules.agent.service.AgentContextProviderService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.correctword.service.CorrectWordFileService;
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser;
@Tag(name = "智能体管理")
@@ -62,39 +64,17 @@ import xiaozhi.modules.security.user.SecurityUser;
@RestController
@RequestMapping("/agent")
public class AgentController {
private static final long AUDIO_PLAY_TOKEN_EXPIRE_SECONDS = 300L;
private final AgentService agentService;
private final AgentTemplateService agentTemplateService;
private final DeviceService deviceService;
private final AgentChatHistoryService agentChatHistoryService;
private final AgentChatAudioService agentChatAudioService;
private final AgentPluginMappingService agentPluginMappingService;
private final AgentContextProviderService agentContextProviderService;
private final AgentChatSummaryService agentChatSummaryService;
private final RedisUtils redisUtils;
private final AgentTagService agentTagService;
private void requireAgentPermission(String agentId) {
if (!agentService.checkAgentPermission(agentId, SecurityUser.getUserId())) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
}
private String requireSessionAgent(String sessionId) {
String agentId = agentChatHistoryService.getAgentIdBySessionId(sessionId);
if (StringUtils.isBlank(agentId)) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
agentService.getAgentById(agentId);
return agentId;
}
private String requireAudioPermission(String audioId) {
String agentId = agentChatHistoryService.getAgentIdByAudioId(audioId);
if (StringUtils.isBlank(agentId)) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
requireAgentPermission(agentId);
return agentId;
}
private final CorrectWordFileService correctWordFileService;
@GetMapping("/list")
@Operation(summary = "获取用户智能体列表")
@@ -126,7 +106,7 @@ public class AgentController {
@Operation(summary = "获取智能体详情")
@RequiresPermissions("sys:role:normal")
public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) {
AgentInfoVO agent = agentService.getAgentById(id, SecurityUser.getUserId());
AgentInfoVO agent = agentService.getAgentById(id);
return ResultUtils.success(agent);
}
@@ -140,16 +120,20 @@ public class AgentController {
@PutMapping("/saveMemory/{macAddress}")
@Operation(summary = "根据设备id更新智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
agentService.updateAgentMemoryByDeviceMacAddress(macAddress, dto, SecurityUser.getUserId());
return new Result<Void>().ok(null);
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null) {
return new Result<>();
}
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
agentService.updateAgentById(device.getAgentId(), agentUpdateDTO);
return new Result<>();
}
@PostMapping("/chat-summary/{sessionId}/save")
@Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)")
public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) {
requireSessionAgent(sessionId);
try {
// 异步执行总结生成任务,立即返回成功响应
new Thread(() -> {
@@ -171,7 +155,6 @@ public class AgentController {
@PostMapping("/chat-title/{sessionId}/generate")
@Operation(summary = "根据会话ID生成聊天标题")
public Result<Void> generateAndSaveChatTitle(@PathVariable String sessionId) {
requireSessionAgent(sessionId);
agentChatSummaryService.generateAndSaveChatTitle(sessionId);
return new Result<Void>().ok(null);
}
@@ -180,7 +163,7 @@ public class AgentController {
@Operation(summary = "更新智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
agentService.updateAgentById(id, dto, SecurityUser.getUserId());
agentService.updateAgentById(id, dto);
return new Result<>();
}
@@ -188,7 +171,18 @@ public class AgentController {
@Operation(summary = "删除智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> delete(@PathVariable String id) {
agentService.deleteAgentById(id, SecurityUser.getUserId());
// 先删除关联的设备
deviceService.deleteByAgentId(id);
// 删除关联的聊天记录
agentChatHistoryService.deleteByAgentId(id, true, true);
// 删除关联的插件
agentPluginMappingService.deleteByAgentId(id);
// 删除关联的上下文源配置
agentContextProviderService.deleteByAgentId(id);
// 删除关联的替换词文件关联记录
correctWordFileService.deleteMappingsByAgentId(id);
// 再删除智能体
agentService.deleteById(id);
return new Result<>();
}
@@ -211,7 +205,6 @@ public class AgentController {
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
@PathVariable("id") String id,
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
requireAgentPermission(id);
params.put("agentId", id);
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
@@ -259,7 +252,6 @@ public class AgentController {
@RequiresPermissions("sys:role:normal")
public Result<String> getContentByAudioId(
@PathVariable("id") String id) {
requireAudioPermission(id);
// 查询聊天记录
String data = agentChatHistoryService.getContentByAudioId(id);
return new Result<String>().ok(data);
@@ -269,13 +261,12 @@ public class AgentController {
@Operation(summary = "获取音频下载ID")
@RequiresPermissions("sys:role:normal")
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
requireAudioPermission(audioId);
byte[] audioData = agentChatAudioService.getAudio(audioId);
if (audioData == null) {
return new Result<String>().error("音频不存在");
}
String uuid = UUID.randomUUID().toString();
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId, AUDIO_PLAY_TOKEN_EXPIRE_SECONDS);
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId);
return new Result<String>().ok(uuid);
}
@@ -331,7 +322,6 @@ public class AgentController {
@Operation(summary = "获取智能体的标签")
@RequiresPermissions("sys:role:normal")
public Result<List<AgentTagDTO>> getAgentTags(@PathVariable String id) {
requireAgentPermission(id);
List<AgentTagDTO> tags = agentTagService.getTagsByAgentId(id);
return new Result<List<AgentTagDTO>>().ok(tags);
}
@@ -340,13 +330,9 @@ public class AgentController {
@Operation(summary = "保存智能体的标签")
@RequiresPermissions("sys:role:normal")
public Result<Void> saveAgentTags(@PathVariable String id, @RequestBody Map<String, Object> params) {
requireAgentPermission(id);
List<String> tagIds = JsonUtils.toList(params.get("tagIds"), String.class);
List<String> tagNames = JsonUtils.toList(params.get("tagNames"), String.class);
AgentUpdateDTO dto = new AgentUpdateDTO();
dto.setTagIds(tagIds);
dto.setTagNames(tagNames);
agentService.updateAgentById(id, dto);
List<String> tagIds = (List<String>) params.get("tagIds");
List<String> tagNames = (List<String>) params.get("tagNames");
agentTagService.saveAgentTags(id, tagIds, tagNames);
return new Result<Void>().ok(null);
}
@@ -1,79 +0,0 @@
package xiaozhi.modules.agent.controller;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
import xiaozhi.modules.agent.dto.AgentSnapshotRestoreDTO;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentSnapshotService;
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
import xiaozhi.modules.security.user.SecurityUser;
@Tag(name = "智能体快照")
@AllArgsConstructor
@RestController
@RequestMapping("/agent/{agentId}/snapshots")
public class AgentSnapshotController {
private final AgentSnapshotService agentSnapshotService;
private final AgentService agentService;
@GetMapping
@Operation(summary = "获取智能体快照列表")
@RequiresPermissions("sys:role:normal")
public Result<PageData<AgentSnapshotVO>> page(
@PathVariable String agentId,
@ParameterObject AgentSnapshotPageDTO params) {
checkPermission(agentId);
return new Result<PageData<AgentSnapshotVO>>().ok(agentSnapshotService.page(agentId, params));
}
@GetMapping("/{snapshotId}")
@Operation(summary = "获取智能体快照详情")
@RequiresPermissions("sys:role:normal")
public Result<AgentSnapshotVO> getSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
checkPermission(agentId);
return new Result<AgentSnapshotVO>().ok(agentSnapshotService.getSnapshot(agentId, snapshotId));
}
@PostMapping("/{snapshotId}/restore")
@Operation(summary = "恢复智能体快照")
@RequiresPermissions("sys:role:normal")
public Result<Void> restore(@PathVariable String agentId, @PathVariable String snapshotId,
@RequestBody @Valid AgentSnapshotRestoreDTO request) {
checkPermission(agentId);
agentSnapshotService.restoreSnapshot(agentId, snapshotId, request.getCurrentStateToken());
return new Result<>();
}
@DeleteMapping("/{snapshotId}")
@Operation(summary = "删除智能体历史快照")
@RequiresPermissions("sys:role:normal")
public Result<Void> deleteSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
checkPermission(agentId);
agentSnapshotService.deleteSnapshot(agentId, snapshotId);
return new Result<>();
}
private void checkPermission(String agentId) {
UserDetail user = SecurityUser.getUser();
if (user == null || !agentService.checkAgentPermission(agentId, user.getId())) {
throw new RenException("没有权限访问该智能体快照");
}
}
}
@@ -36,20 +36,4 @@ public interface AgentDao extends BaseDao<AgentEntity> {
* @param agentId 智能体ID
*/
AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId);
/**
* 锁定智能体主记录,用于串行化同一智能体的配置写入
*
* @param agentId 智能体ID
*/
AgentEntity selectByIdForUpdate(@Param("agentId") String agentId);
/**
* 精确写入快照覆盖的智能体字段,包括目标快照中的 null 值。
* 不更新所属用户、创建信息等不属于快照的字段。
*
* @param agent 已应用目标快照的智能体
* @return 受影响行数
*/
int updateSnapshotFields(@Param("agent") AgentEntity agent);
}
@@ -1,29 +0,0 @@
package xiaozhi.modules.agent.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
@Mapper
public interface AgentSnapshotDao extends BaseDao<AgentSnapshotEntity> {
Integer selectMaxVersionNo(@Param("agentId") String agentId);
AgentSnapshotEntity selectLatestSnapshot(@Param("agentId") String agentId);
AgentSnapshotEntity selectNextSnapshot(@Param("agentId") String agentId, @Param("versionNo") Integer versionNo);
int insertWithNextVersion(@Param("snapshot") AgentSnapshotEntity snapshot);
int deleteOlderThanKeepLimit(@Param("agentId") String agentId, @Param("keepLimit") int keepLimit);
List<AgentSnapshotEntity> selectLegacyRedactionBatch(@Param("afterId") String afterId,
@Param("limit") int limit,
@Param("targetRedactionVersion") int targetRedactionVersion);
int updateRedactedSnapshots(@Param("snapshots") List<AgentSnapshotEntity> snapshots,
@Param("redactionVersion") int redactionVersion);
}
@@ -39,7 +39,7 @@ public class AgentDTO {
private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
private String summaryMemory;
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
@@ -14,6 +14,6 @@ public class AgentMemoryDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
private String summaryMemory;
}
@@ -1,40 +0,0 @@
package xiaozhi.modules.agent.dto;
import java.io.Serializable;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "智能体快照数据")
public class AgentSnapshotDataDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String agentCode;
private String agentName;
private String asrModelId;
private String vadModelId;
private String llmModelId;
private String slmModelId;
private String vllmModelId;
private String ttsModelId;
private String ttsVoiceId;
private String ttsLanguage;
private Integer ttsVolume;
private Integer ttsRate;
private Integer ttsPitch;
private String memModelId;
private String intentModelId;
private Integer chatHistoryConf;
private String systemPrompt;
private String summaryMemory;
private String langCode;
private String language;
private Integer sort;
private List<AgentUpdateDTO.FunctionInfo> functions;
private List<ContextProviderDTO> contextProviders;
private List<String> correctWordFileIds;
private List<String> tagNames;
private List<AgentSnapshotTagDTO> tags;
}
@@ -1,28 +0,0 @@
package xiaozhi.modules.agent.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "智能体快照分页查询参数")
public class AgentSnapshotPageDTO {
@Schema(description = "当前页码,从1开始", example = "1")
private Integer page = 1;
@Schema(description = "每页数量", example = "10")
private Integer limit = 10;
@Schema(description = "版本锚点,只查询小于等于该版本号的历史快照", example = "20")
private Integer maxVersionNo;
public int pageOrDefault() {
return page == null || page < 1 ? 1 : page;
}
public int limitOrDefault() {
if (limit == null || limit < 1) {
return 10;
}
return limit;
}
}
@@ -1,13 +0,0 @@
package xiaozhi.modules.agent.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "智能体快照恢复请求")
public class AgentSnapshotRestoreDTO {
@NotBlank
@Schema(description = "预览时由服务端生成的当前配置状态指纹")
private String currentStateToken;
}
@@ -1,16 +0,0 @@
package xiaozhi.modules.agent.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "智能体快照标签")
public class AgentSnapshotTagDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String tagName;
private Integer sort;
}
@@ -4,12 +4,9 @@ import java.io.Serializable;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import xiaozhi.common.utils.JsonUtils;
/**
* 智能体更新DTO
@@ -39,10 +36,10 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
private String slmModelId;
@Schema(description = "VLLM模型标识", example = "vllm_model_02", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
private String vllmModelId;
@Schema(description = "语音合成模型标识", example = "tts_model_02", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false)
private String ttsModelId;
@Schema(description = "音色标识", example = "voice_02", nullable = true)
@@ -94,49 +91,14 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "替换词文件ID列表", nullable = true)
private List<String> correctWordFileIds;
@Schema(description = "标签名称列表", nullable = true)
private List<String> tagNames;
@Schema(description = "标签ID列表", nullable = true)
private List<String> tagIds;
@Data
@Schema(description = "插件函数信息")
public static class FunctionInfo implements Serializable {
private static final TypeReference<HashMap<String, Object>> PARAM_INFO_TYPE = new TypeReference<>() {
};
@Schema(description = "插件ID", example = "plugin_01")
private String pluginId;
@Schema(description = "函数参数信息", nullable = true)
private HashMap<String, Object> paramInfo = new HashMap<>();
public void setParamInfo(Object paramInfo) {
this.paramInfo = normalizeParamInfo(paramInfo);
}
private static HashMap<String, Object> normalizeParamInfo(Object paramInfo) {
if (paramInfo == null) {
return new HashMap<>();
}
if (paramInfo instanceof String value) {
if (value.trim().isEmpty()) {
return new HashMap<>();
}
return JsonUtils.parseObject(value, PARAM_INFO_TYPE);
}
if (paramInfo instanceof Map<?, ?> value) {
HashMap<String, Object> normalized = new HashMap<>();
value.forEach((key, val) -> {
if (key != null) {
normalized.put(String.valueOf(key), val);
}
});
return normalized;
}
return JsonUtils.parseObject(JsonUtils.toJsonString(paramInfo), PARAM_INFO_TYPE);
}
private HashMap<String, Object> paramInfo;
private static final long serialVersionUID = 1L;
}
@@ -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() {
}
}
}
@@ -7,11 +7,11 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import xiaozhi.modules.agent.dto.ContextProviderDTO;
import xiaozhi.modules.agent.typehandler.ContextProviderListTypeHandler;
@Data
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
@@ -26,7 +26,7 @@ public class AgentContextProviderEntity {
private String agentId;
@Schema(description = "上下文源配置")
@TableField(typeHandler = ContextProviderListTypeHandler.class)
@TableField(typeHandler = JacksonTypeHandler.class)
private List<ContextProviderDTO> contextProviders;
@Schema(description = "创建者")
@@ -74,7 +74,7 @@ public class AgentEntity {
private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
private String summaryMemory;
@Schema(description = "语言编码")
@@ -1,53 +0,0 @@
package xiaozhi.modules.agent.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@TableName("ai_agent_snapshot")
@Schema(description = "智能体配置快照")
public class AgentSnapshotEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "快照ID")
private String id;
@Schema(description = "智能体ID")
private String agentId;
@Schema(description = "所属用户ID")
private Long userId;
@Schema(description = "版本号")
private Integer versionNo;
@Schema(description = "快照数据JSON")
private String snapshotData;
@Schema(description = "变更字段JSON")
private String changedFields;
@Schema(description = "快照来源")
private String source;
@Schema(description = "恢复来源快照ID")
private String restoreFromSnapshotId;
@Schema(description = "恢复来源版本号")
private Integer restoreFromVersionNo;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "快照数据脱敏规则版本")
private Integer redactionVersion;
}
@@ -1,6 +1,6 @@
package xiaozhi.modules.agent.service;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
@@ -11,7 +11,7 @@ import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
* @version 1.0, 2025/5/8
* @since 1.0.0
*/
public interface AgentChatAudioService extends IRepository<AgentChatAudioEntity> {
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
/**
* 保存音频数据
*
@@ -3,7 +3,7 @@ package xiaozhi.modules.agent.service;
import java.util.List;
import java.util.Map;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
@@ -18,7 +18,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
public interface AgentChatHistoryService extends IRepository<AgentChatHistoryEntity> {
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> {
/**
* 根据智能体ID获取会话列表
@@ -37,14 +37,6 @@ public interface AgentChatHistoryService extends IRepository<AgentChatHistoryEnt
*/
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
/**
* 根据会话ID获取智能体ID
*
* @param sessionId 会话ID
* @return 智能体ID
*/
String getAgentIdBySessionId(String sessionId);
/**
* 根据智能体ID删除聊天记录
*
@@ -70,14 +62,6 @@ public interface AgentChatHistoryService extends IRepository<AgentChatHistoryEnt
*/
String getContentByAudioId(String audioId);
/**
* 根据音频ID获取智能体ID
*
* @param audioId 音频ID
* @return 智能体ID
*/
String getAgentIdByAudioId(String audioId);
/**
* 查询此音频id是否属于此智能体
@@ -2,7 +2,7 @@ package xiaozhi.modules.agent.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
@@ -10,7 +10,7 @@ import xiaozhi.modules.agent.entity.AgentPluginMapping;
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service
* @createDate 2025-05-25 22:33:17
*/
public interface AgentPluginMappingService extends IRepository<AgentPluginMapping> {
public interface AgentPluginMappingService extends IService<AgentPluginMapping> {
/**
* 根据智能体id获取插件参数
@@ -26,11 +26,4 @@ public interface AgentPluginMappingService extends IRepository<AgentPluginMappin
* @param agentId
*/
void deleteByAgentId(String agentId);
/**
* 根据插件ID删除所有智能体的插件映射
*
* @param pluginId 插件ID
*/
void deleteByPluginId(String pluginId);
}
@@ -7,7 +7,6 @@ import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.vo.AgentInfoVO;
@@ -36,15 +35,6 @@ public interface AgentService extends BaseService<AgentEntity> {
*/
AgentInfoVO getAgentById(String id);
/**
* 根据ID获取当前用户有权访问的智能体
*
* @param id 智能体ID
* @param userId 当前用户ID
* @return 智能体实体
*/
AgentInfoVO getAgentById(String id, Long userId);
/**
* 插入智能体
*
@@ -60,13 +50,6 @@ public interface AgentService extends BaseService<AgentEntity> {
*/
void deleteAgentByUserId(Long userId);
/**
* 删除智能体及其关联数据
*
* @param agentId 智能体ID
*/
void deleteAgent(String agentId);
/**
* 获取用户智能体列表
*
@@ -110,41 +93,6 @@ public interface AgentService extends BaseService<AgentEntity> {
*/
void updateAgentById(String agentId, AgentUpdateDTO dto);
/**
* 更新当前用户有权访问的智能体
*
* @param agentId 智能体ID
* @param dto 更新智能体所需的信息
* @param userId 当前用户ID
*/
void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId);
/**
* 根据设备MAC地址更新当前用户有权访问的智能体记忆
*
* @param macAddress 设备MAC地址
* @param dto 智能体记忆
* @param userId 当前用户ID
*/
void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId);
/**
* 删除当前用户有权访问的智能体
*
* @param agentId 智能体ID
* @param userId 当前用户ID
*/
void deleteAgentById(String agentId, Long userId);
/**
* 更新智能体
*
* @param agentId 智能体ID
* @param dto 更新智能体所需的信息
* @param createSnapshot 是否创建配置快照
*/
void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot);
/**
* 创建智能体
*
@@ -1,25 +0,0 @@
package xiaozhi.modules.agent.service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
public interface AgentSnapshotService extends BaseService<AgentSnapshotEntity> {
void createSnapshot(String agentId, String source);
PageData<AgentSnapshotVO> page(String agentId, AgentSnapshotPageDTO params);
AgentSnapshotVO getSnapshot(String agentId, String snapshotId);
void restoreSnapshot(String agentId, String snapshotId, String currentStateToken);
void deleteSnapshot(String agentId, String snapshotId);
Integer getCurrentVersionNo(String agentId);
void deleteByAgentId(String agentId);
long redactLegacySnapshots();
}
@@ -1,6 +1,6 @@
package xiaozhi.modules.agent.service;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
@@ -9,7 +9,7 @@ import xiaozhi.modules.agent.entity.AgentTemplateEntity;
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service
* @createDate 2025-03-22 11:48:18
*/
public interface AgentTemplateService extends IRepository<AgentTemplateEntity> {
public interface AgentTemplateService extends IService<AgentTemplateEntity> {
/**
* 获取默认模板
@@ -2,7 +2,7 @@ package xiaozhi.modules.agent.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
@@ -16,7 +16,7 @@ import xiaozhi.modules.agent.service.AgentChatAudioService;
* @since 1.0.0
*/
@Service
public class AgentChatAudioServiceImpl extends CrudRepository<AiAgentChatAudioDao, AgentChatAudioEntity>
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity>
implements AgentChatAudioService {
@Override
public String saveAudio(byte[] audioData) {
@@ -14,7 +14,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
@@ -39,7 +39,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
*/
@Service
@RequiredArgsConstructor
public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHistoryDao, AgentChatHistoryEntity>
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
implements AgentChatHistoryService {
private final AgentChatTitleService agentChatTitleService;
@@ -88,19 +88,6 @@ public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHisto
return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class);
}
@Override
public String getAgentIdBySessionId(String sessionId) {
if (sessionId == null || sessionId.isBlank()) {
return null;
}
AgentChatHistoryEntity entity = baseMapper.selectOne(
new LambdaQueryWrapper<AgentChatHistoryEntity>()
.select(AgentChatHistoryEntity::getAgentId)
.eq(AgentChatHistoryEntity::getSessionId, sessionId)
.last("LIMIT 1"));
return entity == null ? null : entity.getAgentId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
@@ -167,7 +154,7 @@ public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHisto
// 尝试解析为 JSON
try {
Map<String, Object> jsonMap = JsonUtils.parseMap(content);
Map<String, Object> jsonMap = JsonUtils.parseObject(content, Map.class);
if (jsonMap != null && jsonMap.containsKey("content")) {
Object contentObj = jsonMap.get("content");
return contentObj != null ? contentObj.toString() : content;
@@ -189,19 +176,6 @@ public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHisto
return agentChatHistoryEntity == null ? null : agentChatHistoryEntity.getContent();
}
@Override
public String getAgentIdByAudioId(String audioId) {
if (audioId == null || audioId.isBlank()) {
return null;
}
AgentChatHistoryEntity entity = baseMapper.selectOne(
new LambdaQueryWrapper<AgentChatHistoryEntity>()
.select(AgentChatHistoryEntity::getAgentId)
.eq(AgentChatHistoryEntity::getAudioId, audioId)
.last("LIMIT 1"));
return entity == null ? null : entity.getAgentId();
}
@Override
public boolean isAudioOwnedByAgent(String audioId, String agentId) {
// 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体
@@ -118,7 +118,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
{
setSummaryMemory(summaryDTO.getSummary());
}
}, false);
});
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
} else {
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
@@ -76,7 +76,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 等待初始化响应 (id=1) - 移除固定延迟,改为响应驱动
List<String> initResponses = client.listenerWithoutClose(response -> {
try {
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
// 检查是否有result字段,表示初始化成功
return jsonMap.containsKey("result") && !jsonMap.containsKey("error");
@@ -92,7 +92,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
boolean initSucceeded = false;
for (String response : initResponses) {
try {
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
if (jsonMap.containsKey("result")) {
log.info("MCP初始化成功,智能体ID: {}", id);
@@ -123,7 +123,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 等待工具列表响应 (id=2)
List<String> toolsResponses = client.listener(response -> {
try {
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
return jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"));
} catch (Exception e) {
log.warn("解析工具列表响应失败: {}", response, e);
@@ -134,18 +134,18 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 处理工具列表响应
for (String response : toolsResponses) {
try {
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
if (jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"))) {
// 检查是否有result字段
Object resultObj = jsonMap.get("result");
if (resultObj instanceof Map<?, ?>) {
Map<String, Object> resultMap = JsonUtils.toStringObjectMap(resultObj);
if (resultObj instanceof Map) {
Map<String, Object> resultMap = (Map<String, Object>) resultObj;
Object toolsObj = resultMap.get("tools");
if (toolsObj instanceof List<?>) {
List<Map<String, Object>> toolsList = JsonUtils.toStringObjectMapList(toolsObj);
if (toolsObj instanceof List) {
List<Map<String, Object>> toolsList = (List<Map<String, Object>>) toolsObj;
// 提取工具名称列表
List<String> result = toolsList.stream()
.map(tool -> String.class.cast(tool.get("name")))
.map(tool -> (String) tool.get("name"))
.filter(name -> name != null)
.sorted()
.collect(Collectors.toList());
@@ -9,7 +9,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -29,7 +29,7 @@ import xiaozhi.modules.model.service.ModelConfigService;
@Service
@RequiredArgsConstructor
@Slf4j
public class AgentPluginMappingServiceImpl extends CrudRepository<AgentPluginMappingMapper, AgentPluginMapping>
public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappingMapper, AgentPluginMapping>
implements AgentPluginMappingService {
private final AgentPluginMappingMapper agentPluginMappingMapper;
private final KnowledgeBaseService knowledgeBaseService;
@@ -104,11 +104,4 @@ public class AgentPluginMappingServiceImpl extends CrudRepository<AgentPluginMap
agentPluginMappingMapper.delete(updateWrapper);
}
@Override
public void deleteByPluginId(String pluginId) {
UpdateWrapper<AgentPluginMapping> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("plugin_id", pluginId);
agentPluginMappingMapper.delete(updateWrapper);
}
}
@@ -15,8 +15,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
@@ -34,7 +34,6 @@ import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.dao.AgentTagDao;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
import xiaozhi.modules.agent.dto.AgentTagDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
@@ -46,7 +45,6 @@ import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentContextProviderService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentSnapshotService;
import xiaozhi.modules.agent.service.AgentTagService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentInfoVO;
@@ -78,7 +76,6 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
private final AgentContextProviderService agentContextProviderService;
private final AgentTagService agentTagService;
private final CorrectWordFileService correctWordFileService;
private final AgentSnapshotService agentSnapshotService;
@Override
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
@@ -95,7 +92,6 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
if (agent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
requireCurrentUserPermissionIfPresent(agent);
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
@@ -113,71 +109,11 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 查询替换词文件ID列表
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
agent.setCorrectWordFileIds(correctWordFileIds);
agent.setCurrentVersionNo(agentSnapshotService.getCurrentVersionNo(id));
// 无需额外查询插件列表,已通过SQL查询出来
return agent;
}
@Override
public AgentInfoVO getAgentById(String id, Long userId) {
AgentInfoVO agent = getAgentById(id);
requireAgentPermission(agent, userId);
return agent;
}
private AgentEntity getAgentEntityOrThrow(String agentId) {
AgentEntity agent = agentDao.selectById(agentId);
if (agent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
return agent;
}
private boolean isCurrentUserSuperAdmin() {
UserDetail user = SecurityUser.getUser();
return user != null && Integer.valueOf(SuperAdminEnum.YES.value()).equals(user.getSuperAdmin());
}
private void requireCurrentUserPermissionIfPresent(AgentEntity agent) {
Long userId = SecurityUser.getUserId();
if (userId != null) {
requireAgentPermission(agent, userId);
}
}
private boolean hasAgentPermission(AgentEntity agent, Long userId) {
if (agent == null) {
return false;
}
if (isCurrentUserSuperAdmin()) {
return true;
}
return userId != null && userId.equals(agent.getUserId());
}
private void requireAgentPermission(AgentEntity agent, Long userId) {
if (!hasAgentPermission(agent, userId)) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
}
private boolean hasDevicePermission(DeviceEntity device, Long userId) {
if (device == null) {
return false;
}
if (isCurrentUserSuperAdmin()) {
return true;
}
return userId != null && userId.equals(device.getUserId());
}
private void requireDevicePermission(DeviceEntity device, Long userId) {
if (!hasDevicePermission(device, userId)) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
}
@Override
public boolean insert(AgentEntity entity) {
// 如果ID为空,自动生成一个UUID作为ID
@@ -199,30 +135,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAgentByUserId(Long userId) {
List<AgentEntity> agents = baseDao.selectList(new QueryWrapper<AgentEntity>()
.select("id")
.eq("user_id", userId));
for (AgentEntity agent : agents) {
deleteAgent(agent.getId());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAgent(String agentId) {
if (agentDao.selectByIdForUpdate(agentId) == null) {
return;
}
deviceService.deleteByAgentId(agentId);
agentChatHistoryService.deleteByAgentId(agentId, true, true);
agentPluginMappingService.deleteByAgentId(agentId);
agentContextProviderService.deleteByAgentId(agentId);
correctWordFileService.deleteMappingsByAgentId(agentId);
agentTagService.deleteAgentTags(agentId);
agentSnapshotService.deleteByAgentId(agentId);
deleteById(agentId);
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("user_id", userId);
baseDao.delete(wrapper);
}
@Override
@@ -338,46 +254,32 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
@Override
public boolean checkAgentPermission(String agentId, Long userId) {
AgentEntity agent = agentDao.selectById(agentId);
return hasAgentPermission(agent, userId);
if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) {
return false;
}
// 获取智能体信息
AgentEntity agent = getAgentById(agentId);
if (agent == null) {
return false;
}
// 如果是超级管理员,直接返回true
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) {
return true;
}
// 检查是否是智能体的所有者
return userId.equals(agent.getUserId());
}
// 根据id更新智能体信息
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
updateAgentById(agentId, dto, true);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId) {
updateAgentById(agentId, dto, userId, true);
}
// 根据id更新智能体信息
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot) {
updateAgentById(agentId, dto, null, createSnapshot);
}
private void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId, boolean createSnapshot) {
AgentEntity lockedAgent = agentDao.selectByIdForUpdate(agentId);
if (lockedAgent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
if (userId == null) {
requireCurrentUserPermissionIfPresent(lockedAgent);
} else {
requireAgentPermission(lockedAgent, userId);
}
// 锁定后查询现有实体和关联配置
// 先查询现有实体
AgentEntity existingEntity = this.getAgentById(agentId);
if (createSnapshot) {
int currentVersionNo = agentSnapshotService.getCurrentVersionNo(agentId);
agentSnapshotService.createSnapshot(agentId, currentVersionNo == 0 ? "initial" : "current");
if (existingEntity == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
// 只更新提供的非空字段
@@ -483,10 +385,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
.toList();
if (!toUpdate.isEmpty()) {
agentPluginMappingService.updateBatchById(toUpdate, IRepository.DEFAULT_BATCH_SIZE);
agentPluginMappingService.updateBatchById(toUpdate);
}
if (!toInsert.isEmpty()) {
agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
agentPluginMappingService.saveBatch(toInsert);
}
// 5. 删除本次不在提交列表里的插件映射
@@ -495,7 +397,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
.map(AgentPluginMapping::getId)
.toList();
if (!toDelete.isEmpty()) {
agentPluginMappingService.removeByIds(toDelete);
agentPluginMappingService.removeBatchByIds(toDelete);
}
}
@@ -528,42 +430,11 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
}
// 更新智能体标签
if (dto.getTagNames() != null || dto.getTagIds() != null) {
agentTagService.saveAgentTags(agentId, dto.getTagIds(), dto.getTagNames());
}
boolean b = validateLLMIntentParams(existingEntity.getLlmModelId(), existingEntity.getIntentModelId());
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
if (!b) {
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
}
this.updateById(existingEntity);
if (createSnapshot) {
agentSnapshotService.createSnapshot(agentId, "config");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId) {
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null || StringUtils.isBlank(device.getAgentId()) || dto == null) {
return;
}
requireDevicePermission(device, userId);
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
updateAgentById(device.getAgentId(), agentUpdateDTO, userId, false);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAgentById(String agentId, Long userId) {
AgentEntity agent = getAgentEntityOrThrow(agentId);
requireAgentPermission(agent, userId);
deleteAgent(agentId);
}
/**
@@ -619,16 +490,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
}
entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setTtsLanguage(defaultIfBlank(template.getTtsLanguage(),
timbreModelService.getDefaultLanguageById(entity.getTtsVoiceId())));
entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId());
entity.setSystemPrompt(template.getSystemPrompt());
entity.setSummaryMemory(template.getSummaryMemory());
if (Constant.MEMORY_NO_MEM.equals(entity.getMemModelId())
|| Constant.MEMORY_MEM_REPORT_ONLY.equals(entity.getMemModelId())) {
entity.setSummaryMemory("");
}
// 根据记忆模型类型设置默认的chatHistoryConf值
if (template.getMemModelId() != null) {
@@ -677,7 +542,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
mapping.setPluginId(pluginId);
Map<String, Object> paramInfo = new HashMap<>();
List<Map<String, Object>> fields = JsonUtils.parseMapList(provider.getFields());
List<Map<String, Object>> fields = JsonUtils.parseObject(provider.getFields(), List.class);
if (fields != null) {
for (Map<String, Object> field : fields) {
paramInfo.put((String) field.get("key"), field.get("default"));
@@ -688,15 +553,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
toInsert.add(mapping);
}
// 保存默认插件
agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
agentSnapshotService.createSnapshot(entity.getId(), "initial");
agentPluginMappingService.saveBatch(toInsert);
return entity.getId();
}
private String defaultIfBlank(String value, String defaultValue) {
return StringUtils.isBlank(value) ? defaultValue : value;
}
private String getDefaultLLMModelId() {
try {
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
@@ -1,59 +0,0 @@
package xiaozhi.modules.agent.service.impl;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.modules.agent.service.AgentSnapshotService;
@Slf4j
@Component
@RequiredArgsConstructor
public class AgentSnapshotRedactionRunner implements SmartInitializingSingleton {
static final long ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS = 5_000;
static final long ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS = 15_000;
private final AgentSnapshotService agentSnapshotService;
@Override
public void afterSingletonsInstantiated() {
redactAndReport("startup");
}
@Scheduled(initialDelay = ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS,
fixedDelay = ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS)
public void redactLateRollingDeploymentWrites() {
redactAndReport("rolling-deployment");
}
private void redactAndReport(String trigger) {
long startedAt = System.nanoTime();
try {
long migrated = agentSnapshotService.redactLegacySnapshots();
long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt);
if (migrated > 0) {
log.warn("Agent snapshot legacy redaction trigger={} migrated={} durationMs={}. Rotate credentials "
+ "that may have appeared in historical snapshot URLs, cookies, sessions, or structured "
+ "headers.", trigger, migrated, durationMillis);
} else if ("startup".equals(trigger)) {
log.info("Agent snapshot legacy redaction startup pass completed: migrated=0 durationMs={}; "
+ "rolling-deployment compensation starts after {} ms and repeats every {} ms.",
durationMillis, ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS,
ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS);
}
} catch (RuntimeException exception) {
if ("startup".equals(trigger)) {
log.error("Agent snapshot legacy redaction failed during startup; blocking application startup "
+ "before it can accept traffic.", exception);
} else {
log.error("Agent snapshot legacy redaction failed during rolling-deployment compensation; the "
+ "scheduler will retry on its next run.", exception);
}
throw exception;
}
}
}
@@ -133,7 +133,7 @@ public class AgentTagServiceImpl extends BaseServiceImpl<AgentTagDao, AgentTagEn
}
if (tagIds != null && !tagIds.isEmpty()) {
List<AgentTagEntity> tagIdEntities = baseDao.selectByIds(tagIds);
List<AgentTagEntity> tagIdEntities = baseDao.selectBatchIds(tagIds);
for (AgentTagEntity tag : tagIdEntities) {
if (tag != null && (currentTagNames.contains(tag.getTagName()) ||
newTagNames.contains(tag.getTagName()))) {
@@ -4,7 +4,7 @@ import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import xiaozhi.modules.agent.dao.AgentTemplateDao;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
@@ -21,7 +21,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
* @createDate 2025-03-22 11:48:18
*/
@Service
public class AgentTemplateServiceImpl extends CrudRepository<AgentTemplateDao, AgentTemplateEntity>
public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, AgentTemplateEntity>
implements AgentTemplateService {
/**
@@ -22,7 +22,7 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
@@ -46,7 +46,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
*/
@Service
@Slf4j
public class AgentVoicePrintServiceImpl extends CrudRepository<AgentVoicePrintDao, AgentVoicePrintEntity>
public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao, AgentVoicePrintEntity>
implements AgentVoicePrintService {
private final AgentChatAudioService agentChatAudioService;
private final RestTemplate restTemplate;
@@ -1,59 +0,0 @@
package xiaozhi.modules.agent.typehandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import com.fasterxml.jackson.core.type.TypeReference;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.dto.ContextProviderDTO;
/**
* JSON type handler for context providers.
*
* <p>Do not extend MyBatis-Plus {@code AbstractJsonTypeHandler}: its constructor and JSON handler
* contract changed in MyBatis-Plus 3.5.6. {@link BaseTypeHandler} is part of MyBatis itself and
* keeps this handler compatible with both 3.5.5 and newer MyBatis-Plus releases.</p>
*/
public class ContextProviderListTypeHandler extends BaseTypeHandler<List<ContextProviderDTO>> {
private static final TypeReference<List<ContextProviderDTO>> CONTEXT_PROVIDER_LIST_TYPE = new TypeReference<>() {
};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<ContextProviderDTO> parameter,
JdbcType jdbcType) throws SQLException {
ps.setString(i, JsonUtils.toJsonString(parameter));
}
@Override
public List<ContextProviderDTO> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseNullable(rs.getString(columnName));
}
@Override
public List<ContextProviderDTO> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseNullable(rs.getString(columnIndex));
}
@Override
public List<ContextProviderDTO> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseNullable(cs.getString(columnIndex));
}
private List<ContextProviderDTO> parseNullable(String json) {
if (StringUtils.isBlank(json)) {
return null;
}
List<ContextProviderDTO> providers = JsonUtils.parseObject(json, CONTEXT_PROVIDER_LIST_TYPE);
return providers == null ? Collections.emptyList() : providers;
}
}
@@ -1,5 +1,7 @@
package xiaozhi.modules.agent.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -18,6 +20,7 @@ import java.util.List;
public class AgentInfoVO extends AgentEntity
{
@Schema(description = "插件列表Id")
@TableField(typeHandler = JacksonTypeHandler.class)
private List<AgentPluginMapping> functions;
@Schema(description = "上下文源配置")
@@ -25,7 +28,4 @@ public class AgentInfoVO extends AgentEntity
@Schema(description = "替换词文件ID列表")
private List<String> correctWordFileIds;
@Schema(description = "当前配置版本号")
private Integer currentVersionNo;
}
@@ -1,34 +0,0 @@
package xiaozhi.modules.agent.vo;
import java.util.Date;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
@Data
@Schema(description = "智能体配置快照")
public class AgentSnapshotVO {
private String id;
private String agentId;
@Schema(description = "所属用户ID,表示该快照归属的智能体所有者")
private Long userId;
private Integer versionNo;
private List<String> changedFields;
private List<String> fieldOrder;
private String source;
@Schema(description = "恢复来源快照ID,仅恢复结果版本有值")
private String restoreFromSnapshotId;
@Schema(description = "恢复来源版本号,仅恢复结果版本有值")
private Integer restoreFromVersionNo;
@Schema(description = "创建者,表示触发本次快照写入的操作人")
private Long creator;
private Date createdAt;
private AgentSnapshotDataDTO snapshotData;
private AgentSnapshotDataDTO afterSnapshotData;
@Schema(description = "恢复预览对应的脱敏当前配置,仅详情接口有值")
private AgentSnapshotDataDTO currentSnapshotData;
@Schema(description = "恢复预览对应的当前配置状态指纹,仅详情接口有值")
private String currentStateToken;
}
@@ -9,7 +9,6 @@ import xiaozhi.common.constant.Constant;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.sys.service.SysParamsService;
@Configuration
@@ -25,9 +24,6 @@ public class SystemInitConfig {
@Autowired
private RedisUtils redisUtils;
@Autowired
private DeviceAddressBookService deviceAddressBookService;
@PostConstruct
public void init() {
// 检查版本号
@@ -41,8 +37,5 @@ public class SystemInitConfig {
sysParamsService.initServerSecret();
configService.getConfig(false);
// 初始化设备通讯录缓存
deviceAddressBookService.refreshCache();
}
}
@@ -10,7 +10,7 @@ public interface ConfigService {
* @param isCache 是否缓存
* @return 配置信息
*/
Map<String, Object> getConfig(Boolean isCache);
Object getConfig(Boolean isCache);
/**
* 获取智能体模型配置
@@ -65,12 +65,12 @@ public class ConfigServiceImpl implements ConfigService {
private final CorrectWordFileService correctWordFileService;
@Override
public Map<String, Object> getConfig(Boolean isCache) {
public Object getConfig(Boolean isCache) {
if (isCache) {
// 先从Redis获取配置
Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey());
if (cachedConfig != null) {
return JsonUtils.toStringObjectMap(cachedConfig);
return cachedConfig;
}
}
@@ -123,7 +123,7 @@ public class ConfigServiceImpl implements ConfigService {
if (isAdminRequest != null && "true".equals(isAdminRequest)) {
// 管理控制台请求,返回getConfig的结果
redisUtils.delete(redisKey); // 使用后清理
return getConfig(true);
return (Map<String, Object>) getConfig(true);
}
// 根据MAC地址查找设备
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
@@ -277,10 +277,10 @@ public class ConfigServiceImpl implements ConfigService {
// 遍历除最后一个key之外的所有key
for (int i = 0; i < keys.length - 1; i++) {
String key = keys[i];
Object nestedConfig = current.computeIfAbsent(key, ignored -> new HashMap<String, Object>());
Map<String, Object> nestedMap = JsonUtils.toStringObjectMap(nestedConfig);
current.put(key, nestedMap);
current = nestedMap;
if (!current.containsKey(key)) {
current.put(key, new HashMap<String, Object>());
}
current = (Map<String, Object>) current.get(key);
}
// 处理最后一个key
@@ -8,7 +8,6 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -23,17 +22,13 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
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.DeviceRegisterDTO;
import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO;
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService;
@@ -42,14 +37,11 @@ import xiaozhi.modules.sys.service.SysParamsService;
@RequestMapping("/device")
public class DeviceController {
private final DeviceService deviceService;
private final DeviceAddressBookService deviceAddressBookService;
private final RedisUtils redisUtils;
private final SysParamsService sysParamsService;
public DeviceController(DeviceService deviceService, DeviceAddressBookService deviceAddressBookService,
RedisUtils redisUtils, SysParamsService sysParamsService) {
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService) {
this.deviceService = deviceService;
this.deviceAddressBookService = deviceAddressBookService;
this.redisUtils = redisUtils;
this.sysParamsService = sysParamsService;
}
@@ -86,10 +78,10 @@ public class DeviceController {
@GetMapping("/bind/{agentId}")
@Operation(summary = "获取已绑定设备")
@RequiresPermissions("sys:role:normal")
public Result<List<UserShowDeviceListVO>> getUserDevices(@PathVariable String agentId) {
public Result<List<DeviceEntity>> getUserDevices(@PathVariable String agentId) {
UserDetail user = SecurityUser.getUser();
List<UserShowDeviceListVO> devices = deviceService.getUserDeviceList(user.getId(), agentId);
return new Result<List<UserShowDeviceListVO>>().ok(devices);
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
return new Result<List<DeviceEntity>>().ok(devices);
}
@PostMapping("/bind/{agentId}")
@@ -125,9 +117,7 @@ public class DeviceController {
return new Result<Void>().error("设备不存在");
}
BeanUtils.copyProperties(deviceUpdateDTO, entity);
if (!deviceService.updateById(entity)) {
return new Result<Void>().error(ErrorCode.UPDATE_DATA_FAILED);
}
deviceService.updateById(entity);
return new Result<Void>();
}
@@ -169,48 +159,4 @@ public class DeviceController {
response.setMsg("Tools called successfully");
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,7 +1,6 @@
package xiaozhi.modules.device.controller;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
@@ -79,8 +78,8 @@ public class OTAController {
@Hidden
public ResponseEntity<String> getOTA() {
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
if (!isNativeMqttReady() && !isConfiguredValue(mqttUdpConfig)) {
return ResponseEntity.ok("OTA接口不正常,缺少mqtt_gateway地址或未启用原生MQTT,请登录智控台,在参数管理找到【server.mqtt_gateway】或【mqtt_server.enabled/protocols.mqtt_enabled】配置");
if (StringUtils.isBlank(mqttUdpConfig)) {
return ResponseEntity.ok("OTA接口不正常,缺少mqtt_gateway地址,请登录智控台,在参数管理找到【server.mqtt_gateway】配置");
}
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
@@ -93,49 +92,6 @@ public class OTAController {
return ResponseEntity.ok("OTA接口运行正常,websocket集群数量:" + wsUrl.split(";").length);
}
private boolean isNativeMqttEnabled() {
String enabled = sysParamsService.getValue(Constant.MQTT_SERVER_ENABLED, false);
boolean serverEnabled = isConfiguredValue(enabled)
? isTrue(enabled)
: isTrue(sysParamsService.getValue(Constant.SERVER_MQTT_ENABLED, false));
boolean protocolEnabled = isTrue(
sysParamsService.getValue(Constant.PROTOCOLS_MQTT_ENABLED, false));
if (!protocolEnabled) {
String enabledProtocols = sysParamsService.getValue(Constant.PROTOCOLS_ENABLED, false);
if (StringUtils.isNotBlank(enabledProtocols)) {
String normalized = enabledProtocols
.replace("[", "")
.replace("]", "")
.replace("\"", "");
protocolEnabled = Arrays.stream(normalized.split("[;,\\s]+"))
.anyMatch("mqtt"::equalsIgnoreCase);
}
}
return serverEnabled && protocolEnabled;
}
private boolean isNativeMqttReady() {
if (!isNativeMqttEnabled()) {
return false;
}
String signatureKey = sysParamsService.getValue(
Constant.MQTT_SERVER_SIGNATURE_KEY, false);
if (!isConfiguredValue(signatureKey)) {
signatureKey = sysParamsService.getValue(
Constant.SERVER_MQTT_SECRET, false);
}
return isConfiguredValue(signatureKey);
}
private boolean isTrue(String value) {
return "true".equalsIgnoreCase(value) || "1".equals(value);
}
private boolean isConfiguredValue(String value) {
return StringUtils.isNotBlank(value) && !"null".equalsIgnoreCase(value.trim());
}
@SneakyThrows
private ResponseEntity<String> createResponse(DeviceReportRespDTO deviceReportRespDTO) {
ObjectMapper objectMapper = new ObjectMapper();
@@ -1,39 +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> {
/**
* 新增设备通讯录记录
*/
int insertAddressBook(DeviceAddressBookEntity entity);
/**
* 获取设备通讯录列表
*/
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<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);
}
}
@@ -1,307 +0,0 @@
package xiaozhi.modules.device.service.impl;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import cn.hutool.json.JSONUtil;
import xiaozhi.common.constant.Constant;
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.entity.DeviceEntity;
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) {
DeviceEntity callerDevice =
deviceService.getDeviceByMacAddress(callerMac);
if (callerDevice == null) {
return errorResult("接听失败,设备信息不存在");
}
return postCallAccept(
buildMqttClientId(callerDevice),
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("呼叫失败,您没有权限呼叫该设备");
}
DeviceEntity callerDevice =
deviceService.getDeviceByMacAddress(callerMac);
DeviceEntity targetDevice =
deviceService.getDeviceByMacAddress(targetMac);
if (callerDevice == null || targetDevice == null) {
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 = callerDevice.getAlias();
if (StringUtils.isBlank(callerNickname)) {
callerNickname = formatMacAsDeviceName(callerMac);
}
}
return postCallRequest(
buildMqttClientId(callerDevice),
buildMqttClientId(targetDevice),
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.insertAddressBook(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> postCallRequest(
String callerClientId,
String targetClientId,
Map<String, Object> body) {
try {
MqttManagementHttpClient.Response response =
createMqttManagementRouter().sendCallRequest(
callerClientId, targetClientId, body);
return parseCallResponse(response, "呼叫");
} catch (Exception e) {
return errorResult("呼叫失败,请稍后再试");
}
}
private Map<String, Object> postCallAccept(
String clientId,
Map<String, Object> body) {
try {
MqttManagementHttpClient.Response response =
createMqttManagementRouter().sendCallAccept(
clientId, body);
return parseCallResponse(response, "接听");
} catch (Exception e) {
return errorResult("接听失败,请稍后再试");
}
}
private Map<String, Object> parseCallResponse(
MqttManagementHttpClient.Response response,
String action) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
if (response == null || StringUtils.isBlank(response.body())) {
result.put("message", action + "失败,MQTT管理配置缺失");
return result;
}
try {
Map<String, Object> backendResult =
JSONUtil.parseObj(response.body());
result.put("status", backendResult.get("status"));
result.put("message", backendResult.get("message"));
if (backendResult.containsKey("code")) {
result.put("code", backendResult.get("code"));
}
return result;
} catch (Exception e) {
result.put("message", action + "失败,请稍后再试");
return result;
}
}
MqttManagementRouter createMqttManagementRouter() {
return new MqttManagementRouter(
new MqttManagementEndpointResolver(sysParamsService),
new MqttManagementHttpClient());
}
private String buildMqttClientId(DeviceEntity device) {
return MqttClientId.build(
device.getBoard(), device.getMacAddress());
}
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;
}
}
@@ -5,13 +5,10 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -33,9 +30,15 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
@@ -52,7 +55,6 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.device.dao.DeviceDao;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
@@ -61,7 +63,6 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.entity.OtaEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.service.OtaService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
@@ -79,7 +80,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
private final SysParamsService sysParamsService;
private final RedisUtils redisUtils;
private final OtaService otaService;
private final DeviceAddressBookService deviceAddressBookService;
@Async
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
@@ -112,7 +112,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
String deviceId = (String) cacheDeviceId;
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(cacheDeviceKey));
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
if (ToolUtil.isEmpty(cacheMap)) {
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
}
@@ -159,20 +159,39 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
*/
@Override
public String getDeviceOnlineData(String agentId) {
// 从系统参数中获取MQTT网关地址
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
return "";
}
// 构建完整的URL
String url = StrUtil.format("http://{}/api/devices/status", mqttGatewayUrl);
// 获取当前用户的设备列表
UserDetail user = SecurityUser.getUser();
List<DeviceEntity> devices = getUserDevices(user.getId(), agentId);
// 构建deviceIds数组
Set<String> deviceIds = devices.stream()
.map(device -> MqttClientId.build(
device.getBoard(), device.getMacAddress()))
.collect(Collectors.toSet());
Set<String> deviceIds = devices.stream().map(o -> {
String macAddress = Optional.ofNullable(o.getMacAddress()).orElse("unknown").replace(":", "_");
String groupId = Optional.ofNullable(o.getBoard()).orElse("GID_default").replace(":", "_");
return StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress);
}).collect(Collectors.toSet());
// 构建请求入参
Map<String, Set<String>> params = MapUtil
.builder(new HashMap<String, Set<String>>())
.put("clientIds", deviceIds).build();
if (ToolUtil.isNotEmpty(deviceIds)) {
return createMqttManagementRouter()
.getMergedStatus(deviceIds);
// 发送请求
String resultMessage = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
.body(JSONUtil.toJsonStr(params))
.timeout(10000) // 超时,毫秒
.execute().body();
return resultMessage;
}
// 返回响应
return "";
@@ -184,15 +203,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
response.setServer_time(buildServerTime());
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
String reportedBoard = deviceReport.getBoard() == null
? null
: deviceReport.getBoard().getType();
if (deviceById != null
&& StringUtils.isBlank(deviceById.getBoard())
&& StringUtils.isNotBlank(reportedBoard)) {
deviceById.setBoard(reportedBoard);
baseDao.updateById(deviceById);
}
// 设备未绑定,则返回当前上传的固件信息(不更新)以此兼容旧固件版本
if (deviceById == null) {
@@ -201,9 +211,10 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
response.setFirmware(firmware);
} else {
// 只有在设备已绑定且明确开启自动升级时才返回固件升级信息
if (Integer.valueOf(1).equals(deviceById.getAutoUpdate())) {
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(reportedBoard,
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
if (deviceById.getAutoUpdate() != 0) {
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
response.setFirmware(firmware);
}
@@ -246,35 +257,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
response.setWebsocket(websocket);
// UDP key/nonce/endpoint is negotiated by the MQTT hello response.
// OTA only needs to provide the MQTT connection parameters.
String groupId = "GID_default";
if (deviceById != null && StringUtils.isNotBlank(deviceById.getBoard())) {
groupId = deviceById.getBoard();
} else if (deviceReport.getBoard() != null && StringUtils.isNotBlank(deviceReport.getBoard().getType())) {
groupId = deviceReport.getBoard().getType();
}
boolean mqttConfigured = false;
if (isNativeMqttEnabled()) {
try {
DeviceReportRespDTO.MQTT mqtt = buildNativeMqttConfig(macAddress, groupId, clientId);
String endpoint = buildNativeMqttEndpoint();
if (mqtt != null && StringUtils.isNotBlank(endpoint)) {
mqtt.setEndpoint(endpoint);
response.setMqtt(mqtt);
mqttConfigured = true;
}
} catch (Exception e) {
log.error("生成原生MQTT配置失败: {}", e.getMessage());
}
}
if (!mqttConfigured) {
// Native 未启用或配置无效时继续兼容 MQTT Gateway。
// 添加MQTT UDP配置
// 从系统参数获取MQTT Gateway地址,仅在配置有效时使用
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, true);
if (mqttUdpConfig != null && !mqttUdpConfig.equals("null") && !mqttUdpConfig.isEmpty()) {
try {
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
: "GID_default";
DeviceReportRespDTO.MQTT mqtt = buildMqttConfig(macAddress, groupId);
if (mqtt != null) {
mqtt.setEndpoint(mqttUdpConfig);
@@ -284,7 +273,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
log.error("生成MQTT配置失败: {}", e.getMessage());
}
}
}
if (deviceById != null) {
// 如果设备存在,则异步更新上次连接时间和版本信息
@@ -310,34 +298,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
return baseDao.selectList(wrapper);
}
@Override
public List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId) {
List<DeviceEntity> devices = getUserDevices(userId, agentId);
return devices.stream().map(this::toUserShowDeviceListVO).toList();
}
private UserShowDeviceListVO toUserShowDeviceListVO(DeviceEntity device) {
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
vo.setDeviceType(device.getBoard());
vo.setBoard(device.getBoard());
vo.setAutoUpdate(device.getAutoUpdate());
vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate()));
vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt()));
return vo;
}
private Long toTimestamp(Date date) {
return date == null ? null : date.getTime();
}
@Override
public void unbindDevice(Long userId, String deviceId) {
// 先查询设备信息,获取agentId和macAddress
// 先查询设备信息,获取agentId
DeviceEntity device = baseDao.selectById(deviceId);
if (device == null) {
return;
}
String macAddress = device.getMacAddress();
if (StringUtils.isNotBlank(device.getAgentId())) {
// 清除智能体设备数量缓存
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
@@ -347,9 +314,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
wrapper.eq("user_id", userId);
wrapper.eq("id", deviceId);
baseDao.delete(wrapper);
// 删除设备相关的通讯录权限记录
deviceAddressBookService.deleteByMacAddresses(Collections.singletonList(macAddress));
}
@Override
@@ -368,23 +332,9 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override
public void deleteByAgentId(String agentId) {
// 先查询该智能体下的所有设备,获取mac地址用于删除通讯录记录
QueryWrapper<DeviceEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("agent_id", agentId);
List<DeviceEntity> devices = baseDao.selectList(queryWrapper);
// 删除设备
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("agent_id", agentId);
baseDao.delete(wrapper);
// 批量删除这些设备相关的所有通讯录权限记录
if (!devices.isEmpty()) {
List<String> macAddresses = devices.stream()
.map(DeviceEntity::getMacAddress)
.collect(Collectors.toList());
deviceAddressBookService.deleteByMacAddresses(macAddresses);
}
}
@Override
@@ -400,11 +350,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords()));
// 循环处理page获取回来的数据,返回需要的字段
List<UserShowDeviceListVO> list = page.getRecords().stream().map(device -> {
UserShowDeviceListVO vo = toUserShowDeviceListVO(device);
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
// 把最后修改的时间,改为简短描述的时间
vo.setRecentChatTime(DateUtils.getShortTime(device.getUpdateDate()));
sysUserUtilService.assignUsername(device.getUserId(),
vo::setBindUserName);
vo.setDeviceType(device.getBoard());
return vo;
}).toList();
// 计算页数
@@ -434,7 +385,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
public String geCodeByDeviceId(String deviceId) {
String dataKey = getDeviceCacheKey(deviceId);
Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(dataKey));
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey);
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
String cachedCode = (String) cacheMap.get("activation_code");
return cachedCode;
@@ -654,210 +605,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
return String.format("%s.%d", signatureBase64, timestamp);
}
private boolean isNativeMqttEnabled() {
String enabled = sysParamsService.getValue(Constant.MQTT_SERVER_ENABLED, true);
String legacyEnabled = sysParamsService.getValue(Constant.SERVER_MQTT_ENABLED, true);
boolean serverEnabled = isConfiguredValue(enabled)
? isTrue(enabled)
: isTrue(legacyEnabled);
String protoEnabled = sysParamsService.getValue(Constant.PROTOCOLS_MQTT_ENABLED, true);
String enabledProtocols = sysParamsService.getValue(Constant.PROTOCOLS_ENABLED, true);
boolean protocolEnabled = isTrue(protoEnabled);
if (!protocolEnabled && StringUtils.isNotBlank(enabledProtocols)) {
String normalizedProtocols = enabledProtocols
.replace("[", "")
.replace("]", "")
.replace("\"", "");
protocolEnabled = Arrays.stream(normalizedProtocols.split("[;,\\s]+"))
.anyMatch("mqtt"::equalsIgnoreCase);
}
return serverEnabled && protocolEnabled;
}
private boolean isTrue(String value) {
return "true".equalsIgnoreCase(value) || "1".equals(value);
}
private boolean isConfiguredValue(String value) {
return StringUtils.isNotBlank(value)
&& !"null".equalsIgnoreCase(value.trim())
&& !value.contains("");
}
private String buildNativeMqttEndpoint() {
String publicEndpoint = sysParamsService.getValue(Constant.MQTT_SERVER_PUBLIC_ENDPOINT, true);
String host = sysParamsService.getValue(Constant.MQTT_SERVER_HOST, true);
EndpointParts endpointParts = parseEndpoint(publicEndpoint);
EndpointParts hostParts = parseEndpoint(host);
if ((isConfiguredValue(publicEndpoint) && endpointParts == null)
|| (isConfiguredValue(host) && hostParts == null)) {
return null;
}
EndpointParts selectedEndpoint = null;
if (endpointParts != null && isClientReachableMqttHost(endpointParts.host)) {
selectedEndpoint = endpointParts;
} else if (hostParts != null && isClientReachableMqttHost(hostParts.host)) {
selectedEndpoint = hostParts;
}
if (selectedEndpoint == null) {
return null;
}
String mqttHost = selectedEndpoint.host;
Integer port = selectedEndpoint.port;
if (port == null) {
port = parseInt(sysParamsService.getValue(Constant.MQTT_SERVER_PORT, true), 1883);
}
if (StringUtils.isBlank(mqttHost) || port == null) {
return null;
}
return formatEndpoint(mqttHost, port);
}
private boolean isClientReachableMqttHost(String host) {
if (StringUtils.isBlank(host)) {
return false;
}
String normalized = host.trim().toLowerCase(Locale.ROOT);
return !"null".equals(normalized)
&& !"localhost".equals(normalized)
&& !"localhost.localdomain".equals(normalized)
&& !"0.0.0.0".equals(normalized)
&& !normalized.startsWith("127.");
}
private Integer parseInt(String value, Integer defaultValue) {
if (StringUtils.isBlank(value) || "null".equalsIgnoreCase(value)) {
return defaultValue;
}
try {
int port = Integer.parseInt(value);
if (port < 1 || port > 65535) {
return null;
}
return port;
} catch (NumberFormatException e) {
return null;
}
}
private String formatEndpoint(String host, int port) {
String normalizedHost = host.trim();
if (normalizedHost.indexOf(':') >= 0) {
return null;
}
return normalizedHost + ":" + port;
}
private static class EndpointParts {
private final String host;
private final Integer port;
private EndpointParts(String host, Integer port) {
this.host = host;
this.port = port;
}
}
private EndpointParts parseEndpoint(String raw) {
if (StringUtils.isBlank(raw) || "null".equalsIgnoreCase(raw)) {
return new EndpointParts(null, null);
}
String trimmed = raw.trim();
trimmed = trimmed.replaceFirst("(?i)^(mqtt|tcp|ssl|ws|wss|http|https)://", "");
int slashIdx = trimmed.indexOf('/');
if (slashIdx >= 0) {
trimmed = trimmed.substring(0, slashIdx);
}
if (StringUtils.isBlank(trimmed) || trimmed.startsWith("[")
|| trimmed.indexOf(':') != trimmed.lastIndexOf(':')) {
return null;
}
String host = trimmed;
Integer port = null;
int colon = trimmed.lastIndexOf(':');
if (colon >= 0) {
if (colon == 0 || colon == trimmed.length() - 1) {
return null;
}
port = parseInt(trimmed.substring(colon + 1), null);
if (port == null) {
return null;
}
host = trimmed.substring(0, colon);
}
if (!isValidEndpointHost(host)) {
return null;
}
return new EndpointParts(host, port);
}
private boolean isValidEndpointHost(String host) {
if (StringUtils.isBlank(host)
|| host.length() > 253
|| host.chars().anyMatch(Character::isWhitespace)
|| host.indexOf('@') >= 0
|| host.indexOf('?') >= 0
|| host.indexOf('#') >= 0
|| host.indexOf('\\') >= 0) {
return false;
}
return Arrays.stream(host.split("\\.", -1))
.allMatch(label -> !label.isEmpty()
&& label.length() <= 63
&& Character.isLetterOrDigit(label.charAt(0))
&& Character.isLetterOrDigit(label.charAt(label.length() - 1))
&& label.chars().allMatch(ch ->
Character.isLetterOrDigit(ch)
|| ch == '-'
|| ch == '_'));
}
private String buildMqttUsername() throws Exception {
Map<String, String> userData = new HashMap<>();
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String clientIp = request.getRemoteAddr();
userData.put("ip", clientIp);
}
} catch (Exception e) {
userData.put("ip", "unknown");
}
String userDataJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(userData);
return Base64.getEncoder().encodeToString(userDataJson.getBytes(StandardCharsets.UTF_8));
}
private DeviceReportRespDTO.MQTT buildNativeMqttConfig(String macAddress, String groupId, String clientId)
throws Exception {
String deviceIdSafeStr = MqttClientId.normalizeDeviceId(macAddress);
String mqttClientId = MqttClientId.build(groupId, macAddress);
String username = buildMqttUsername();
String password = "";
String signatureKey = sysParamsService.getValue(Constant.MQTT_SERVER_SIGNATURE_KEY, true);
if (!isConfiguredValue(signatureKey)) {
signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, true);
}
if (!isConfiguredValue(signatureKey)) {
log.error("原生MQTT已启用但未配置签名密钥,跳过原生MQTT配置下发");
return null;
}
password = generatePasswordSignature(mqttClientId + "|" + username, signatureKey);
DeviceReportRespDTO.MQTT mqtt = new DeviceReportRespDTO.MQTT();
mqtt.setClient_id(mqttClientId);
mqtt.setUsername(username);
mqtt.setPassword(password);
mqtt.setPublish_topic("device-server");
mqtt.setSubscribe_topic("devices/p2p/" + deviceIdSafeStr);
return mqtt;
}
/**
* 构建MQTT配置信息
*
@@ -875,10 +622,28 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
}
// 构建客户端ID格式:groupId@@@macAddress@@@uuid
String deviceIdSafeStr = MqttClientId.normalizeDeviceId(macAddress);
String mqttClientId = MqttClientId.build(groupId, macAddress);
String groupIdSafeStr = groupId.replace(":", "_");
String deviceIdSafeStr = macAddress.replace(":", "_");
String mqttClientId = String.format("%s@@@%s@@@%s", groupIdSafeStr, deviceIdSafeStr, deviceIdSafeStr);
String username = buildMqttUsername();
// 构建用户数据(包含IP等信息)
Map<String, String> userData = new HashMap<>();
// 尝试获取客户端IP
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String clientIp = request.getRemoteAddr();
userData.put("ip", clientIp);
}
} catch (Exception e) {
userData.put("ip", "unknown");
}
// 将用户数据编码为Base64 JSON
String userDataJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(userData);
String username = Base64.getEncoder().encodeToString(userDataJson.getBytes(StandardCharsets.UTF_8));
// 生成密码签名
String password = generatePasswordSignature(mqttClientId + "|" + username, signatureKey);
@@ -894,14 +659,30 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
return mqtt;
}
MqttManagementRouter createMqttManagementRouter() {
return new MqttManagementRouter(
new MqttManagementEndpointResolver(sysParamsService),
new MqttManagementHttpClient());
/**
* 生成BearerToken
*/
private String generateBearerToken() {
try {
String dateStr = DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN);
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
if (ToolUtil.isEmpty(signatureKey)) {
return null;
}
return DigestUtil.sha256Hex(dateStr + signatureKey);
} catch (Exception e) {
return null;
}
}
@Override
public Object getDeviceTools(String deviceId) {
// 从系统参数中获取MQTT网关地址
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
return null;
}
// 获取设备信息
DeviceEntity device = baseDao.selectById(deviceId);
if (device == null) {
@@ -915,20 +696,19 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
}
// 构建clientId
String clientId = MqttClientId.build(
device.getBoard(), device.getMacAddress());
String macAddress = Optional.ofNullable(device.getMacAddress()).orElse("unknown").replace(":", "_");
String groupId = Optional.ofNullable(device.getBoard()).orElse("GID_default").replace(":", "_");
String clientId = StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress);
// 构建完整的URL
String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId);
// 存储所有工具列表
List<Object> allTools = new ArrayList<>();
String cursor = null;
Set<String> seenCursors = new java.util.HashSet<>();
int pageCount = 0;
MqttManagementRouter managementRouter =
createMqttManagementRouter();
MqttManagementEndpointResolver.Backend selectedBackend = null;
// 循环获取分页数据
while (pageCount++ < 32) {
while (true) {
// 构建params
Map<String, Object> paramsMap = MapUtil.builder(new HashMap<String, Object>())
.put("withUserTools", true)
@@ -953,44 +733,32 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.put("payload", payload)
.build();
MqttManagementHttpClient.Response response =
managementRouter.sendReadOnlyCommand(
clientId, requestBody, selectedBackend);
if (response == null || !response.isSuccessfulHttp()) {
return null;
}
String resultMessage =
response.body();
if (selectedBackend == null) {
selectedBackend = response.backend();
}
// 发送请求
String resultMessage = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
.body(JSONUtil.toJsonStr(requestBody))
.timeout(10000) // 超时,毫秒
.execute().body();
// 解析响应
if (StringUtils.isBlank(resultMessage)) {
return null;
break;
}
JSONObject jsonObject;
try {
jsonObject = JSONUtil.parseObj(resultMessage);
} catch (RuntimeException e) {
return null;
}
JSONObject jsonObject = JSONUtil.parseObj(resultMessage);
if (!jsonObject.getBool("success", false)) {
return null;
break;
}
JSONObject data = jsonObject.getJSONObject("data");
if (data == null) {
return null;
break;
}
// 获取当前页的工具列表
JSONArray tools = data.getJSONArray("tools");
if (tools == null) {
return null;
}
if (!tools.isEmpty()) {
if (tools != null && !tools.isEmpty()) {
allTools.addAll(tools);
}
@@ -998,23 +766,29 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
String nextCursor = data.getStr("nextCursor");
if (StringUtils.isBlank(nextCursor)) {
// 没有下一页了
Map<String, Object> resultData = new HashMap<>();
resultData.put("tools", allTools);
return resultData;
}
if (!seenCursors.add(nextCursor)) {
log.warn("MQTT设备工具列表返回重复cursor,终止分页: {}",
nextCursor);
return null;
break;
}
cursor = nextCursor;
}
// 构建返回结果
if (allTools.isEmpty()) {
return null;
}
Map<String, Object> resultData = new HashMap<>();
resultData.put("tools", allTools);
return resultData;
}
@Override
public Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments) {
// 从系统参数中获取MQTT网关地址
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
return null;
}
// 获取设备信息
DeviceEntity device = baseDao.selectById(deviceId);
if (device == null) {
@@ -1028,8 +802,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
}
// 构建clientId
String clientId = MqttClientId.build(
device.getBoard(), device.getMacAddress());
String macAddress = Optional.ofNullable(device.getMacAddress()).orElse("unknown").replace(":", "_");
String groupId = Optional.ofNullable(device.getBoard()).orElse("GID_default").replace(":", "_");
String clientId = StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress);
// 构建完整的URL
String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId);
// 构建请求体
Map<String, Object> params = MapUtil
@@ -1052,11 +830,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.put("payload", payload)
.build();
MqttManagementHttpClient.Response response =
createMqttManagementRouter()
.sendMutatingCommand(clientId, requestBody);
String resultMessage =
response == null ? null : response.body();
// 发送请求
String resultMessage = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
.body(JSONUtil.toJsonStr(requestBody))
.timeout(10000) // 超时,毫秒
.execute().body();
// 解析响应
if (StringUtils.isNotBlank(resultMessage)) {
@@ -1,28 +0,0 @@
package xiaozhi.modules.device.service.impl;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils;
final class MqttClientId {
private MqttClientId() {
}
static String build(String board, String macAddress) {
String groupId = StringUtils.defaultIfBlank(
board, "GID_default").trim().replace(":", "_");
String deviceId = StringUtils.defaultIfBlank(macAddress, "unknown")
.trim()
.replace(":", "_");
return groupId + "@@@" + deviceId + "@@@" + deviceId;
}
static String normalizeDeviceId(String macAddress) {
return StringUtils.defaultIfBlank(macAddress, "unknown")
.trim()
.toLowerCase(Locale.ROOT)
.replace(":", "_")
.replace("-", "_");
}
}
@@ -1,122 +0,0 @@
package xiaozhi.modules.device.service.impl;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.List;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
final class MqttGatewayAuthorization {
private static final int HTTP_UNAUTHORIZED = 401;
private static final int DEFAULT_TIMEOUT_MILLIS = 10000;
private MqttGatewayAuthorization() {
}
static String postJson(String url, String jsonBody, String signatureKey, Instant now) {
return postJson(url, jsonBody, signatureKey, now, DEFAULT_TIMEOUT_MILLIS);
}
static String postJson(String url, String jsonBody, String signatureKey, Instant now, int timeoutMillis) {
GatewayResponse response = postJsonResponse(
url, jsonBody, signatureKey, now, timeoutMillis);
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new GatewayRequestException(
"MQTT Gateway request failed with HTTP status " + response.statusCode(),
response.statusCode());
}
return response.body();
}
static GatewayResponse postJsonResponse(
String url,
String jsonBody,
String signatureKey,
Instant now,
int timeoutMillis) {
return executeWithDateFallback(
signatureKey,
now,
token -> executeRequest(
url, jsonBody, token, timeoutMillis));
}
static List<String> generateDailyTokens(String signatureKey, Instant now) {
if (isMissingSignatureKey(signatureKey)) {
throw new GatewayRequestException("MQTT Gateway signature key is empty", null);
}
LocalDate utcDate = now.atZone(ZoneOffset.UTC).toLocalDate();
return List.of(
tokenFor(utcDate, signatureKey),
tokenFor(utcDate.minusDays(1), signatureKey),
tokenFor(utcDate.plusDays(1), signatureKey));
}
static GatewayResponse executeWithDateFallback(String signatureKey, Instant now,
Function<String, GatewayResponse> requestExecutor) {
GatewayResponse lastAuthenticationFailure = null;
for (String token : generateDailyTokens(signatureKey, now)) {
GatewayResponse response = requestExecutor.apply(token);
if (!isAuthenticationFailure(response.statusCode())) {
return response;
}
lastAuthenticationFailure = response;
}
Integer statusCode = lastAuthenticationFailure == null ? null : lastAuthenticationFailure.statusCode();
throw new GatewayRequestException(
"MQTT Gateway rejected all daily authorization tokens"
+ (statusCode == null ? "" : " (HTTP " + statusCode + ")"),
statusCode);
}
private static String tokenFor(LocalDate date, String signatureKey) {
return DigestUtil.sha256Hex(date + signatureKey);
}
private static boolean isAuthenticationFailure(int statusCode) {
return statusCode == HTTP_UNAUTHORIZED;
}
static boolean isMissingSignatureKey(String signatureKey) {
return StringUtils.isBlank(signatureKey) || "null".equalsIgnoreCase(signatureKey.trim());
}
private static GatewayResponse executeRequest(String url, String jsonBody, String token, int timeoutMillis) {
try (HttpResponse response = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + token)
.body(jsonBody)
.timeout(timeoutMillis)
.execute()) {
return new GatewayResponse(response.getStatus(), response.body());
}
}
record GatewayResponse(int statusCode, String body) {
}
static final class GatewayRequestException extends RuntimeException {
private final Integer statusCode;
GatewayRequestException(String message, Integer statusCode) {
super(message);
this.statusCode = statusCode;
}
Integer statusCode() {
return statusCode;
}
}
}
@@ -1,183 +0,0 @@
package xiaozhi.modules.device.service.impl;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils;
import xiaozhi.common.constant.Constant;
import xiaozhi.modules.sys.service.SysParamsService;
class MqttManagementEndpointResolver {
enum Backend {
NATIVE,
GATEWAY
}
record Endpoint(Backend backend, String baseUrl, String signatureKey) {
}
private final SysParamsService sysParamsService;
MqttManagementEndpointResolver(SysParamsService sysParamsService) {
this.sysParamsService = sysParamsService;
}
List<Endpoint> resolve() {
List<Endpoint> endpoints = new ArrayList<>(2);
resolveNative().ifPresent(endpoints::add);
resolveGateway().ifPresent(endpoints::add);
return endpoints;
}
java.util.Optional<Endpoint> resolveNative() {
if (!isNativeMqttEnabled()) {
return java.util.Optional.empty();
}
String endpoint = normalizeHttpEndpoint(sysParamsService.getValue(
Constant.MQTT_SERVER_MANAGER_API, true));
String signatureKey = firstConfigured(
sysParamsService.getValue(
Constant.MQTT_SERVER_MANAGER_API_SECRET, true),
sysParamsService.getValue(
Constant.MQTT_SERVER_SIGNATURE_KEY, true),
sysParamsService.getValue(
Constant.SERVER_MQTT_SECRET, true));
if (endpoint == null || signatureKey == null) {
throw new ManagementConfigurationException(
"原生MQTT已启用但管理端点或密钥无效");
}
return java.util.Optional.of(
new Endpoint(Backend.NATIVE, endpoint, signatureKey));
}
java.util.Optional<Endpoint> resolveGateway() {
String rawGateway = sysParamsService.getValue(
Constant.SERVER_MQTT_GATEWAY, true);
String rawEndpoint = sysParamsService.getValue(
Constant.SERVER_MQTT_MANAGER_API, true);
if (configured(rawGateway) == null
&& configured(rawEndpoint) == null) {
return java.util.Optional.empty();
}
String endpoint = normalizeHttpEndpoint(rawEndpoint);
String signatureKey = configured(
sysParamsService.getValue(
Constant.SERVER_MQTT_SECRET, true));
if (endpoint == null || signatureKey == null) {
throw new ManagementConfigurationException(
"MQTT Gateway管理端点或密钥无效");
}
return java.util.Optional.of(
new Endpoint(Backend.GATEWAY, endpoint, signatureKey));
}
boolean isNativeMqttEnabled() {
String enabled = sysParamsService.getValue(
Constant.MQTT_SERVER_ENABLED, true);
String legacyEnabled = sysParamsService.getValue(
Constant.SERVER_MQTT_ENABLED, true);
boolean serverEnabled = configured(enabled) != null
? isTrue(enabled)
: isTrue(legacyEnabled);
String protocolEnabled = sysParamsService.getValue(
Constant.PROTOCOLS_MQTT_ENABLED, true);
String enabledProtocols = sysParamsService.getValue(
Constant.PROTOCOLS_ENABLED, true);
boolean enabledByProtocol = isTrue(protocolEnabled);
if (!enabledByProtocol && StringUtils.isNotBlank(enabledProtocols)) {
String normalized = enabledProtocols
.replace("[", "")
.replace("]", "")
.replace("\"", "");
enabledByProtocol = Arrays.stream(
normalized.split("[;,\\s]+"))
.anyMatch("mqtt"::equalsIgnoreCase);
}
return serverEnabled && enabledByProtocol;
}
static String normalizeHttpEndpoint(String raw) {
String configured = configured(raw);
if (configured == null) {
return null;
}
boolean hasScheme = configured.matches(
"^[A-Za-z][A-Za-z0-9+.-]*://.*");
if (hasScheme
&& !configured.matches("(?i)^https?://.*")) {
return null;
}
String candidate = hasScheme ? configured : "http://" + configured;
try {
URI uri = new URI(candidate);
String scheme = uri.getScheme();
if (scheme == null
|| (!"http".equalsIgnoreCase(scheme)
&& !"https".equalsIgnoreCase(scheme))
|| StringUtils.isBlank(uri.getHost())
|| uri.getUserInfo() != null
|| uri.getQuery() != null
|| uri.getFragment() != null
|| uri.getPort() == 0
|| uri.getPort() > 65535) {
return null;
}
String path = uri.getPath();
if (path == null || "/".equals(path)) {
path = "";
} else {
path = path.replaceAll("/+$", "");
}
return new URI(
scheme.toLowerCase(Locale.ROOT),
null,
uri.getHost(),
uri.getPort(),
path,
null,
null).toString();
} catch (URISyntaxException | IllegalArgumentException e) {
return null;
}
}
private static String firstConfigured(String... values) {
for (String value : values) {
String normalized = configured(value);
if (normalized != null) {
return normalized;
}
}
return null;
}
private static String configured(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
String normalized = value.trim();
if ("null".equalsIgnoreCase(normalized)
|| normalized.contains("")) {
return null;
}
return normalized;
}
private static boolean isTrue(String value) {
return "true".equalsIgnoreCase(value) || "1".equals(value);
}
static final class ManagementConfigurationException
extends RuntimeException {
ManagementConfigurationException(String message) {
super(message);
}
}
}
@@ -1,56 +0,0 @@
package xiaozhi.modules.device.service.impl;
import java.time.Instant;
import cn.hutool.json.JSONUtil;
class MqttManagementHttpClient {
private final int timeoutMillis;
MqttManagementHttpClient() {
this(5000);
}
MqttManagementHttpClient(int timeoutMillis) {
this.timeoutMillis = timeoutMillis;
}
Response post(
MqttManagementEndpointResolver.Endpoint endpoint,
String path,
Object requestBody) {
String url = appendPath(endpoint.baseUrl(), path);
MqttGatewayAuthorization.GatewayResponse response =
MqttGatewayAuthorization.postJsonResponse(
url,
JSONUtil.toJsonStr(requestBody),
endpoint.signatureKey(),
Instant.now(),
timeoutMillis);
return new Response(
endpoint.backend(),
response.statusCode(),
response.body());
}
static String appendPath(String baseUrl, String path) {
String normalizedBase = baseUrl.endsWith("/")
? baseUrl.substring(0, baseUrl.length() - 1)
: baseUrl;
String normalizedPath = path.startsWith("/")
? path
: "/" + path;
return normalizedBase + normalizedPath;
}
record Response(
MqttManagementEndpointResolver.Backend backend,
int statusCode,
String body) {
boolean isSuccessfulHttp() {
return statusCode >= 200 && statusCode < 300;
}
}
}
@@ -1,431 +0,0 @@
package xiaozhi.modules.device.service.impl;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
final class MqttManagementRouter {
private final MqttManagementEndpointResolver endpointResolver;
private final MqttManagementHttpClient httpClient;
MqttManagementRouter(
MqttManagementEndpointResolver endpointResolver,
MqttManagementHttpClient httpClient) {
this.endpointResolver = endpointResolver;
this.httpClient = httpClient;
}
String getMergedStatus(Set<String> clientIds) {
List<MqttManagementEndpointResolver.Endpoint> endpoints =
endpointResolver.resolve();
if (endpoints.isEmpty()) {
return "";
}
Map<String, MutableDeviceStatus> merged =
initializeStatuses(clientIds);
int successfulBackends = 0;
RuntimeException lastFailure = null;
for (MqttManagementEndpointResolver.Endpoint endpoint : endpoints) {
try {
MqttManagementHttpClient.Response response =
httpClient.post(
endpoint,
"/api/devices/status",
Map.of("clientIds", clientIds));
if (!response.isSuccessfulHttp()) {
throw new IllegalStateException(
"MQTT管理状态查询失败: "
+ response.statusCode());
}
mergeStatusResponse(
merged, endpoint.backend(), response.body());
successfulBackends++;
} catch (RuntimeException e) {
lastFailure = e;
}
}
if (lastFailure != null
|| successfulBackends != endpoints.size()) {
throw new ManagementUnavailableException(
"无法确认全部MQTT后端状态", lastFailure);
}
Map<String, Object> result = new LinkedHashMap<>();
merged.forEach((clientId, status) ->
result.put(clientId, status.toMap()));
return JSONUtil.toJsonStr(result);
}
MqttManagementHttpClient.Response sendReadOnlyCommand(
String clientId, Object requestBody) {
return sendReadOnlyCommand(clientId, requestBody, null);
}
MqttManagementHttpClient.Response sendReadOnlyCommand(
String clientId,
Object requestBody,
MqttManagementEndpointResolver.Backend preferredBackend) {
List<MqttManagementEndpointResolver.Endpoint> endpoints =
endpointResolver.resolve();
if (endpoints.isEmpty()) {
return null;
}
if (preferredBackend != null) {
MqttManagementEndpointResolver.Endpoint endpoint =
endpoints.stream()
.filter(candidate ->
candidate.backend()
== preferredBackend)
.findFirst()
.orElseThrow(() ->
new ManagementUnavailableException(
"分页MQTT后端配置已变化",
null));
return sendCommand(endpoint, clientId, requestBody);
}
MqttManagementHttpClient.Response lastResponse = null;
RuntimeException lastFailure = null;
for (MqttManagementEndpointResolver.Endpoint endpoint : endpoints) {
try {
MqttManagementHttpClient.Response response =
sendCommand(endpoint, clientId, requestBody);
lastResponse = response;
if (isCommandSuccess(response)) {
return response;
}
} catch (RuntimeException e) {
lastFailure = e;
}
}
if (lastResponse != null) {
return lastResponse;
}
throw new ManagementUnavailableException(
"MQTT管理服务均不可用", lastFailure);
}
MqttManagementHttpClient.Response sendMutatingCommand(
String clientId, Object requestBody) {
List<MqttManagementEndpointResolver.Endpoint> endpoints =
endpointResolver.resolve();
if (endpoints.isEmpty()) {
return null;
}
List<MqttManagementEndpointResolver.Endpoint> online =
new ArrayList<>();
int successfulStatusBackends = 0;
RuntimeException lastStatusFailure = null;
for (MqttManagementEndpointResolver.Endpoint endpoint : endpoints) {
try {
MqttManagementHttpClient.Response statusResponse =
httpClient.post(
endpoint,
"/api/devices/status",
Map.of("clientIds", Set.of(clientId)));
if (!statusResponse.isSuccessfulHttp()) {
throw new IllegalStateException(
"MQTT管理状态查询失败: "
+ statusResponse.statusCode());
}
Map<String, BackendDeviceStatus> statuses =
parseStatusResponse(
statusResponse.body(),
Set.of(clientId));
successfulStatusBackends++;
if (statuses.get(clientId).exists()) {
online.add(endpoint);
}
} catch (RuntimeException e) {
lastStatusFailure = e;
}
}
if (lastStatusFailure != null
|| successfulStatusBackends != endpoints.size()) {
throw new ManagementUnavailableException(
"无法确认设备所在的MQTT后端",
lastStatusFailure);
}
if (online.isEmpty()) {
return offlineResponse(endpoints.get(0).backend());
}
if (online.size() > 1) {
return commandErrorResponse(
endpoints.get(0).backend(),
409,
"设备同时存在于多个MQTT后端",
"DEVICE_BACKEND_AMBIGUOUS");
}
return sendCommand(online.get(0), clientId, requestBody);
}
MqttManagementHttpClient.Response sendCallRequest(
String callerClientId,
String targetClientId,
Object requestBody) {
Set<String> clientIds = new java.util.LinkedHashSet<>();
clientIds.add(callerClientId);
clientIds.add(targetClientId);
return sendCallMutation(
clientIds,
"/api/call/request",
requestBody);
}
MqttManagementHttpClient.Response sendCallAccept(
String clientId, Object requestBody) {
return sendCallMutation(
Set.of(clientId),
"/api/call/accept",
requestBody);
}
private MqttManagementHttpClient.Response sendCallMutation(
Set<String> clientIds,
String path,
Object requestBody) {
List<MqttManagementEndpointResolver.Endpoint> endpoints =
endpointResolver.resolve();
if (endpoints.isEmpty()) {
return null;
}
Map<String, List<MqttManagementEndpointResolver.Endpoint>> owners =
new LinkedHashMap<>();
clientIds.forEach(clientId ->
owners.put(clientId, new ArrayList<>()));
RuntimeException statusFailure = null;
int successfulStatusBackends = 0;
for (MqttManagementEndpointResolver.Endpoint endpoint : endpoints) {
try {
MqttManagementHttpClient.Response response =
httpClient.post(
endpoint,
"/api/devices/status",
Map.of("clientIds", clientIds));
if (!response.isSuccessfulHttp()) {
throw new IllegalStateException(
"MQTT管理状态查询失败: "
+ response.statusCode());
}
Map<String, BackendDeviceStatus> statuses =
parseStatusResponse(response.body(), clientIds);
owners.forEach((clientId, matches) -> {
if (statuses.get(clientId).exists()) {
matches.add(endpoint);
}
});
successfulStatusBackends++;
} catch (RuntimeException e) {
statusFailure = e;
}
}
if (statusFailure != null
|| successfulStatusBackends != endpoints.size()) {
throw new ManagementUnavailableException(
"无法确认通话设备所在的MQTT后端",
statusFailure);
}
for (Map.Entry<String, List<MqttManagementEndpointResolver.Endpoint>>
owner : owners.entrySet()) {
if (owner.getValue().isEmpty()) {
return callErrorResponse(
endpoints.get(0).backend(),
404,
"offline",
"设备不在线",
"DEVICE_OFFLINE");
}
if (owner.getValue().size() > 1) {
return callErrorResponse(
endpoints.get(0).backend(),
409,
"error",
"设备同时存在于多个MQTT后端",
"DEVICE_BACKEND_AMBIGUOUS");
}
}
MqttManagementEndpointResolver.Endpoint selected =
owners.values().iterator().next().get(0);
boolean sameBackend = owners.values().stream()
.allMatch(matches -> matches.get(0).equals(selected));
if (!sameBackend) {
return callErrorResponse(
selected.backend(),
409,
"error",
"跨MQTT后端通话暂不支持",
"CROSS_BACKEND_CALL_UNSUPPORTED");
}
return httpClient.post(selected, path, requestBody);
}
private MqttManagementHttpClient.Response sendCommand(
MqttManagementEndpointResolver.Endpoint endpoint,
String clientId,
Object requestBody) {
String encodedClientId = URLEncoder.encode(
clientId, StandardCharsets.UTF_8)
.replace("+", "%20");
return httpClient.post(
endpoint,
"/api/commands/" + encodedClientId,
requestBody);
}
private static Map<String, MutableDeviceStatus> initializeStatuses(
Set<String> clientIds) {
Map<String, MutableDeviceStatus> result = new LinkedHashMap<>();
clientIds.forEach(clientId ->
result.put(clientId, new MutableDeviceStatus()));
return result;
}
private static void mergeStatusResponse(
Map<String, MutableDeviceStatus> merged,
MqttManagementEndpointResolver.Backend backend,
String body) {
Map<String, BackendDeviceStatus> response =
parseStatusResponse(body, merged.keySet());
merged.forEach((clientId, status) -> {
BackendDeviceStatus value = response.get(clientId);
status.exists |= value.exists();
status.alive |= value.alive();
if (value.exists()) {
status.backends.add(
backend.name().toLowerCase());
}
});
}
private static Map<String, BackendDeviceStatus> parseStatusResponse(
String body, Set<String> clientIds) {
JSONObject response;
try {
response = JSONUtil.parseObj(body);
} catch (RuntimeException e) {
throw new IllegalStateException(
"MQTT管理状态响应不是JSON对象", e);
}
Map<String, BackendDeviceStatus> statuses =
new LinkedHashMap<>();
for (String clientId : clientIds) {
Object rawStatus = response.get(clientId);
if (!(rawStatus instanceof JSONObject status)
|| !status.containsKey("exists")
|| !status.containsKey("isAlive")) {
throw new IllegalStateException(
"MQTT管理状态响应缺少设备状态: " + clientId);
}
Object rawExists = status.get("exists");
Object rawAlive = status.get("isAlive");
if (!(rawExists instanceof Boolean exists)
|| !(rawAlive instanceof Boolean alive)
|| (!exists && alive)) {
throw new IllegalStateException(
"MQTT管理状态响应字段无效: " + clientId);
}
statuses.put(
clientId,
new BackendDeviceStatus(exists, alive));
}
return statuses;
}
private static boolean isCommandSuccess(
MqttManagementHttpClient.Response response) {
if (response == null || !response.isSuccessfulHttp()) {
return false;
}
try {
return JSONUtil.parseObj(response.body())
.getBool("success", false);
} catch (Exception e) {
return false;
}
}
private static MqttManagementHttpClient.Response offlineResponse(
MqttManagementEndpointResolver.Backend backend) {
return new MqttManagementHttpClient.Response(
backend,
404,
JSONUtil.toJsonStr(Map.of(
"success", false,
"error", "设备未连接",
"code", "DEVICE_OFFLINE",
"dispatchAttempted", false)));
}
private static MqttManagementHttpClient.Response callErrorResponse(
MqttManagementEndpointResolver.Backend backend,
int statusCode,
String status,
String message,
String code) {
return new MqttManagementHttpClient.Response(
backend,
statusCode,
JSONUtil.toJsonStr(Map.of(
"status", status,
"message", message,
"code", code)));
}
private static MqttManagementHttpClient.Response commandErrorResponse(
MqttManagementEndpointResolver.Backend backend,
int statusCode,
String error,
String code) {
return new MqttManagementHttpClient.Response(
backend,
statusCode,
JSONUtil.toJsonStr(Map.of(
"success", false,
"error", error,
"code", code,
"dispatchAttempted", false)));
}
static final class ManagementUnavailableException
extends RuntimeException {
ManagementUnavailableException(
String message, Throwable cause) {
super(message, cause);
}
}
private static final class MutableDeviceStatus {
private boolean exists;
private boolean alive;
private final List<String> backends = new ArrayList<>();
Map<String, Object> toMap() {
Map<String, Object> result = new LinkedHashMap<>();
result.put("isAlive", alive);
result.put("exists", exists);
result.put("backends", backends);
return result;
}
}
private record BackendDeviceStatus(boolean exists, boolean alive) {
}
}
@@ -56,7 +56,7 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
@Override
public void delete(String[] ids) {
baseDao.deleteByIds(Arrays.asList(ids));
baseDao.deleteBatchIds(Arrays.asList(ids));
}
@Override
@@ -1,9 +1,5 @@
package xiaozhi.modules.device.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -20,32 +16,16 @@ public class UserShowDeviceListVO {
@Schema(description = "设备型号")
private String deviceType;
@Schema(description = "设备型号(board)")
private String board;
@Schema(description = "设备唯一标识符")
private String id;
@Schema(description = "mac地址")
private String macAddress;
@Schema(description = "设备别名")
private String alias;
@Schema(description = "自动更新开关(0关闭/1开启)")
private Integer autoUpdate;
@Schema(description = "开启OTA")
private Integer otaUpgrade;
@Schema(description = "最近对话时间")
private String recentChatTime;
@Schema(description = "最后连接时间戳(毫秒)", type = "string", example = "1783689702000")
private Long lastConnectedAtTimestamp;
@Schema(description = "绑定时间戳(毫秒)", type = "string", example = "1783689702000")
private Long createDateTimestamp;
@Schema(description = "绑定时间(兼容字段,请使用 createDateTimestamp", deprecated = true)
@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(
@PathVariable("dataset_id") String datasetId,
@PathVariable("document_id") String documentId,
@RequestParam(name = "page", required = false, defaultValue = "1") Integer page,
@RequestParam(name = "page_size", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keywords,
@RequestParam(required = false) String id) {
@ParameterObject ChunkDTO.ListReq req) {
// 验证权限 (内部已包含知识库存在性校验与归属权校验)
validateKnowledgeBasePermission(datasetId);
// 构建请求对象
ChunkDTO.ListReq req = ChunkDTO.ListReq.builder()
.page(page)
.pageSize(pageSize)
.keywords(keywords)
.id(id)
.build();
// 设置默认值
if (req.getPage() == null)
req.setPage(1);
if (req.getPageSize() == null)
req.setPageSize(50);
// 调用服务层获取强类型切片列表
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 = "文档数量")
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;
}
}
@@ -149,8 +149,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
log.info("=== [RAGFlow] 获取文档列表: datasetId={} ===", datasetId);
// 使用 Jackson 将 DTO 转为 Map 作为查询参数
Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
});
@SuppressWarnings("unchecked")
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
@@ -174,12 +174,13 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
.pageSize(1)
.build();
Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
});
@SuppressWarnings("unchecked")
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
Object dataObj = response.get("data");
if (dataObj instanceof Map<?, ?> dataMap) {
if (dataObj instanceof Map) {
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
List<?> documents = (List<?>) dataMap.get("docs");
if (documents != null && !documents.isEmpty()) {
return objectMapper.convertValue(documents.get(0), DocumentDTO.InfoVO.class);
@@ -484,11 +485,16 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
@Override
public Integer getDocumentCount(String datasetId) {
try {
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
if (info != null && info.getDocumentCount() != null) {
return info.getDocumentCount().intValue();
}
return 0;
} catch (Exception e) {
log.warn("获取文档数量失败: {}", e.getMessage());
return 0;
}
}
@Override
@@ -512,7 +518,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
return null;
} catch (Exception e) {
log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage());
throw convertToRenException(e);
return null;
}
}
@@ -566,8 +572,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
return new PageData<>(new ArrayList<>(), 0);
}
Map<?, ?> dataMap = Map.class.cast(dataObj);
List<?> documents = List.class.cast(dataMap.get("docs"));
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
List<Map<String, Object>> documents = (List<Map<String, Object>>) dataMap.get("docs");
if (documents == null || documents.isEmpty()) {
// RAGFlow 明确返回了空文档列表,这是合法的"真空"
return new PageData<>(new ArrayList<>(), 0);
@@ -678,9 +684,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
dto.setChunkMethod(info.getChunkMethod().name().toLowerCase());
}
if (info.getParserConfig() != null) {
dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(),
new TypeReference<Map<String, Object>>() {
}));
dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(), Map.class));
}
return dto;
@@ -100,9 +100,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
}
DatasetDTO.InfoVO datasetInfo = adapter.getDatasetInfo(dto.getDatasetId());
// getDatasetInfo 正常返回 null 表示远端确实不存在;异常时已抛出 RenException 由外层 catch 接管
if (datasetInfo == null) {
// RAGFlow 端已确认删除 → 本地级联清理
// RAGFlow 端已删除 → 本地级联清理
log.info("数据集 {} 在 RAGFlow 端不存在,执行本地清理", dto.getDatasetId());
cleanupLocalDataset(dto.getDatasetId(), dto.getId());
// 标记为已删除,让上层从列表中移除
@@ -145,9 +145,8 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
}
} catch (Exception e) {
log.error("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
log.warn("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
dto.setDocumentCount(0);
dto.setErrorMessage(e.getMessage());
}
}
@@ -529,8 +529,7 @@ public class KnowledgeFilesServiceImpl extends BaseServiceImpl<DocumentDao, Docu
adapter.deleteDocument(datasetId, req);
log.info("远程批量删除请求成功");
} catch (Exception e) {
log.error("远程删除请求失败,中止本地清理以避免数据不一致: {}", e.getMessage());
throw new RenException(e.getMessage());
log.warn("远程删除请求部分或全部失败: {}", e.getMessage());
}
// 4. 原子化清理本地影子记录并同步统计数据
@@ -116,11 +116,13 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
Map<String, Object>[] messages = new Map[1];
Map<String, Object> message = new HashMap<>();
message.put("role", "user");
message.put("content", prompt);
messages[0] = message;
requestBody.put("messages", List.of(message));
requestBody.put("messages", messages);
requestBody.put("temperature", temperature != null ? temperature : 0.7);
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
@@ -210,11 +212,13 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
Map<String, Object>[] messages = new Map[1];
Map<String, Object> message = new HashMap<>();
message.put("role", "user");
message.put("content", prompt);
messages[0] = message;
requestBody.put("messages", List.of(message));
requestBody.put("messages", messages);
requestBody.put("temperature", 0.2);
requestBody.put("max_tokens", 2000);
@@ -364,11 +368,13 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
Map<String, Object>[] messages = new Map[1];
Map<String, Object> message = new HashMap<>();
message.put("role", "user");
message.put("content", prompt);
messages[0] = message;
requestBody.put("messages", List.of(message));
requestBody.put("messages", messages);
requestBody.put("temperature", 0.3);
requestBody.put("max_tokens", 50);
@@ -39,11 +39,9 @@ import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService;
import lombok.extern.slf4j.Slf4j;
@Service
@AllArgsConstructor
@Slf4j
public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, ModelConfigEntity>
implements ModelConfigService {
@@ -362,21 +360,20 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
if (SensitiveDataUtils.isSensitiveField(key)) {
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
updatedJson.set(key, value);
updatedJson.put(key, value);
}
} else if (value instanceof JSONObject) {
// 递归处理嵌套JSON
mergeJson(updatedJson, key, (JSONObject) value);
} else {
// 非敏感字段直接更新
updatedJson.set(key, value);
updatedJson.put(key, value);
}
}
// 删除在新JSON中不存在的非敏感字段
for (String oldKey : originalJson.keySet().toArray(new String[0])) {
if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey)
&& !SensitiveDataUtils.isSensitiveField(oldKey)) {
if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey) && !SensitiveDataUtils.isSensitiveField(oldKey)) {
updatedJson.remove(oldKey);
}
}
@@ -395,33 +392,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
return value.contains("***");
}
// 辅助方法:递归合并 JSON,保留原始敏感字段
// 辅助方法:递归合并JSON,保留原始敏感字段
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)) {
original.set(key, new JSONObject());
}
// 获取 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.set(key, originalChild);
original.put(key, new JSONObject());
}
JSONObject originalChild = original.getJSONObject(key);
for (String childKey : updated.keySet()) {
Object childValue = updated.get(childKey);
@@ -430,12 +406,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
} else {
if (!SensitiveDataUtils.isSensitiveField(childKey) ||
(childValue instanceof String && !isMaskedValue((String) childValue))) {
originalChild.set(childKey, childValue);
originalChild.put(childKey, childValue);
}
}
}
// 删除在新 JSON 中不存在的非敏感子字段
// 删除在新JSON中不存在的非敏感子字段
for (String oldChildKey : originalChild.keySet().toArray(new String[0])) {
if (!updated.containsKey(oldChildKey) && !SensitiveDataUtils.isSensitiveField(oldChildKey)) {
originalChild.remove(oldChildKey);
@@ -164,7 +164,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
@Override
public void delete(List<String> ids) {
if (modelProviderDao.deleteByIds(ids) == 0) {
if (modelProviderDao.deleteBatchIds(ids) == 0) {
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
}
}
@@ -4,19 +4,16 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.config.ShiroFilterConfiguration;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Role;
import jakarta.servlet.Filter;
import xiaozhi.modules.security.oauth2.Oauth2Filter;
@@ -42,7 +39,7 @@ public class ShiroConfig {
}
@Bean("securityManager")
public WebSecurityManager securityManager(Oauth2Realm oAuth2Realm, SessionManager sessionManager) {
public SecurityManager securityManager(Oauth2Realm oAuth2Realm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(oAuth2Realm);
securityManager.setSessionManager(sessionManager);
@@ -51,8 +48,7 @@ public class ShiroConfig {
}
@Bean("shiroFilter")
public static ShiroFilterFactoryBean shirFilter(@Lazy WebSecurityManager securityManager,
@Lazy SysParamsService sysParamsService) {
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
config.setFilterOncePerRequest(true);
@@ -91,7 +87,6 @@ public class ShiroConfig {
filterMap.put("/user/retrieve-password", "anon");
// 将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/download/**", "anon");
filterMap.put("/agent/chat-summary/**", "server");
@@ -105,14 +100,12 @@ public class ShiroConfig {
}
@Bean("lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public static AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
@Lazy WebSecurityManager securityManager) {
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
@@ -85,7 +85,6 @@ public class SysParamsController {
public Result<Void> save(@RequestBody SysParamsDTO dto) {
// 效验数据
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
sysParamsService.save(dto);
configService.getConfig(false);
@@ -115,12 +114,7 @@ public class SysParamsController {
// 校验mqtt密钥长度
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
// 如果是系统功能菜单配置,使用特殊处理
if (Constant.SYSTEM_WEB_MENU.equals(dto.getParamCode())) {
sysParamsService.updateSystemWebMenu(dto.getParamValue());
} else {
sysParamsService.update(dto);
}
configService.getConfig(false);
return new Result<Void>();
}
@@ -278,10 +272,7 @@ public class SysParamsController {
// 校验mqtt密钥长度和复杂度
private void validateMqttSecretLength(String paramCode, String secret) {
if (!paramCode.equals(Constant.SERVER_MQTT_SECRET)
&& !paramCode.equals(Constant.MQTT_SERVER_SIGNATURE_KEY)
&& !paramCode.equals(
Constant.MQTT_SERVER_MANAGER_API_SECRET)) {
if (!paramCode.equals(Constant.SERVER_MQTT_SECRET)) {
return;
}
if (StringUtils.isBlank(secret) || secret.equals("null")) {
@@ -31,7 +31,7 @@ public class SysUserDTO implements Serializable {
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
private Long id;
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "用户名", required = true)
@NotBlank(message = "{sysuser.username.require}", groups = DefaultGroup.class)
private String username;
@@ -40,14 +40,14 @@ public class SysUserDTO implements Serializable {
@NotBlank(message = "{sysuser.password.require}", groups = AddGroup.class)
private String password;
@Schema(description = "姓名", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "姓名", required = true)
@NotBlank(message = "{sysuser.realname.require}", groups = DefaultGroup.class)
private String realName;
@Schema(description = "头像")
private String headUrl;
@Schema(description = "性别 0:男 1:女 2:保密", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "性别 0:男 1:女 2:保密", required = true)
@Range(min = 0, max = 2, message = "{sysuser.gender.range}", groups = DefaultGroup.class)
private Integer gender;
@@ -58,11 +58,11 @@ public class SysUserDTO implements Serializable {
@Schema(description = "手机号")
private String mobile;
@Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "部门ID", required = true)
@NotNull(message = "{sysuser.deptId.require}", groups = DefaultGroup.class)
private Long deptId;
@Schema(description = "状态 0:停用 1:正常", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "状态 0:停用 1:正常", required = true)
@Range(min = 0, max = 1, message = "{sysuser.status.range}", groups = DefaultGroup.class)
private Integer status;
@@ -53,19 +53,4 @@ public interface SysParamsService extends BaseService<SysParamsEntity> {
* 初始化服务器密钥
*/
void initServerSecret();
/**
* 获取系统功能菜单配置
*
* @param fromCache 是否从缓存获取
* @return 系统功能菜单配置JSON字符串
*/
String getSystemWebMenu(boolean fromCache);
/**
* 更新系统功能菜单配置(自动处理功能相关的插件清理)
*
* @param configJson 新的系统功能菜单配置JSON
*/
void updateSystemWebMenu(String configJson);
}

Some files were not shown because too many files have changed in this diff Show More