diff --git a/README.md b/README.md index 0737c834..b018d5b6 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,11 @@ + + + MCP接入点 + + @@ -170,8 +175,8 @@ #### 🚀 部署方式选择 | 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 | |---------|------|---------|---------|---------|---------| -| **最简化安装** | 智能对话、IOT、MCP、视觉感知,数据存储在配置文件 | 低配置环境,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - | -| **全模块安装** | 智能对话、IOT、MCP、视觉感知、OTA、智控台,数据存储在数据库 | 完整功能体验 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) | +| **最简化安装** | 智能对话、IOT、MCP、视觉感知 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - | +| **全模块安装** | 智能对话、IOT、MCP接入点、视觉感知、OTA、智控台 | 完整功能体验,数据存储在数据库 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) | > 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据 @@ -186,18 +191,22 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ #### 🚩 配置说明和推荐 > [!Note] -> 本项目默认的配置是`入门全免费`设置,如果想效果更优,推荐使用`流式配置`。 +> 本项目提供两种配置方案: > -> 本项目自`0.5.2`版本,已支持使用流式配置,相比`0.5`版本以前,响应速度提升约`2.5秒` +> 1. `入门全免费`配置:适合个人家庭使用,所有组件均采用免费方案,无需额外付费。 +> +> 2. `流式配置`:适合演示、培训、超过2个并发等场景,采用流式处理技术,响应速度更快,体验更佳。 +> +> 自`0.5.2`版本起,项目支持流式配置,相比早期版本,响应速度提升约`2.5秒`,显著改善用户体验。 | 模块名称 | 入门全免费设置 | 流式配置 | |:---:|:---:|:---:| -| ASR(语音识别) | FunASR(本地) | 👍DoubaoStreamASR(火山流式语音识别) | +| ASR(语音识别) | FunASR(本地) | 👍FunASRServer 或 👍DoubaoStreamASR | | LLM(大模型) | ChatGLMLLM(智谱glm-4-flash) | 👍DoubaoLLM(火山doubao-1-5-pro-32k-250115) | | VLLM(视觉大模型) | ChatGLMVLLM(智谱glm-4v-flash) | 👍QwenVLVLLM(千问qwen2.5-vl-3b-instructh) | -| TTS(语音合成) | 👍LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山双流式语音合成) | -| Intent(意图识别) | function_call(函数调用) | ✅function_call(函数调用) | -| Memory(记忆功能) | mem_local_short(本地短期记忆) | ✅mem_local_short(本地短期记忆) | +| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山双流式语音合成) | +| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) | +| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) | #### 🔧 测试工具 本项目提供以下测试工具,帮助您验证系统和选择合适的模型: @@ -222,7 +231,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ | 视觉感知系统 | 支持多种VLLM(视觉大模型),实现多模态交互 | | 意图识别系统 | 支持LLM意图识别、Function Call函数调用,提供插件化意图处理机制 | | 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 | -| IOT/MCP控制协议 | 支持设备注册管理、智能控制接口,同时支持IOT、MCP控制协议 | +| 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 | | 管理后台 | 提供Web管理界面,支持用户管理、系统配置和设备管理 | | 测试工具 | 提供性能测试工具、视觉模型测试工具和音频交互测试工具 | | 部署支持 | 支持Docker部署和本地部署,提供完整的配置文件管理 | diff --git a/docs/homeassistant-integration.md b/docs/homeassistant-integration.md index 474049c1..7e69c229 100644 --- a/docs/homeassistant-integration.md +++ b/docs/homeassistant-integration.md @@ -89,55 +89,25 @@ http://homeassistant.local:8123 公司,台灯,switch.iot_cn_831898993_socn1_on_p_2_1; ``` -这段字符,我们成为“设备清单字符”需要保存好,等一下有用。 - +这段字符,我们称为“设备清单字符”需要保存好,等一下有用。 #### 2. 登录`智控台` -使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来 +![image-20250504051716417](images/image-ha-integration-06.png) -编辑`plugins.home_assistant.devices`,把刚才整理的设备清单字符粘贴进去。 +使用管理员账号,登录`智控台`。在`智能体管理`,找到你的智能体,再点击`配置角色`。 +将意图识别设置成`函数调用`或`LLM意图识别`。这时你会看到右侧有一个`编辑功能`。点击`编辑功能`按钮,会弹出`功能管理`的框。 -编辑`plugins.home_assistant.base_url`,把你部署的`HomeAssistant`接口地址粘贴进去,我粘贴进去的地址是这样的 +在`功能管理`的框里,你需要勾选`HomeAssistant设备状态查询`和`HomeAssistant设备状态修改`。 -``` -http://192.168.4.7:8123 -``` +勾选后,在`已选功能`点击`HomeAssistant设备状态查询`,然后在`参数配置`里配置你的`HomeAssistant`地址、密钥、设备清单字符。 -编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去 +编辑好后,点击`保存配置`,这时`功能管理`的框会隐藏,这时你再点击保存智能体配置。 +保存成功后,即可唤醒设备操作。 -#### 3. 设置`意图识别`函数 - -在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑 - -然后在弹框中,在原来的基础上追加两个函数:“hass_get_state”和“hass_set_state” - -修改前 -``` -change_role;get_weather;get_news;play_music -``` - -修改后 - -``` -change_role;get_weather;get_news;play_music;hass_get_state;hass_set_state -``` - -#### 4. 手动重启xiaozhi-server - -重启xiaozhi-server程序 - - -#### 5. 确认角色配置是否设置了函数意图识别 - -在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色” - -确认意图识别(Intent),是否选择“函数调用意图识别” - - -#### 6. 唤醒设别进行控制 +#### 3. 唤醒设别进行控制 尝试和esp32说,“打开XXX灯” diff --git a/docs/images/demo13.png b/docs/images/demo13.png new file mode 100644 index 00000000..1ad79fa3 Binary files /dev/null and b/docs/images/demo13.png differ diff --git a/docs/images/image-ha-integration-06.png b/docs/images/image-ha-integration-06.png new file mode 100644 index 00000000..56ef98fc Binary files /dev/null and b/docs/images/image-ha-integration-06.png differ diff --git a/docs/mcp-endpoint-integration.md b/docs/mcp-endpoint-integration.md new file mode 100644 index 00000000..ca72aef1 --- /dev/null +++ b/docs/mcp-endpoint-integration.md @@ -0,0 +1,134 @@ +# MCP 接入点部署使用指南 + +本教程包含2个部分 +- 1、如何开启mcp接入点 +- 2、如何为智能体接入一个简单的mcp功能,如计算器功能 + +部署的前提条件: +- 1、你已经部署了全模块,因为mcp接入点需要全模块中的智控台功能 +- 2、你想在不修改xiaozhi-server项目的前提下,扩展小智的功能 + +# 如何开启mcp接入点 + +## 第一步,下载mcp接入点项目源码 + +浏览器打开[mcp接入点项目地址](https://github.com/xinnan-tech/mcp-endpoint-server) + +打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。 + +点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`mcp-endpoint-server-main` +你需要把它重命名成`mcp-endpoint-server`。 + +## 第二步,启动程序 +这个项目是一个很简单的项目,建议使用docker运行。不过如果你不想使用docker运行,你可以参考[这个页面](https://github.com/xinnan-tech/mcp-endpoint-server/blob/main/README_dev.md)使用源码运行。以下是docker运行的方法 + +``` +# 进入本项目源码根目录 +cd mcp-endpoint-server + +# 清除缓存 +docker compose -f docker-compose.yml down +docker stop mcp-endpoint-server +docker rm mcp-endpoint-server +docker rmi ghcr.nju.edu.cn/xinnan-tech/mcp-endpoint-server:latest + +# 启动docker容器 +docker compose -f docker-compose.yml up -d +# 查看日志 +docker logs -f mcp-endpoint-server +``` + +此时,日志里会输出类似以下的日志 +``` +====================================================== +接口地址: http://172.1.1.1:8004/mcp_endpoint/health?key=xxxx +=======上面的地址是MCP接入点地址,请勿泄露给任何人============ +``` + +请你把接口地址复制出来: + +由于你是docker部署,切不可直接使用上面的地址! + +由于你是docker部署,切不可直接使用上面的地址! + +由于你是docker部署,切不可直接使用上面的地址! + +你先把地址复制出来,放在一个草稿里,你要知道你的电脑的局域网ip是什么,例如我的电脑局域网ip是`192.168.1.25`,那么 +原来我的接口地址 +``` +http://172.1.1.1:8004/mcp_endpoint/health?key=xxxx +``` +就要改成 +``` +http://192.168.1.25:8004/mcp_endpoint/health?key=xxxx +``` + +改好后,请使用浏览器直接访问这个接口。当浏览器出现类似这样的代码,说明是成功了。 +``` +{"result":{"status":"success","connections":{"tool_connections":0,"robot_connections":0,"total_connections":0}},"error":null,"id":null,"jsonrpc":"2.0"} +``` + +请你保留好这个`接口地址`,下一步要用到。 + +## 第三步,配置智控台 + +使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。 + +然后搜索参数`server.mcp_endpoint`,此时,它的值应该是`null`值。 +点击修改按钮,把上一步得来的`接口地址`粘贴到`参数值`里。然后保存。 + +如果能保存成功,说明一切顺利,你可以去智能体查看效果了。如果不成功,说明智控台无法访问mcp接入点,很大概率是网络防火墙,或者没有填写正确的局域网ip。 + +# 如何为智能体接入一个简单的mcp功能,如计算器功能 + +如果以上步骤顺利,你可以进入智能体管理,点击`配置角色`,在`意图识别`的右边,有一个`编辑功能`的按钮。 + +点击这个按钮。在弹出的页面里,位于底部,会有`MCP接入点`,正常来说,会显示这个智能体的`MCP接入点地址`,接下来,我们来给这个智能体扩展一个基于MCP技术的计算器的功能。 + +这个`MCP接入点地址`很重要,你等一下会用到。 + +## 第一步 下载虾哥MCP计算器项目代码 + +浏览器打开虾哥写的[计算器项目](https://github.com/78/mcp-calculator), + +打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。 + +点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`mcp-calculatorr-main` +你需要把它重命名成`mcp-calculator`。接下来,我们用命令行进入项目目录即安装依赖 + + +```bash +# 进入项目目录 +cd mcp-calculator + +conda remove -n mcp-calculator --all -y +conda create -n mcp-calculator python=3.10 -y +conda activate mcp-calculator + +pip install -r requirements.txt +``` + +## 第二步 启动 + +启动前,先从你的智控台的智能体里,复制到了MCP接入点的地址。 + +例如我的智能体的mcp地址是 +``` +ws://192.168.4.7:8004/mcp_endpoint/mcp/?token=abc +``` + +开始输入命令 + +```bash +export MCP_ENDPOINT=ws://192.168.4.7:8004/mcp_endpoint/mcp/?token=abc +``` + +输入完后,启动程序 + +```bash +python mcp_pipe.py calculator.py +``` + + +启动完后,你再进入智控台,点击刷新MCP的接入状态,就会看到你扩展的功能列表了。 + diff --git a/docs/newsnow_plugin_config.md b/docs/newsnow_plugin_config.md new file mode 100644 index 00000000..88744423 --- /dev/null +++ b/docs/newsnow_plugin_config.md @@ -0,0 +1,84 @@ +# get_news_from_newsnow 插件新闻源配置指南 + +## 概述 + +`get_news_from_newsnow` 插件现在支持通过Web管理界面动态配置新闻源,不再需要修改代码。用户可以在智控台中为每个智能体配置不同的新闻源。 + +## 配置方式 + +### 1. 通过Web管理界面配置(推荐) + +1. 登录智控台 +2. 进入"角色配置"页面 +3. 选择要配置的智能体 +4. 点击"编辑功能"按钮 +5. 在右侧参数配置区域找到"newsnow新闻聚合"插件 +6. 在"新闻源配置"字段中输入JSON格式的新闻源配置 + +### 2. 配置文件方式 + +在 `config.yaml` 中配置: + +```yaml +plugins: + get_news_from_newsnow: + url: "https://newsnow.busiyi.world/api/s?id=" + news_sources: + thepaper: "澎湃新闻" + baidu: "百度热搜" + cls-depth: "财联社" + # 可以添加更多新闻源 + weibo: "微博头条" + douyin: "抖音热搜" + solidot: "奇客Solidot" +``` + +## 新闻源配置格式 + +新闻源配置使用JSON格式,格式为: + +```json +{ + "source_id": "显示名称", + "source_id2": "显示名称2" +} +``` + +### 配置示例 + +```json +{ + "thepaper": "澎湃新闻", + "baidu": "百度热搜", + "cls-depth": "财联社", + "weibo": "微博头条", + "douyin": "抖音热搜", + "solidot": "奇客Solidot" +} +``` + +## 默认配置 + +如果未配置新闻源,插件将使用以下默认配置: + +```json +{ + "thepaper": "澎湃新闻", + "baidu": "百度热搜", + "cls-depth": "财联社" +} +``` + +## 使用说明 + +1. **配置新闻源**:在Web界面或配置文件中设置新闻源 +2. **调用插件**:用户可以说"播报新闻"或"获取新闻" +3. **指定新闻源**:用户可以说"播报澎湃新闻"或"获取百度热搜" +4. **获取详情**:用户可以说"详细介绍这条新闻" + +## 注意事项 + +1. 新闻源的 `source_id` 必须与API接口支持的ID一致 +2. 配置更改后需要重启服务或重新加载配置 +3. 如果配置的新闻源无效,插件会自动使用默认新闻源 +4. JSON格式必须正确,否则会使用默认配置 \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index 44294756..32a121fa 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -111,6 +111,11 @@ public interface Constant { */ String FILE_EXTENSION_SEG = "."; + /** + * mcp接入点路径 + */ + String SERVER_MCP_ENDPOINT = "server.mcp_endpoint"; + /** * 无记忆 */ @@ -227,7 +232,7 @@ public interface Constant { /** * 版本号 */ - public static final String VERSION = "0.5.5"; + public static final String VERSION = "0.6.1"; /** * 无效固件URL diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/AESUtils.java b/main/manager-api/src/main/java/xiaozhi/common/utils/AESUtils.java new file mode 100644 index 00000000..660e3cba --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/AESUtils.java @@ -0,0 +1,78 @@ +package xiaozhi.common.utils; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +public class AESUtils { + + private static final String ALGORITHM = "AES"; + private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding"; + + /** + * AES加密 + * + * @param key 密钥(16位、24位或32位) + * @param plainText 待加密字符串 + * @return 加密后的Base64字符串 + */ + public static String encrypt(String key, String plainText) { + try { + // 确保密钥长度为16、24或32位 + byte[] keyBytes = padKey(key.getBytes(StandardCharsets.UTF_8)); + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encryptedBytes); + } catch (Exception e) { + throw new RuntimeException("AES加密失败", e); + } + } + + /** + * AES解密 + * + * @param key 密钥(16位、24位或32位) + * @param encryptedText 待解密的Base64字符串 + * @return 解密后的字符串 + */ + public static String decrypt(String key, String encryptedText) { + try { + // 确保密钥长度为16、24或32位 + byte[] keyBytes = padKey(key.getBytes(StandardCharsets.UTF_8)); + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + + byte[] encryptedBytes = Base64.getDecoder().decode(encryptedText); + byte[] decryptedBytes = cipher.doFinal(encryptedBytes); + return new String(decryptedBytes, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException("AES解密失败", e); + } + } + + /** + * 填充密钥到指定长度(16、24或32位) + * + * @param keyBytes 原始密钥字节数组 + * @return 填充后的密钥字节数组 + */ + private static byte[] padKey(byte[] keyBytes) { + int keyLength = keyBytes.length; + if (keyLength == 16 || keyLength == 24 || keyLength == 32) { + return keyBytes; + } + + // 如果密钥长度不足,用0填充;如果超过,截取前32位 + byte[] paddedKey = new byte[32]; + System.arraycopy(keyBytes, 0, paddedKey, 0, Math.min(keyLength, 32)); + return paddedKey; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/HashEncryptionUtil.java b/main/manager-api/src/main/java/xiaozhi/common/utils/HashEncryptionUtil.java new file mode 100644 index 00000000..79b05192 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/HashEncryptionUtil.java @@ -0,0 +1,52 @@ +package xiaozhi.common.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * 哈希加密算法的工具类 + * @author zjy + */ +@Slf4j +public class HashEncryptionUtil { + /** + * 使用md5进行加密 + * @param context 被加密的内容 + * @return 哈希值 + */ + public static String Md5hexDigest(String context){ + return hexDigest(context,"MD5"); + } + + /** + * 指定哈希算法进行加密 + * @param context 被加密的内容 + * @param algorithm 哈希算法 + * @return 哈希值 + */ + public static String hexDigest(String context,String algorithm ){ + // 获取MD5算法实例 + MessageDigest md = null; + try { + md = MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + log.error("加密失败的算法:{}",algorithm); + throw new RuntimeException("加密失败,"+ algorithm +"哈希算法系统不支持"); + } + // 计算智能体id的MD5值 + byte[] messageDigest = md.digest(context.getBytes()); + // 将字节数组转换为十六进制字符串 + StringBuilder hexString = new StringBuilder(); + for (byte b : messageDigest) { + String hex = Integer.toHexString(0xFF & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + +} diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/ResultUtils.java b/main/manager-api/src/main/java/xiaozhi/common/utils/ResultUtils.java new file mode 100644 index 00000000..3f9fe27d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/ResultUtils.java @@ -0,0 +1,31 @@ +package xiaozhi.common.utils; + +/** + * 返回响应体工具类 + */ +public class ResultUtils +{ + public static Result success(T data) { + return new Result().ok(data); + } + + public static Result error() { + return new Result().error(); + } + + public static Result error(String msg) { + return new Result().error(msg); + } + + public static Result error(int errorCode, String msg) { + return new Result().error(errorCode, msg); + } + + public static Result error(int errorCode) { + return new Result().error(errorCode); + } + + public static Result empty() { + return new Result(); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index 8aa912a4..bfd82e41 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -1,6 +1,5 @@ package xiaozhi.modules.agent.controller; -import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; @@ -33,8 +32,8 @@ import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.user.UserDetail; -import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.Result; +import xiaozhi.common.utils.ResultUtils; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatSessionDTO; import xiaozhi.modules.agent.dto.AgentCreateDTO; @@ -45,8 +44,10 @@ import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatHistoryService; +import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; +import xiaozhi.modules.agent.vo.AgentInfoVO; import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.security.user.SecurityUser; @@ -61,6 +62,7 @@ public class AgentController { private final DeviceService deviceService; private final AgentChatHistoryService agentChatHistoryService; private final AgentChatAudioService agentChatAudioService; + private final AgentPluginMappingService agentPluginMappingService; private final RedisUtils redisUtils; @GetMapping("/list") @@ -88,46 +90,17 @@ public class AgentController { @GetMapping("/{id}") @Operation(summary = "获取智能体详情") @RequiresPermissions("sys:role:normal") - public Result getAgentById(@PathVariable("id") String id) { - AgentEntity agent = agentService.getAgentById(id); - return new Result().ok(agent); + public Result getAgentById(@PathVariable("id") String id) { + AgentInfoVO agent = agentService.getAgentById(id); + return ResultUtils.success(agent); } @PostMapping @Operation(summary = "创建智能体") @RequiresPermissions("sys:role:normal") public Result save(@RequestBody @Valid AgentCreateDTO dto) { - AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class); - - // 获取默认模板 - AgentTemplateEntity template = agentTemplateService.getDefaultTemplate(); - if (template != null) { - // 设置模板中的默认值 - entity.setAsrModelId(template.getAsrModelId()); - entity.setVadModelId(template.getVadModelId()); - entity.setLlmModelId(template.getLlmModelId()); - entity.setVllmModelId(template.getVllmModelId()); - entity.setTtsModelId(template.getTtsModelId()); - entity.setTtsVoiceId(template.getTtsVoiceId()); - entity.setMemModelId(template.getMemModelId()); - entity.setIntentModelId(template.getIntentModelId()); - entity.setSystemPrompt(template.getSystemPrompt()); - entity.setSummaryMemory(template.getSummaryMemory()); - entity.setChatHistoryConf(template.getChatHistoryConf()); - entity.setLangCode(template.getLangCode()); - entity.setLanguage(template.getLanguage()); - } - - // 设置用户ID和创建者信息 - UserDetail user = SecurityUser.getUser(); - entity.setUserId(user.getId()); - entity.setCreator(user.getId()); - entity.setCreatedAt(new Date()); - - // ID、智能体编码和排序会在Service层自动生成 - agentService.insert(entity); - - return new Result().ok(entity.getId()); + String agentId = agentService.createAgent(dto); + return new Result().ok(agentId); } @PutMapping("/saveMemory/{macAddress}") @@ -139,88 +112,15 @@ public class AgentController { } AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO(); agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory()); - return updateAgentById(device.getAgentId(), agentUpdateDTO); + agentService.updateAgentById(device.getAgentId(), agentUpdateDTO); + return new Result<>(); } @PutMapping("/{id}") @Operation(summary = "更新智能体") @RequiresPermissions("sys:role:normal") public Result update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) { - return updateAgentById(id, dto); - } - - private Result updateAgentById(String id, AgentUpdateDTO dto) { - // 先查询现有实体 - AgentEntity existingEntity = agentService.getAgentById(id); - if (existingEntity == null) { - return new Result().error("智能体不存在"); - } - - // 只更新提供的非空字段 - if (dto.getAgentName() != null) { - existingEntity.setAgentName(dto.getAgentName()); - } - if (dto.getAgentCode() != null) { - existingEntity.setAgentCode(dto.getAgentCode()); - } - if (dto.getAsrModelId() != null) { - existingEntity.setAsrModelId(dto.getAsrModelId()); - } - if (dto.getVadModelId() != null) { - existingEntity.setVadModelId(dto.getVadModelId()); - } - if (dto.getLlmModelId() != null) { - existingEntity.setLlmModelId(dto.getLlmModelId()); - } - if (dto.getVllmModelId() != null) { - existingEntity.setVllmModelId(dto.getVllmModelId()); - } - if (dto.getTtsModelId() != null) { - existingEntity.setTtsModelId(dto.getTtsModelId()); - } - if (dto.getTtsVoiceId() != null) { - existingEntity.setTtsVoiceId(dto.getTtsVoiceId()); - } - if (dto.getMemModelId() != null) { - existingEntity.setMemModelId(dto.getMemModelId()); - } - if (dto.getIntentModelId() != null) { - existingEntity.setIntentModelId(dto.getIntentModelId()); - } - if (dto.getSystemPrompt() != null) { - existingEntity.setSystemPrompt(dto.getSystemPrompt()); - } - if (dto.getSummaryMemory() != null) { - existingEntity.setSummaryMemory(dto.getSummaryMemory()); - } - if (dto.getChatHistoryConf() != null) { - existingEntity.setChatHistoryConf(dto.getChatHistoryConf()); - } - if (dto.getLangCode() != null) { - existingEntity.setLangCode(dto.getLangCode()); - } - if (dto.getLanguage() != null) { - existingEntity.setLanguage(dto.getLanguage()); - } - if (dto.getSort() != null) { - existingEntity.setSort(dto.getSort()); - } - - // 设置更新者信息 - UserDetail user = SecurityUser.getUser(); - existingEntity.setUpdater(user.getId()); - existingEntity.setUpdatedAt(new Date()); - - // 更新记忆策略 - if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { - // 删除所有记录 - agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true); - existingEntity.setSummaryMemory(""); - } else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) { - // 删除音频数据 - agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false); - } - agentService.updateById(existingEntity); + agentService.updateAgentById(id, dto); return new Result<>(); } @@ -232,6 +132,8 @@ public class AgentController { deviceService.deleteByAgentId(id); // 删除关联的聊天记录 agentChatHistoryService.deleteByAgentId(id, true, true); + // 删除关联的插件 + agentPluginMappingService.deleteByAgentId(id); // 再删除智能体 agentService.deleteById(id); return new Result<>(); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java new file mode 100644 index 00000000..3f369774 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java @@ -0,0 +1,66 @@ +package xiaozhi.modules.agent.controller; + +import java.util.List; + +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import xiaozhi.common.user.UserDetail; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.agent.service.AgentMcpAccessPointService; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.security.user.SecurityUser; + +@Tag(name = "智能体Mcp接入点管理") +@RequiredArgsConstructor +@RestController +@RequestMapping("/agent/mcp") +public class AgentMcpAccessPointController { + private final AgentMcpAccessPointService agentMcpAccessPointService; + private final AgentService agentService; + + /** + * 获取智能体的Mcp接入点地址 + * + * @param audioId 智能体id + * @return 返回错误提醒或者Mcp接入点地址 + */ + @Operation(summary = "获取智能体的Mcp接入点地址") + @GetMapping("/address/{agentId}") + @RequiresPermissions("sys:role:normal") + public Result getAgentMcpAccessAddress(@PathVariable("agentId") String agentId) { + // 获取当前用户 + UserDetail user = SecurityUser.getUser(); + + // 检查权限 + if (!agentService.checkAgentPermission(agentId, user.getId())) { + return new Result().error("没有权限查看该智能体的MCP接入点地址"); + } + String agentMcpAccessAddress = agentMcpAccessPointService.getAgentMcpAccessAddress(agentId); + if (agentMcpAccessAddress == null) { + return new Result().ok("请联系管理员进入参数管理配置mcp接入点地址"); + } + return new Result().ok(agentMcpAccessAddress); + } + + @Operation(summary = "获取智能体的Mcp工具列表") + @GetMapping("/tools/{agentId}") + @RequiresPermissions("sys:role:normal") + public Result> getAgentMcpToolsList(@PathVariable("agentId") String agentId) { + // 获取当前用户 + UserDetail user = SecurityUser.getUser(); + + // 检查权限 + if (!agentService.checkAgentPermission(agentId, user.getId())) { + return new Result>().error("没有权限查看该智能体的MCP工具列表"); + } + List agentMcpToolsList = agentMcpAccessPointService.getAgentMcpToolsList(agentId); + return new Result>().ok(agentMcpToolsList); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java index 3b4be452..03e569cd 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java @@ -6,6 +6,7 @@ import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import xiaozhi.common.dao.BaseDao; import xiaozhi.modules.agent.entity.AgentEntity; +import xiaozhi.modules.agent.vo.AgentInfoVO; @Mapper public interface AgentDao extends BaseDao { @@ -28,4 +29,11 @@ public interface AgentDao extends BaseDao { " WHERE d.mac_address = #{macAddress} " + " ORDER BY d.id DESC LIMIT 1") AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress); + + /** + * 根据id查询agent信息,包括插件信息 + * + * @param agentId 智能体ID + */ + AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentPluginMappingMapper.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentPluginMappingMapper.java new file mode 100644 index 00000000..2fe81753 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentPluginMappingMapper.java @@ -0,0 +1,22 @@ +package xiaozhi.modules.agent.dao; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import xiaozhi.modules.agent.entity.AgentPluginMapping; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Mapper +* @createDate 2025-05-25 22:33:17 +* @Entity xiaozhi.modules.agent.entity.AgentPluginMapping +*/ +@Mapper +public interface AgentPluginMappingMapper extends BaseMapper { + List selectPluginsByAgentId(@Param("agentId") String agentId); +} + + + + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java index 32190d98..0e3d9bc3 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java @@ -1,6 +1,8 @@ package xiaozhi.modules.agent.dto; import java.io.Serializable; +import java.util.HashMap; +import java.util.List; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -15,19 +17,19 @@ import lombok.Data; public class AgentUpdateDTO implements Serializable { private static final long serialVersionUID = 1L; - @Schema(description = "智能体编码", example = "AGT_1234567890", required = false) + @Schema(description = "智能体编码", example = "AGT_1234567890", nullable = true) private String agentCode; - @Schema(description = "智能体名称", example = "客服助手", required = false) + @Schema(description = "智能体名称", example = "客服助手", nullable = true) private String agentName; - @Schema(description = "语音识别模型标识", example = "asr_model_02", required = false) + @Schema(description = "语音识别模型标识", example = "asr_model_02", nullable = true) private String asrModelId; - @Schema(description = "语音活动检测标识", example = "vad_model_02", required = false) + @Schema(description = "语音活动检测标识", example = "vad_model_02", nullable = true) private String vadModelId; - @Schema(description = "大语言模型标识", example = "llm_model_02", required = false) + @Schema(description = "大语言模型标识", example = "llm_model_02", nullable = true) private String llmModelId; @Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false) @@ -36,31 +38,46 @@ public class AgentUpdateDTO implements Serializable { @Schema(description = "语音合成模型标识", example = "tts_model_02", required = false) private String ttsModelId; - @Schema(description = "音色标识", example = "voice_02", required = false) + @Schema(description = "音色标识", example = "voice_02", nullable = true) private String ttsVoiceId; - @Schema(description = "记忆模型标识", example = "mem_model_02", required = false) + @Schema(description = "记忆模型标识", example = "mem_model_02", nullable = true) private String memModelId; - @Schema(description = "意图模型标识", example = "intent_model_02", required = false) + @Schema(description = "意图模型标识", example = "intent_model_02", nullable = true) private String intentModelId; - @Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false) + @Schema(description = "插件函数信息", nullable = true) + private List functions; + + @Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", nullable = true) private String systemPrompt; - @Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" + - "根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false) + @Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" + + "根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", nullable = true) private String summaryMemory; - @Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false) + @Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", nullable = true) private Integer chatHistoryConf; - @Schema(description = "语言编码", example = "zh_CN", required = false) + @Schema(description = "语言编码", example = "zh_CN", nullable = true) private String langCode; - @Schema(description = "交互语种", example = "中文", required = false) + @Schema(description = "交互语种", example = "中文", nullable = true) private String language; - @Schema(description = "排序", example = "1", required = false) + @Schema(description = "排序", example = "1", nullable = true) private Integer sort; + + @Data + @Schema(description = "插件函数信息") + public static class FunctionInfo implements Serializable { + @Schema(description = "插件ID", example = "plugin_01") + private String pluginId; + + @Schema(description = "函数参数信息", nullable = true) + private HashMap paramInfo; + + private static final long serialVersionUID = 1L; + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcRequest.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcRequest.java new file mode 100644 index 00000000..e4fac002 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcRequest.java @@ -0,0 +1,32 @@ +package xiaozhi.modules.agent.dto; + +import lombok.Data; + +/** + * MCP JSON-RPC 请求 DTO + */ +@Data +public class McpJsonRpcRequest { + private String jsonrpc = "2.0"; + private String method; + private Object params; + private Integer id; + + public McpJsonRpcRequest() { + } + + public McpJsonRpcRequest(String method) { + this.method = method; + } + + public McpJsonRpcRequest(String method, Object params, Integer id) { + this.method = method; + this.params = params; + this.id = id; + } + + public McpJsonRpcRequest(String method, Object params) { + this.method = method; + this.params = params; + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcResponse.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcResponse.java new file mode 100644 index 00000000..1bd667e6 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcResponse.java @@ -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() { + } + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentPluginMapping.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentPluginMapping.java new file mode 100644 index 00000000..ae99b0eb --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentPluginMapping.java @@ -0,0 +1,54 @@ +package xiaozhi.modules.agent.entity; + +import java.io.Serializable; + +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; + +/** + * Agent与插件的唯一映射表 + * + * @TableName ai_agent_plugin_mapping + */ +@Data +@TableName(value = "ai_agent_plugin_mapping") +@Schema(description = "Agent与插件的唯一映射表") +public class AgentPluginMapping implements Serializable { + /** + * 主键 + */ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "映射信息主键ID") + private Long id; + + /** + * 智能体ID + */ + @Schema(description = "智能体ID") + private String agentId; + + /** + * 插件ID + */ + @Schema(description = "插件ID") + private String pluginId; + + /** + * 插件参数(Json)格式 + */ + @Schema(description = "插件参数(Json)格式") + private String paramInfo; + + // 冗余字段,用于方便在根据id查询插件时,对照查出插件的Provider_code,详见dao层xml文件 + @TableField(exist = false) + @Schema(description = "插件provider_code, 对应表ai_model_provider") + private String providerCode; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentMcpAccessPointService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentMcpAccessPointService.java new file mode 100644 index 00000000..336ade65 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentMcpAccessPointService.java @@ -0,0 +1,25 @@ +package xiaozhi.modules.agent.service; + + +import java.util.List; + +/** + * 智能体Mcp接入点处理service + * + * @author zjy + */ +public interface AgentMcpAccessPointService { + /** + * 获取智能体的mcp接入点地址 + * @param id 智能体id + * @return mcp接入点地址 + */ + String getAgentMcpAccessAddress(String id); + + /** + * 获取智能体的mcp接入点已有的工具列表 + * @param id 智能体id + * @return 工具列表 + */ + List getAgentMcpToolsList(String id); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java new file mode 100644 index 00000000..79d97d2e --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java @@ -0,0 +1,29 @@ +package xiaozhi.modules.agent.service; + +import java.util.List; + +import com.baomidou.mybatisplus.extension.service.IService; + +import xiaozhi.modules.agent.entity.AgentPluginMapping; + +/** + * @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service + * @createDate 2025-05-25 22:33:17 + */ +public interface AgentPluginMappingService extends IService { + + /** + * 根据智能体id获取插件参数 + * + * @param agentId + * @return + */ + List agentPluginParamsByAgentId(String agentId); + + /** + * 根据智能体id删除插件参数 + * + * @param agentId + */ + void deleteByAgentId(String agentId); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java index 2104d353..a9d4e5bb 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java @@ -5,8 +5,11 @@ import java.util.Map; 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.AgentUpdateDTO; import xiaozhi.modules.agent.entity.AgentEntity; +import xiaozhi.modules.agent.vo.AgentInfoVO; /** * 智能体表处理service @@ -30,7 +33,7 @@ public interface AgentService extends BaseService { * @param id 智能体ID * @return 智能体实体 */ - AgentEntity getAgentById(String id); + AgentInfoVO getAgentById(String id); /** * 插入智能体 @@ -79,4 +82,20 @@ public interface AgentService extends BaseService { * @return 是否有权限 */ boolean checkAgentPermission(String agentId, Long userId); + + /** + * 更新智能体 + * + * @param agentId 智能体ID + * @param dto 更新智能体所需的信息 + */ + void updateAgentById(String agentId, AgentUpdateDTO dto); + + /** + * 创建智能体 + * + * @param dto 创建智能体所需的信息 + * @return 创建的智能体ID + */ + String createAgent(AgentCreateDTO dto); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java index 087c1b9d..36b59993 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java @@ -19,6 +19,8 @@ import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService; +import xiaozhi.modules.device.entity.DeviceEntity; +import xiaozhi.modules.device.service.DeviceService; /** * {@link AgentChatHistoryBizService} impl @@ -35,6 +37,7 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic private final AgentChatHistoryService agentChatHistoryService; private final AgentChatAudioService agentChatAudioService; private final RedisUtils redisUtils; + private final DeviceService deviceService; /** * 处理聊天记录上报,包括文件上传和相关信息记录 @@ -68,6 +71,15 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic // 更新设备最后对话时间 redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date()); + + // 更新设备最后连接时间 + DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); + if (device != null) { + deviceService.updateDeviceConnectionInfo(agentId, device.getId(), null); + } else { + log.warn("聊天记录上报时,未找到mac地址为 {} 的设备", macAddress); + } + return Boolean.TRUE; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java new file mode 100644 index 00000000..e45d2201 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java @@ -0,0 +1,180 @@ +package xiaozhi.modules.agent.service.impl; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.utils.AESUtils; +import xiaozhi.common.utils.HashEncryptionUtil; +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.modules.agent.dto.McpJsonRpcRequest; +import xiaozhi.modules.agent.dto.McpJsonRpcResponse; +import xiaozhi.modules.agent.service.AgentMcpAccessPointService; +import xiaozhi.modules.sys.service.SysParamsService; +import xiaozhi.modules.sys.utils.WebSocketClientManager; + +@AllArgsConstructor +@Service +@Slf4j +public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointService { + private SysParamsService sysParamsService; + + @Override + public String getAgentMcpAccessAddress(String id) { + // 获取到mcp的地址 + String url = sysParamsService.getValue(Constant.SERVER_MCP_ENDPOINT, true); + if (StringUtils.isBlank(url) || "null".equals(url)) { + return null; + } + URI uri = getURI(url); + // 获取智能体mcp的url前缀 + String agentMcpUrl = getAgentMcpUrl(uri); + // 获取密钥 + String key = getSecretKey(uri); + // 获取加密的token + String encryptToken = encryptToken(id, key); + // 对token进行URL编码 + String encodedToken = URLEncoder.encode(encryptToken, StandardCharsets.UTF_8); + // 返回智能体Mcp路径的格式 + agentMcpUrl = "%s/mcp/?token=%s".formatted(agentMcpUrl, encodedToken); + return agentMcpUrl; + } + + @Override + public List getAgentMcpToolsList(String id) { + String wsUrl = getAgentMcpAccessAddress(id); + if (StringUtils.isBlank(wsUrl)) { + return List.of(); + } + + // 将 /mcp 替换为 /call + wsUrl = wsUrl.replace("/mcp/", "/call/"); + + try { + // 创建 WebSocket 连接 + try (WebSocketClientManager client = WebSocketClientManager.build( + new WebSocketClientManager.Builder() + .uri(wsUrl) + .connectTimeout(5, TimeUnit.SECONDS) + .maxSessionDuration(20, TimeUnit.SECONDS))) { + + // 发送初始化通知 + McpJsonRpcRequest initRequest = new McpJsonRpcRequest("notifications/initialized"); + client.sendJson(initRequest); + + // 等待 0.2 秒 + Thread.sleep(200); + + // 发送工具列表请求 + McpJsonRpcRequest toolsRequest = new McpJsonRpcRequest("tools/list", null, 1); + client.sendJson(toolsRequest); + + // 监听响应,直到收到包含 id=1 的响应 + List responses = client.listener(response -> { + try { + McpJsonRpcResponse jsonResponse = JsonUtils.parseObject(response, McpJsonRpcResponse.class); + return jsonResponse != null && Integer.valueOf(1).equals(jsonResponse.getId()); + } catch (Exception e) { + log.warn("解析响应失败: {}", response, e); + return false; + } + }); + + // 处理响应 + for (String response : responses) { + try { + McpJsonRpcResponse jsonResponse = JsonUtils.parseObject(response, McpJsonRpcResponse.class); + if (jsonResponse != null && Integer.valueOf(1).equals(jsonResponse.getId()) + && jsonResponse.getResult() != null && jsonResponse.getResult().getTools() != null) { + + // 提取工具名称列表 + return java.util.Arrays.stream(jsonResponse.getResult().getTools()) + .map(McpJsonRpcResponse.McpTool::getName) + .collect(Collectors.toList()); + } + } catch (Exception e) { + log.warn("处理工具列表响应失败: {}", response, e); + } + } + + log.warn("未找到有效的工具列表响应"); + return List.of(); + + } + } catch (Exception e) { + log.error("获取智能体 MCP 工具列表失败,智能体ID: {}", id, e); + return List.of(); + } + } + + /** + * 获取URI对象 + * + * @param url 路径 + * @return URI对象 + */ + private static URI getURI(String url) { + try { + return new URI(url); + } catch (URISyntaxException e) { + log.error("路径格式不正确路径:{},\n错误信息:{}", url, e.getMessage()); + throw new RuntimeException("mcp的地址存在错误,请进入参数管理修改mcp接入点地址"); + } + } + + /** + * 获取密钥 + * + * @param uri mcp地址 + * @return 密钥 + */ + private static String getSecretKey(URI uri) { + // 获取参数 + String query = uri.getQuery(); + // 获取aes加密密钥 + String str = "key="; + return query.substring(query.indexOf(str) + str.length()); + } + + /** + * 获取智能体mcp接入点url + * + * @param uri mcp地址 + * @return 智能体mcp接入点url + */ + private String getAgentMcpUrl(URI uri) { + // 获取协议 + String wsScheme = (uri.getScheme().equals("https")) ? "wss" : "ws"; + // 获取主机,端口,路径 + String path = uri.getSchemeSpecificPart(); + // 获取到最后一个/前的path + path = path.substring(0, path.lastIndexOf("/")); + return wsScheme + ":" + path; + } + + /** + * 获取对智能体id加密的token + * + * @param agentId 智能体id + * @param key 加密密钥 + * @return 加密后token + */ + private static String encryptToken(String agentId, String key) { + // 使用md5对智能体id进行加密 + String md5 = HashEncryptionUtil.Md5hexDigest(agentId); + // aes需要加密文本 + String json = "{\"agentId\": \"%s\"}".formatted(md5); + // 加密后成token值 + return AESUtils.encrypt(key, json); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java new file mode 100644 index 00000000..2a53d474 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java @@ -0,0 +1,37 @@ +package xiaozhi.modules.agent.service.impl; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import lombok.RequiredArgsConstructor; +import xiaozhi.modules.agent.dao.AgentPluginMappingMapper; +import xiaozhi.modules.agent.entity.AgentPluginMapping; +import xiaozhi.modules.agent.service.AgentPluginMappingService; + +/** + * @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service实现 + * @createDate 2025-05-25 22:33:17 + */ +@Service +@RequiredArgsConstructor +public class AgentPluginMappingServiceImpl extends ServiceImpl + implements AgentPluginMappingService { + private final AgentPluginMappingMapper agentPluginMappingMapper; + + @Override + public List agentPluginParamsByAgentId(String agentId) { + return agentPluginMappingMapper.selectPluginsByAgentId(agentId); + } + + @Override + public void deleteByAgentId(String agentId) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("agent_id", agentId); + agentPluginMappingMapper.delete(updateWrapper); + } + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index ad6e75c3..36aa1192 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -1,12 +1,17 @@ package xiaozhi.modules.agent.service.impl; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; @@ -14,16 +19,30 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.AllArgsConstructor; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.service.impl.BaseServiceImpl; +import xiaozhi.common.user.UserDetail; +import xiaozhi.common.utils.ConvertUtils; +import xiaozhi.common.utils.JsonUtils; import xiaozhi.modules.agent.dao.AgentDao; +import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentDTO; +import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.entity.AgentEntity; +import xiaozhi.modules.agent.entity.AgentPluginMapping; +import xiaozhi.modules.agent.entity.AgentTemplateEntity; +import xiaozhi.modules.agent.service.AgentChatHistoryService; +import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.agent.service.AgentTemplateService; +import xiaozhi.modules.agent.vo.AgentInfoVO; import xiaozhi.modules.device.service.DeviceService; +import xiaozhi.modules.model.dto.ModelProviderDTO; import xiaozhi.modules.model.service.ModelConfigService; +import xiaozhi.modules.model.service.ModelProviderService; import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.sys.enums.SuperAdminEnum; import xiaozhi.modules.timbre.service.TimbreService; @@ -36,6 +55,10 @@ public class AgentServiceImpl extends BaseServiceImpl imp private final ModelConfigService modelConfigService; private final RedisUtils redisUtils; private final DeviceService deviceService; + private final AgentPluginMappingService agentPluginMappingService; + private final AgentChatHistoryService agentChatHistoryService; + private final AgentTemplateService agentTemplateService; + private final ModelProviderService modelProviderService; @Override public PageData adminAgentList(Map params) { @@ -46,15 +69,20 @@ public class AgentServiceImpl extends BaseServiceImpl imp } @Override - public AgentEntity getAgentById(String id) { - AgentEntity agent = agentDao.selectById(id); - if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { - agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode()); - } else if (agent != null && agent.getMemModelId() != null - && !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM) - && agent.getChatHistoryConf() == null) { - agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode()); + public AgentInfoVO getAgentById(String id) { + AgentInfoVO agent = agentDao.selectAgentInfoById(id); + + if (agent == null) { + throw new RenException("智能体不存在"); } + + if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { + agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode()); + if (agent.getChatHistoryConf() == null) { + agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode()); + } + } + // 无需额外查询插件列表,已通过SQL查询出来 return agent; } @@ -167,4 +195,198 @@ public class AgentServiceImpl extends BaseServiceImpl imp // 检查是否是智能体的所有者 return userId.equals(agent.getUserId()); } + + // 根据id更新智能体信息 + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAgentById(String agentId, AgentUpdateDTO dto) { + // 先查询现有实体 + AgentEntity existingEntity = this.getAgentById(agentId); + if (existingEntity == null) { + throw new RuntimeException("智能体不存在"); + } + + // 只更新提供的非空字段 + if (dto.getAgentName() != null) { + existingEntity.setAgentName(dto.getAgentName()); + } + if (dto.getAgentCode() != null) { + existingEntity.setAgentCode(dto.getAgentCode()); + } + if (dto.getAsrModelId() != null) { + existingEntity.setAsrModelId(dto.getAsrModelId()); + } + if (dto.getVadModelId() != null) { + existingEntity.setVadModelId(dto.getVadModelId()); + } + if (dto.getLlmModelId() != null) { + existingEntity.setLlmModelId(dto.getLlmModelId()); + } + if (dto.getVllmModelId() != null) { + existingEntity.setVllmModelId(dto.getVllmModelId()); + } + if (dto.getTtsModelId() != null) { + existingEntity.setTtsModelId(dto.getTtsModelId()); + } + if (dto.getTtsVoiceId() != null) { + existingEntity.setTtsVoiceId(dto.getTtsVoiceId()); + } + if (dto.getMemModelId() != null) { + existingEntity.setMemModelId(dto.getMemModelId()); + } + if (dto.getIntentModelId() != null) { + existingEntity.setIntentModelId(dto.getIntentModelId()); + } + if (dto.getSystemPrompt() != null) { + existingEntity.setSystemPrompt(dto.getSystemPrompt()); + } + if (dto.getSummaryMemory() != null) { + existingEntity.setSummaryMemory(dto.getSummaryMemory()); + } + if (dto.getChatHistoryConf() != null) { + existingEntity.setChatHistoryConf(dto.getChatHistoryConf()); + } + if (dto.getLangCode() != null) { + existingEntity.setLangCode(dto.getLangCode()); + } + if (dto.getLanguage() != null) { + existingEntity.setLanguage(dto.getLanguage()); + } + if (dto.getSort() != null) { + existingEntity.setSort(dto.getSort()); + } + + // 更新函数插件信息 + List functions = dto.getFunctions(); + if (functions != null) { + // 1. 收集本次提交的 pluginId + List newPluginIds = functions.stream() + .map(AgentUpdateDTO.FunctionInfo::getPluginId) + .toList(); + + // 2. 查询当前agent现有的所有映射 + List existing = agentPluginMappingService.list( + new QueryWrapper() + .eq("agent_id", agentId)); + Map existMap = existing.stream() + .collect(Collectors.toMap(AgentPluginMapping::getPluginId, Function.identity())); + + // 3. 构造所有要 保存或更新 的实体 + List allToPersist = functions.stream().map(info -> { + AgentPluginMapping m = new AgentPluginMapping(); + m.setAgentId(agentId); + m.setPluginId(info.getPluginId()); + m.setParamInfo(JsonUtils.toJsonString(info.getParamInfo())); + AgentPluginMapping old = existMap.get(info.getPluginId()); + if (old != null) { + // 已存在,设置id表示更新 + m.setId(old.getId()); + } + return m; + }).toList(); + + // 4. 拆分:已有ID的走更新,无ID的走插入 + List toUpdate = allToPersist.stream() + .filter(m -> m.getId() != null) + .toList(); + List toInsert = allToPersist.stream() + .filter(m -> m.getId() == null) + .toList(); + + if (!toUpdate.isEmpty()) { + agentPluginMappingService.updateBatchById(toUpdate); + } + if (!toInsert.isEmpty()) { + agentPluginMappingService.saveBatch(toInsert); + } + + // 5. 删除本次不在提交列表里的插件映射 + List toDelete = existing.stream() + .filter(old -> !newPluginIds.contains(old.getPluginId())) + .map(AgentPluginMapping::getId) + .toList(); + if (!toDelete.isEmpty()) { + agentPluginMappingService.removeBatchByIds(toDelete); + } + } + + // 设置更新者信息 + UserDetail user = SecurityUser.getUser(); + existingEntity.setUpdater(user.getId()); + existingEntity.setUpdatedAt(new Date()); + + // 更新记忆策略 + if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { + // 删除所有记录 + agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true); + existingEntity.setSummaryMemory(""); + } else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) { + // 删除音频数据 + agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false); + } + this.updateById(existingEntity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String createAgent(AgentCreateDTO dto) { + // 转换为实体 + AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class); + + // 获取默认模板 + AgentTemplateEntity template = agentTemplateService.getDefaultTemplate(); + if (template != null) { + // 设置模板中的默认值 + entity.setAsrModelId(template.getAsrModelId()); + entity.setVadModelId(template.getVadModelId()); + entity.setLlmModelId(template.getLlmModelId()); + entity.setVllmModelId(template.getVllmModelId()); + entity.setTtsModelId(template.getTtsModelId()); + entity.setTtsVoiceId(template.getTtsVoiceId()); + entity.setMemModelId(template.getMemModelId()); + entity.setIntentModelId(template.getIntentModelId()); + entity.setSystemPrompt(template.getSystemPrompt()); + entity.setSummaryMemory(template.getSummaryMemory()); + entity.setChatHistoryConf(template.getChatHistoryConf()); + entity.setLangCode(template.getLangCode()); + entity.setLanguage(template.getLanguage()); + } + + // 设置用户ID和创建者信息 + UserDetail user = SecurityUser.getUser(); + entity.setUserId(user.getId()); + entity.setCreator(user.getId()); + entity.setCreatedAt(new Date()); + + // 保存智能体 + insert(entity); + + // 设置默认插件 + List toInsert = new ArrayList<>(); + // 播放音乐、查天气、查新闻 + String[] pluginIds = new String[] { "SYSTEM_PLUGIN_MUSIC", "SYSTEM_PLUGIN_WEATHER", + "SYSTEM_PLUGIN_NEWS_NEWSNOW" }; + for (String pluginId : pluginIds) { + ModelProviderDTO provider = modelProviderService.getById(pluginId); + if (provider == null) { + continue; + } + AgentPluginMapping mapping = new AgentPluginMapping(); + mapping.setPluginId(pluginId); + + Map paramInfo = new HashMap<>(); + List> fields = JsonUtils.parseObject(provider.getFields(), List.class); + if (fields != null) { + for (Map field : fields) { + paramInfo.put((String) field.get("key"), field.get("default")); + } + } + mapping.setParamInfo(JsonUtils.toJsonString(paramInfo)); + mapping.setAgentId(entity.getId()); + toInsert.add(mapping); + } + // 保存默认插件 + agentPluginMappingService.saveBatch(toInsert); + return entity.getId(); + } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentTemplateServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentTemplateServiceImpl.java index 9ac3de25..581adbdd 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentTemplateServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentTemplateServiceImpl.java @@ -56,6 +56,9 @@ public class AgentTemplateServiceImpl extends ServiceImpl functions; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java index f83efb3a..f21795a3 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java @@ -27,7 +27,7 @@ public class ConfigController { private final ConfigService configService; @PostMapping("server-base") - @Operation(summary = "获取配置") + @Operation(summary = "服务端获取配置接口") public Result getConfig() { Object config = configService.getConfig(true); return new Result().ok(config); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index b9a7918c..34192dd8 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -16,7 +17,10 @@ import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.utils.JsonUtils; import xiaozhi.modules.agent.entity.AgentEntity; +import xiaozhi.modules.agent.entity.AgentPluginMapping; import xiaozhi.modules.agent.entity.AgentTemplateEntity; +import xiaozhi.modules.agent.service.AgentMcpAccessPointService; +import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.config.service.ConfigService; @@ -39,6 +43,8 @@ public class ConfigServiceImpl implements ConfigService { private final AgentTemplateService agentTemplateService; private final RedisUtils redisUtils; private final TimbreService timbreService; + private final AgentPluginMappingService agentPluginMappingService; + private final AgentMcpAccessPointService agentMcpAccessPointService; @Override public Object getConfig(Boolean isCache) { @@ -132,6 +138,25 @@ public class ConfigServiceImpl implements ConfigService { agent.setAsrModelId(null); } + // 添加函数调用参数信息 + if (!Objects.equals(agent.getIntentModelId(), "Intent_nointent")) { + String agentId = agent.getId(); + List pluginMappings = agentPluginMappingService.agentPluginParamsByAgentId(agentId); + if (pluginMappings != null && !pluginMappings.isEmpty()) { + Map pluginParams = new HashMap<>(); + for (AgentPluginMapping pluginMapping : pluginMappings) { + pluginParams.put(pluginMapping.getProviderCode(), pluginMapping.getParamInfo()); + } + result.put("plugins", pluginParams); + } + } + // 获取mcp接入点地址 + String mcpEndpoint = agentMcpAccessPointService.getAgentMcpAccessAddress(agent.getId()); + if (StringUtils.isNotBlank(mcpEndpoint) && mcpEndpoint.startsWith("ws")) { + mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/"); + result.put("mcp_endpoint", mcpEndpoint); + } + // 构建模块配置 buildModuleConfig( agent.getAgentName(), @@ -284,6 +309,7 @@ public class ConfigServiceImpl implements ConfigService { map.put("functions", functions); } } + System.out.println("map: " + map); } if ("Memory".equals(modelTypes[i])) { Map map = (Map) model.getConfigJson(); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java index 5b0bf4bc..b0320e3c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java @@ -4,6 +4,7 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; 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.PostMapping; @@ -14,6 +15,7 @@ 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.ErrorCode; import xiaozhi.common.redis.RedisKeys; @@ -22,6 +24,8 @@ import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.Result; import xiaozhi.modules.device.dto.DeviceRegisterDTO; import xiaozhi.modules.device.dto.DeviceUnBindDTO; +import xiaozhi.modules.device.dto.DeviceUpdateDTO; +import xiaozhi.modules.device.dto.DeviceManualAddDTO; import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.security.user.SecurityUser; @@ -80,16 +84,29 @@ public class DeviceController { return new Result(); } - @PutMapping("/enableOta/{id}/{status}") - @Operation(summary = "启用/关闭OTA自动升级") + @PutMapping("/update/{id}") + @Operation(summary = "更新设备信息") @RequiresPermissions("sys:role:normal") - public Result enableOtaUpgrade(@PathVariable String id, @PathVariable Integer status) { + public Result updateDeviceInfo(@PathVariable String id, @Valid @RequestBody DeviceUpdateDTO deviceUpdateDTO) { DeviceEntity entity = deviceService.selectById(id); if (entity == null) { return new Result().error("设备不存在"); } - entity.setAutoUpdate(status); + UserDetail user = SecurityUser.getUser(); + if (!entity.getUserId().equals(user.getId())) { + return new Result().error("设备不存在"); + } + BeanUtils.copyProperties(deviceUpdateDTO, entity); deviceService.updateById(entity); return new Result(); } + + @PostMapping("/manual-add") + @Operation(summary = "手动添加设备") + @RequiresPermissions("sys:role:normal") + public Result manualAddDevice(@RequestBody @Valid DeviceManualAddDTO dto) { + UserDetail user = SecurityUser.getUser(); + deviceService.manualAddDevice(user.getId(), dto); + return new Result<>(); + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceManualAddDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceManualAddDTO.java new file mode 100644 index 00000000..369726b0 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceManualAddDTO.java @@ -0,0 +1,11 @@ +package xiaozhi.modules.device.dto; + +import lombok.Data; + +@Data +public class DeviceManualAddDTO { + private String agentId; + private String board; // 设备型号 + private String appVersion; // 固件版本 + private String macAddress; // Mac地址 +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceUpdateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceUpdateDTO.java new file mode 100644 index 00000000..2e9a4793 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceUpdateDTO.java @@ -0,0 +1,29 @@ +package xiaozhi.modules.device.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.io.Serializable; + +/** + * 设备更新DTO + */ +@Data +public class DeviceUpdateDTO implements Serializable { + /** + * 自动更新状态 + */ + @Max(1) + @Min(0) + private Integer autoUpdate; + + /** + * 设备别名 + */ + @Size(max = 64) + private String alias; + + private static final long serialVersionUID = 1L; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java index fb91ff89..3392c96f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java @@ -8,6 +8,7 @@ import xiaozhi.common.service.BaseService; import xiaozhi.modules.device.dto.DevicePageUserDTO; import xiaozhi.modules.device.dto.DeviceReportReqDTO; import xiaozhi.modules.device.dto.DeviceReportRespDTO; +import xiaozhi.modules.device.dto.DeviceManualAddDTO; import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.vo.UserShowDeviceListVO; @@ -87,5 +88,14 @@ public interface DeviceService extends BaseService { */ Date getLatestLastConnectionTime(String agentId); + /** + * 手动添加设备 + */ + void manualAddDevice(Long userId, DeviceManualAddDTO dto); + + /** + * 更新设备连接信息 + */ + void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion); } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index ee31c41f..3cff111d 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -44,6 +44,7 @@ import xiaozhi.modules.device.vo.UserShowDeviceListVO; import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.sys.service.SysUserUtilService; +import xiaozhi.modules.device.dto.DeviceManualAddDTO; @Slf4j @Service @@ -410,4 +411,30 @@ public class DeviceServiceImpl extends BaseServiceImpl } return 0; } + + @Override + public void manualAddDevice(Long userId, DeviceManualAddDTO dto) { + // 检查mac是否已存在 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("mac_address", dto.getMacAddress()); + DeviceEntity exist = baseDao.selectOne(wrapper); + if (exist != null) { + throw new RenException("该Mac地址已存在"); + } + Date now = new Date(); + DeviceEntity entity = new DeviceEntity(); + entity.setId(dto.getMacAddress()); + entity.setUserId(userId); + entity.setAgentId(dto.getAgentId()); + entity.setBoard(dto.getBoard()); + entity.setAppVersion(dto.getAppVersion()); + entity.setMacAddress(dto.getMacAddress()); + entity.setCreateDate(now); + entity.setUpdateDate(now); + entity.setLastConnectedAt(now); + entity.setCreator(userId); + entity.setUpdater(userId); + entity.setAutoUpdate(1); + baseDao.insert(entity); + } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java b/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java index fc534a0a..24a7c7ca 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java @@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import xiaozhi.common.page.PageData; import xiaozhi.common.utils.Result; +import xiaozhi.common.utils.ResultUtils; import xiaozhi.common.validator.group.UpdateGroup; import xiaozhi.modules.model.dto.ModelProviderDTO; import xiaozhi.modules.model.service.ModelProviderService; @@ -65,4 +66,10 @@ public class ModelProviderController { return new Result<>(); } + @GetMapping("/plugin/names") + @Tag(name = "获取插件名称列表") + public Result> getPluginNameList() { + return ResultUtils.success(modelProviderService.getPluginList()); + } + } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java index d41766f6..829f86a8 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java @@ -1,5 +1,6 @@ package xiaozhi.modules.model.service; +import java.util.Collection; import java.util.List; import xiaozhi.common.page.PageData; @@ -7,7 +8,11 @@ import xiaozhi.modules.model.dto.ModelProviderDTO; public interface ModelProviderService { - // List getModelNames(String modelType, String modelName); + List getPluginList(); + + ModelProviderDTO getById(String id); + + List getPluginListByIds(Collection ids); List getListByModelType(String modelType); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java index ffeec288..ccab6256 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java @@ -1,5 +1,6 @@ package xiaozhi.modules.model.service.impl; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -8,6 +9,7 @@ import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; @@ -32,6 +34,29 @@ public class ModelProviderServiceImpl extends BaseServiceImpl getPluginList() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ModelProviderEntity::getModelType, "Plugin"); + List providerEntities = modelProviderDao.selectList(queryWrapper); + return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class); + } + + @Override + public ModelProviderDTO getById(String id) { + ModelProviderEntity entity = modelProviderDao.selectById(id); + return ConvertUtils.sourceToTarget(entity, ModelProviderDTO.class); + } + + @Override + public List getPluginListByIds(Collection ids) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(ModelProviderEntity::getId, ids); + queryWrapper.eq(ModelProviderEntity::getModelType, "Plugin"); + List providerEntities = modelProviderDao.selectList(queryWrapper); + return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class); + } + @Override public List getListByModelType(String modelType) { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java index 4860cf1c..edcc9328 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java @@ -104,6 +104,9 @@ public class SysParamsController { // 验证OTA地址 validateOtaUrl(dto.getParamCode(), dto.getParamValue()); + // 验证MCP地址 + validateMcpUrl(dto.getParamCode(), dto.getParamValue()); + sysParamsService.update(dto); configService.getConfig(false); return new Result(); @@ -195,4 +198,33 @@ public class SysParamsController { throw new RenException("OTA接口验证失败:" + e.getMessage()); } } + + private void validateMcpUrl(String paramCode, String url) { + if (!paramCode.equals(Constant.SERVER_MCP_ENDPOINT)) { + return; + } + if (StringUtils.isBlank(url) || url.equals("null")) { + throw new RenException("MCP地址不能为空"); + } + if (url.contains("localhost") || url.contains("127.0.0.1")) { + throw new RenException("MCP地址不能使用localhost或127.0.0.1"); + } + if (!url.toLowerCase().contains("key")) { + throw new RenException("不是正确的MCP地址"); + } + try { + // 发送GET请求 + ResponseEntity response = restTemplate.getForEntity(url, String.class); + if (response.getStatusCode() != HttpStatus.OK) { + throw new RenException("MCP接口访问失败,状态码:" + response.getStatusCode()); + } + // 检查响应内容是否包含OTA相关信息 + String body = response.getBody(); + if (body == null || !body.contains("success")) { + throw new RenException("MCP接口返回内容格式不正确,可能不是一个真实的MCP接口"); + } + } catch (Exception e) { + throw new RenException("MCP接口验证失败:" + e.getMessage()); + } + } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java index 94e2672f..ec3ce2f5 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java @@ -2,36 +2,29 @@ package xiaozhi.modules.sys.enums; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -import xiaozhi.common.exception.RenException; /** * 服务端动作枚举 */ -public enum ServerActionEnum -{ +public enum ServerActionEnum { RESTART("restart"), UPDATE_CONFIG("update_config"); private final String value; - ServerActionEnum(String value) - { + ServerActionEnum(String value) { this.value = value; } @JsonValue - public String getValue() - { + public String getValue() { return value; } @JsonCreator - public static ServerActionEnum fromValue(String value) - { - for (ServerActionEnum action : ServerActionEnum.values()) - { - if (action.value.equalsIgnoreCase(value)) - { + public static ServerActionEnum fromValue(String value) { + for (ServerActionEnum action : ServerActionEnum.values()) { + if (action.value.equalsIgnoreCase(value)) { return action; } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java index 9e4c8307..040b2085 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java @@ -1,16 +1,16 @@ package xiaozhi.modules.sys.enums; +import org.apache.commons.lang3.StringUtils; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -import lombok.Getter; -import org.apache.commons.lang3.StringUtils; /** * 服务端调用响应枚举 */ -public enum ServerActionResponseEnum -{ +public enum ServerActionResponseEnum { SUCCESS("success"), FAIL("fail"); + private final String value; ServerActionResponseEnum(String value) { @@ -18,8 +18,7 @@ public enum ServerActionResponseEnum } @JsonValue - public String getValue() - { + public String getValue() { return value; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketClientManager.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketClientManager.java index 0c49aead..45ac488e 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketClientManager.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketClientManager.java @@ -1,13 +1,5 @@ package xiaozhi.modules.sys.utils; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StopWatch; -import org.springframework.web.socket.*; -import org.springframework.web.socket.client.standard.StandardWebSocketClient; -import org.springframework.web.socket.handler.AbstractWebSocketHandler; -import xiaozhi.common.utils.DateUtils; - import java.io.Closeable; import java.io.IOException; import java.net.URI; @@ -15,30 +7,51 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.concurrent.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; +import org.springframework.util.StopWatch; +import org.springframework.web.socket.BinaryMessage; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHttpHeaders; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.handler.AbstractWebSocketHandler; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; +import xiaozhi.common.utils.DateUtils; + /** * WebSocketClientResource:支持 try-with-resources 模式 */ @Slf4j -public class WebSocketClientManager implements Closeable -{ +public class WebSocketClientManager implements Closeable { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); // 全局回调线程池 - private static final ExecutorService CALLBACK_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() { - private final AtomicInteger cnt = new AtomicInteger(); + private static final ExecutorService CALLBACK_EXECUTOR = Executors + .newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() { + private final AtomicInteger cnt = new AtomicInteger(); - public Thread newThread(Runnable r) - { - Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement()); - t.setDaemon(true); - return t; - } - }); + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement()); + t.setDaemon(true); + return t; + } + }); private volatile WebSocketSession session; private final BlockingQueue textMessageQueue; @@ -51,18 +64,10 @@ public class WebSocketClientManager implements Closeable private volatile Consumer onBinary; private volatile Consumer onError; - private final String uri; - private final WebSocketHttpHeaders headers; - private final long connectTimeout; - private final TimeUnit connectUnit; private final int queueCapacity; // 私有构造,仅由 Builder 调用 private WebSocketClientManager(Builder b) { - this.uri = b.uri; - this.headers = b.headers != null ? b.headers : new WebSocketHttpHeaders(); - this.connectTimeout = b.connectTimeout; - this.connectUnit = b.connectUnit; this.maxSessionDuration = b.maxSessionDuration; this.maxSessionDurationUnit = b.maxSessionDurationUnit; this.queueCapacity = b.queueCapacity; @@ -71,13 +76,14 @@ public class WebSocketClientManager implements Closeable this.errorFuture = new CompletableFuture<>(); } - public static WebSocketClientManager build(Builder b) throws InterruptedException, ExecutionException, TimeoutException, IOException { + public static WebSocketClientManager build(Builder b) + throws InterruptedException, ExecutionException, TimeoutException, IOException { WebSocketClientManager ws = new WebSocketClientManager(b); StandardWebSocketClient client = new StandardWebSocketClient(); - CompletableFuture future = client.execute(ws.new InternalHandler(b.uri), b.headers, URI.create(b.uri)); + CompletableFuture future = client.execute(ws.new InternalHandler(b.uri), b.headers, + URI.create(b.uri)); WebSocketSession sess = future.get(b.connectTimeout, b.connectUnit); - if (sess == null || !sess.isOpen()) - { + if (sess == null || !sess.isOpen()) { throw new IOException("握手失败或会话未打开"); } ws.session = sess; @@ -100,13 +106,10 @@ public class WebSocketClientManager implements Closeable session.sendMessage(new TextMessage(json)); } - - private List listenerCustom( BlockingQueue queue, Predicate predicate) - throws InterruptedException, TimeoutException, ExecutionException - { + throws InterruptedException, TimeoutException, ExecutionException { List collected = new ArrayList<>(); long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration); @@ -136,17 +139,16 @@ public class WebSocketClientManager implements Closeable /** * 同步接收多条消息,直到 predicate 为 true 或超时抛异常; + * * @return 返回监听期间的所有消息列表 */ public List listener(Predicate predicate) - throws InterruptedException, TimeoutException, ExecutionException - { + throws InterruptedException, TimeoutException, ExecutionException { return listenerCustom(textMessageQueue, predicate); } public List listenerBinary(Predicate predicate) - throws InterruptedException, TimeoutException, ExecutionException - { + throws InterruptedException, TimeoutException, ExecutionException { return listenerCustom(binaryMessageQueue, predicate); } @@ -183,8 +185,8 @@ public class WebSocketClientManager implements Closeable if (session != null && session.isOpen()) { session.close(CloseStatus.NORMAL); } + } catch (IOException ignored) { } - catch (IOException ignored) {} textMessageQueue.clear(); binaryMessageQueue.clear(); errorFuture.completeExceptionally(new IOException("WebSocket 已关闭")); @@ -207,7 +209,8 @@ public class WebSocketClientManager implements Closeable // 保存会话 WebSocketClientManager.this.session = session; this.stopWatch.start(); - log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN)); + log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri, + DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN)); } /** @@ -264,18 +267,19 @@ public class WebSocketClientManager implements Closeable stopWatch.stop(); } log.info("ws连接关闭, 目标URI: {}, 关闭时间: {}, 连接总时长: {}s", - targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN), DateUtils.millsToSecond(stopWatch.getTotalTimeMillis())); + targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN), + DateUtils.millsToSecond(stopWatch.getTotalTimeMillis())); } } public static class Builder { - private String uri; // 目标 WS URI - private long connectTimeout = 3; // 请求连接等待时间 - private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位 - private long maxSessionDuration = 5; // 最大连线时间,默认5秒 + private String uri; // 目标 WS URI + private long connectTimeout = 3; // 请求连接等待时间 + private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位 + private long maxSessionDuration = 5; // 最大连线时间,默认5秒 private TimeUnit maxSessionDurationUnit = TimeUnit.SECONDS; // 最大连线时间单位 - private int queueCapacity = 100; // 消息队列容量 - private WebSocketHttpHeaders headers; // 请求头 + private int queueCapacity = 100; // 消息队列容量 + private WebSocketHttpHeaders headers; // 请求头 /** * 目标 WS URI @@ -307,7 +311,8 @@ public class WebSocketClientManager implements Closeable return this; } - public WebSocketClientManager build() throws InterruptedException, ExecutionException, TimeoutException, IOException { + public WebSocketClientManager build() + throws InterruptedException, ExecutionException, TimeoutException, IOException { return WebSocketClientManager.build(this); } diff --git a/main/manager-api/src/main/resources/db/changelog/202505091409.sql b/main/manager-api/src/main/resources/db/changelog/202505091409.sql deleted file mode 100644 index 76ea8ae0..00000000 --- a/main/manager-api/src/main/resources/db/changelog/202505091409.sql +++ /dev/null @@ -1,6 +0,0 @@ --- 更新intent_llmM供应器 -update `ai_model_provider` set fields = '[{"key":"llm","label":"LLM模型","type":"string"},{"key":"functions","label":"函数列表","type":"dict","dict_name":"functions"}]' where id = 'SYSTEM_Intent_intent_llm'; --- 更新ChatGLMLLM的意图识别配置 -update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\", \"functions\": \"get_weather;get_news_from_newsnow;play_music\"}' where id = 'Intent_intent_llm'; --- 更新函数调用意图识别配置 -UPDATE `ai_model_config` SET config_json = REPLACE(config_json, ';get_news;', ';get_news_from_newsnow;') WHERE id = 'Intent_function_call'; diff --git a/main/manager-api/src/main/resources/db/changelog/202505292203.sql b/main/manager-api/src/main/resources/db/changelog/202505292203.sql new file mode 100644 index 00000000..4102c7fa --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202505292203.sql @@ -0,0 +1,189 @@ +-- =============================== +-- 一、在ai_model_provider中插入plugin 记录 +-- =============================== +START TRANSACTION; + + +-- intent_llm和function_call不设置函数列表 +update `ai_model_provider` set fields = '[{"key":"llm","label":"LLM模型","type":"string"}]' where id = 'SYSTEM_Intent_intent_llm'; +update `ai_model_provider` set fields = '[]' where id = 'SYSTEM_Intent_function_call'; +update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Intent_intent_llm'; +UPDATE `ai_model_config` SET config_json = '{\"type\": \"function_call\"}' WHERE id = 'Intent_function_call'; + + +delete from ai_model_provider where model_type = 'Plugin'; +-- 1. 天气查询 +INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields, + sort, creator, create_date, updater, update_date) +VALUES ('SYSTEM_PLUGIN_WEATHER', + 'Plugin', + 'get_weather', + '天气查询', + JSON_ARRAY( + JSON_OBJECT( + 'key', 'api_key', + 'type', 'string', + 'label', '天气插件 API 密钥', + 'default', (SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_weather.api_key') + ), + JSON_OBJECT( + 'key', 'default_location', + 'type', 'string', + 'label', '默认查询城市', + 'default', + (SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_weather.default_location') + ), + JSON_OBJECT( + 'key', 'api_host', + 'type', 'string', + 'label', '开发者 API Host', + 'default', + (SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_weather.api_host') + ) + ), + 10, 0, NOW(), 0, NOW()); + +-- 6. 本地播放音乐 +INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields, + sort, creator, create_date, updater, update_date) +VALUES ('SYSTEM_PLUGIN_MUSIC', + 'Plugin', + 'play_music', + '服务器音乐播放', + JSON_ARRAY(), + 20, 0, NOW(), 0, NOW()); + +-- 2. 新闻订阅 +INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields, + sort, creator, create_date, updater, update_date) +VALUES ('SYSTEM_PLUGIN_NEWS_CHINANEWS', + 'Plugin', + 'get_news_from_chinanews', + '中新网新闻', + JSON_ARRAY( + JSON_OBJECT( + 'key', 'default_rss_url', + 'type', 'string', + 'label', '默认 RSS 源', + 'default', + (SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_news.default_rss_url') + ), + JSON_OBJECT( + 'key', 'society_rss_url', + 'type', 'string', + 'label', '社会新闻 RSS 地址', + 'default', + 'https://www.chinanews.com.cn/rss/society.xml' + ), + JSON_OBJECT( + 'key', 'world_rss_url', + 'type', 'string', + 'label', '国际新闻 RSS 地址', + 'default', + 'https://www.chinanews.com.cn/rss/world.xml' + ), + JSON_OBJECT( + 'key', 'finance_rss_url', + 'type', 'string', + 'label', '财经新闻 RSS 地址', + 'default', + 'https://www.chinanews.com.cn/rss/finance.xml' + ) + ), + 30, 0, NOW(), 0, NOW()); + +-- 3. 新闻订阅 +INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields, + sort, creator, create_date, updater, update_date) +VALUES ('SYSTEM_PLUGIN_NEWS_NEWSNOW', + 'Plugin', + 'get_news_from_newsnow', + 'newsnow新闻聚合', + JSON_ARRAY( + JSON_OBJECT( + 'key', 'url', + 'type', 'string', + 'label', '接口地址', + 'default', + 'https://newsnow.busiyi.world/api/s?id=' + ) + ), + 40, 0, NOW(), 0, NOW()); + + +-- 4. HomeAssistant 状态查询 +INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields, + sort, creator, create_date, updater, update_date) +VALUES ('SYSTEM_PLUGIN_HA_GET_STATE', + 'Plugin', + 'hass_get_state', + 'HomeAssistant设备状态查询', + JSON_ARRAY( + JSON_OBJECT( + 'key', 'base_url', + 'type', 'string', + 'label', 'HA 服务器地址', + 'default', + (SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.base_url') + ), + JSON_OBJECT( + 'key', 'api_key', + 'type', 'string', + 'label', 'HA API 访问令牌', + 'default', + (SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.api_key') + ), + JSON_OBJECT( + 'key', 'devices', + 'type', 'array', + 'label', '设备列表(名称,实体ID;…)', + 'default', + (SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.devices') + ) + ), + 50, 0, NOW(), 0, NOW()); + +-- 5. HomeAssistant 状态写入 +INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields, + sort, creator, create_date, updater, update_date) +VALUES ('SYSTEM_PLUGIN_HA_SET_STATE', + 'Plugin', + 'hass_set_state', + 'HomeAssistant设备状态修改', + JSON_ARRAY(), + 60, 0, NOW(), 0, NOW()); + +-- 5. HomeAssistant 音乐播放 +INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields, + sort, creator, create_date, updater, update_date) +VALUES ('SYSTEM_PLUGIN_HA_PLAY_MUSIC', + 'Plugin', + 'hass_play_music', + 'HomeAssistant音乐播放', + JSON_ARRAY(), + 70, 0, NOW(), 0, NOW()); + + +-- =============================== +-- 二、删除sys_params中旧的plugins.*参数 +-- =============================== +DELETE +FROM sys_params +WHERE param_code LIKE 'plugins.%'; + + +-- =============================== +-- 三、添加智能体插件id字段 +-- =============================== +CREATE TABLE IF NOT EXISTS ai_agent_plugin_mapping +( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', + agent_id VARCHAR(32) NOT NULL COMMENT '智能体ID', + plugin_id VARCHAR(32) NOT NULL COMMENT '插件ID', + param_info JSON NOT NULL COMMENT '参数信息', + UNIQUE KEY uk_agent_provider (agent_id, plugin_id) +) COMMENT 'Agent与插件的唯一映射表'; + + +COMMIT; + diff --git a/main/manager-api/src/main/resources/db/changelog/202506161101.sql b/main/manager-api/src/main/resources/db/changelog/202506161101.sql new file mode 100644 index 00000000..fcfff082 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202506161101.sql @@ -0,0 +1 @@ +ALTER TABLE ai_agent_plugin_mapping CONVERT TO CHARACTER SET utf8mb4; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/202506191643.sql b/main/manager-api/src/main/resources/db/changelog/202506191643.sql new file mode 100644 index 00000000..20559061 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202506191643.sql @@ -0,0 +1,19 @@ +-- LLM意图识别配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = 'LLM意图识别配置说明: +1. 使用独立的LLM进行意图识别 +2. 默认使用selected_module.LLM的模型 +3. 可以配置使用独立的LLM(如免费的ChatGLMLLM) +4. 通用性强,但会增加处理时间 +配置说明: +1. 在llm字段中指定使用的LLM模型 +2. 如果不指定,则使用selected_module.LLM的模型' WHERE `id` = 'Intent_intent_llm'; + +-- 函数调用意图识别配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '函数调用意图识别配置说明: +1. 使用LLM的function_call功能进行意图识别 +2. 需要所选择的LLM支持function_call +3. 按需调用工具,处理速度快' WHERE `id` = 'Intent_function_call'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/202506251619.sql b/main/manager-api/src/main/resources/db/changelog/202506251619.sql new file mode 100644 index 00000000..a458daea --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202506251619.sql @@ -0,0 +1,21 @@ +-- 更新 get_news_from_newsnow 插件配置,添加新闻源配置字段 +-- 执行时间:2025-06-25 + +-- 更新现有的 get_news_from_newsnow 插件配置 +UPDATE ai_model_provider +SET fields = JSON_ARRAY( + JSON_OBJECT( + 'key', 'url', + 'type', 'string', + 'label', '接口地址', + 'default', 'https://newsnow.busiyi.world/api/s?id=' + ), + JSON_OBJECT( + 'key', 'news_sources', + 'type', 'json', + 'label', '新闻源配置', + 'default', '{"thepaper":"澎湃新闻","baidu":"百度热搜","cls-depth":"财联社"}' + ) +) +WHERE provider_code = 'get_news_from_newsnow' +AND model_type = 'Plugin'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/202506261637.sql b/main/manager-api/src/main/resources/db/changelog/202506261637.sql new file mode 100644 index 00000000..792e00c2 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202506261637.sql @@ -0,0 +1,2 @@ +delete from `sys_params` where id = 113; +INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (113, 'server.mcp_endpoint', 'null', 'string', 1, 'mcp接入点地址'); \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index f33d28a2..63a08e11 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -107,13 +107,6 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202505081146.sql - - changeSet: - id: 202505091409 - author: hrz - changes: - - sqlFile: - encoding: utf8 - path: classpath:db/changelog/202505091409.sql - changeSet: id: 202505091555 author: whosmyqueen @@ -170,6 +163,13 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202505271414.sql + - changeSet: + id: 202505292203 + author: CAIXYPROMISE + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202505292203.sql - changeSet: id: 202506010920 author: hrz @@ -204,4 +204,32 @@ databaseChangeLog: changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202506080955.sql \ No newline at end of file + path: classpath:db/changelog/202506080955.sql + - changeSet: + id: 202506161101 + author: hrz + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202506161101.sql + - changeSet: + id: 202506191643 + author: hrz + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202506191643.sql + - changeSet: + id: 202506251619 + author: Tink + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202506251619.sql + - changeSet: + id: 202506261637 + author: hrz + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202506261637.sql diff --git a/main/manager-api/src/main/resources/lua/emptyAll.lua b/main/manager-api/src/main/resources/lua/emptyAll.lua index 1d984cb7..2626a894 100644 --- a/main/manager-api/src/main/resources/lua/emptyAll.lua +++ b/main/manager-api/src/main/resources/lua/emptyAll.lua @@ -1 +1 @@ -redis.call('FLUSHALL') \ No newline at end of file +redis.call('FLUSHDB') \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml b/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml index 322ca172..439be096 100644 --- a/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml +++ b/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml @@ -5,4 +5,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentPluginMappingMapper.xml b/main/manager-api/src/main/resources/mapper/agent/AgentPluginMappingMapper.xml new file mode 100644 index 00000000..659fcd6e --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/agent/AgentPluginMappingMapper.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + id,agent_id,plugin_id + + + + + + + diff --git a/main/manager-api/src/test/java/xiaozhi/common/utils/AESUtilsTest.java b/main/manager-api/src/test/java/xiaozhi/common/utils/AESUtilsTest.java new file mode 100644 index 00000000..c4f37115 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/common/utils/AESUtilsTest.java @@ -0,0 +1,85 @@ +package xiaozhi.common.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class AESUtilsTest { + + @Test + public void testEncryptAndDecrypt() { + String key = "xiaozhi1234567890"; + String plainText = "Hello, 小智!"; + + System.out.println("原始文本: " + plainText); + System.out.println("密钥: " + key); + + // 加密 + String encrypted = AESUtils.encrypt(key, plainText); + System.out.println("加密结果: " + encrypted); + + // 解密 + String decrypted = AESUtils.decrypt(key, encrypted); + System.out.println("解密结果: " + decrypted); + + // 验证 + assertEquals(plainText, decrypted, "加解密结果应该一致"); + System.out.println("加解密一致性: " + plainText.equals(decrypted)); + } + + @Test + public void testDifferentKeyLengths() { + String[] keys = { + "1234567890123456", // 16位 + "123456789012345678901234", // 24位 + "12345678901234567890123456789012", // 32位 + "short", // 短密钥 + "verylongkeythatwillbetruncatedto32bytes" // 长密钥 + }; + + String plainText = "测试文本"; + + for (String key : keys) { + String encrypted = AESUtils.encrypt(key, plainText); + String decrypted = AESUtils.decrypt(key, encrypted); + assertEquals(plainText, decrypted, "密钥长度: " + key.length()); + } + } + + @Test + public void testSpecialCharacters() { + String key = "xiaozhi1234567890"; + String[] testTexts = { + "Hello World", + "你好世界", + "Hello, 小智!", + "特殊字符: !@#$%^&*()", + "数字123和中文混合", + "Emoji: 😀🎉🚀", + "空字符串测试", + "" + }; + + for (String text : testTexts) { + String encrypted = AESUtils.encrypt(key, text); + String decrypted = AESUtils.decrypt(key, encrypted); + assertEquals(text, decrypted, "测试文本: " + text); + } + } + + @Test + public void testCrossLanguageCompatibility() { + // 这些是Python版本生成的加密结果,用于测试跨语言兼容性 + String key = "xiaozhi1234567890"; + String plainText = "Hello, 小智!"; + + // Python版本生成的加密结果(需要运行Python测试后获取) + // String pythonEncrypted = "从Python测试中获取的加密结果"; + // String decrypted = AESUtils.decrypt(key, pythonEncrypted); + // assertEquals(plainText, decrypted, "Java应该能解密Python加密的结果"); + + // 生成Java加密结果供Python测试 + String javaEncrypted = AESUtils.encrypt(key, plainText); + System.out.println("Java加密结果供Python测试: " + javaEncrypted); + } +} \ No newline at end of file diff --git a/main/manager-web/src/apis/api.js b/main/manager-web/src/apis/api.js index 0a3e8d18..4d1d7d7d 100755 --- a/main/manager-web/src/apis/api.js +++ b/main/manager-web/src/apis/api.js @@ -7,6 +7,7 @@ import model from './module/model.js' import ota from './module/ota.js' import timbre from "./module/timbre.js" import user from './module/user.js' + /** * 接口地址 * 开发时自动读取使用.env.development文件 @@ -22,7 +23,6 @@ export function getServiceUrl() { return DEV_API_SERVICE } - /** request服务封装 */ export default { getServiceUrl, diff --git a/main/manager-web/src/apis/module/agent.js b/main/manager-web/src/apis/module/agent.js index 7f33d320..f6c37d7e 100644 --- a/main/manager-web/src/apis/module/agent.js +++ b/main/manager-web/src/apis/module/agent.js @@ -143,4 +143,34 @@ export default { }); }).send(); }, + // 获取智能体的MCP接入点地址 + getAgentMcpAccessAddress(agentId, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/mcp/address/${agentId}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.getAgentMcpAccessAddress(agentId, callback); + }); + }).send(); + }, + // 获取智能体的MCP工具列表 + getAgentMcpToolsList(agentId, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/mcp/tools/${agentId}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.getAgentMcpToolsList(agentId, callback); + }); + }).send(); + }, } diff --git a/main/manager-web/src/apis/module/device.js b/main/manager-web/src/apis/module/device.js index 2f20425d..233c764b 100644 --- a/main/manager-web/src/apis/module/device.js +++ b/main/manager-web/src/apis/module/device.js @@ -51,10 +51,11 @@ export default { }); }).send(); }, - enableOtaUpgrade(id, status, callback) { + updateDeviceInfo(id, payload, callback) { RequestService.sendRequest() - .url(`${getServiceUrl()}/device/enableOta/${id}/${status}`) + .url(`${getServiceUrl()}/device/update/${id}`) .method('PUT') + .data(payload) .success((res) => { RequestService.clearRequestTime() callback(res) @@ -63,8 +64,25 @@ export default { console.error('更新OTA状态失败:', err) this.$message.error(err.msg || '更新OTA状态失败') RequestService.reAjaxFun(() => { - this.enableOtaUpgrade(id, status, callback) + this.updateDeviceInfo(id, payload, callback) }) }).send() }, + // 手动添加设备 + manualAddDevice(params, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/device/manual-add`) + .method('POST') + .data(params) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail((err) => { + console.error('手动添加设备失败:', err); + RequestService.reAjaxFun(() => { + this.manualAddDevice(params, callback); + }); + }).send(); + }, } \ No newline at end of file diff --git a/main/manager-web/src/apis/module/model.js b/main/manager-web/src/apis/module/model.js index badf4174..7db2b288 100644 --- a/main/manager-web/src/apis/module/model.js +++ b/main/manager-web/src/apis/module/model.js @@ -305,4 +305,20 @@ export default { }) }).send() }, + // 获取插件列表 + getPluginFunctionList(params, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/models/provider/plugin/names`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .networkFail((err) => { + this.$message.error(err.msg || '获取插件列表失败') + RequestService.reAjaxFun(() => { + this.getPluginFunctionList(params, callback) + }) + }).send() + } } diff --git a/main/manager-web/src/components/FirmwareDialog.vue b/main/manager-web/src/components/FirmwareDialog.vue index 56e2f327..e4eb2bb7 100644 --- a/main/manager-web/src/components/FirmwareDialog.vue +++ b/main/manager-web/src/components/FirmwareDialog.vue @@ -1,5 +1,5 @@