mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
update: 单模块部署也能使用mcp接入点 (#1763)
* 优化:OTA接口需要适应最小示例参数 #1757 * update:修复智能体选择的模型不存在导致的500错误 #1752 * update: 单模块部署也能使用mcp接入点
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# MCP 接入点部署使用指南
|
||||
|
||||
本教程包含2个部分
|
||||
- 1、如何部署MCP接入点这个服务
|
||||
- 1、全模块部署时,怎么配置MCP接入点
|
||||
- 2、单模块部署时,怎么配置MCP接入点
|
||||
|
||||
# 1、如何部署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
|
||||
```
|
||||
|
||||
此时,日志里会输出类似以下的日志
|
||||
```
|
||||
250705 INFO-=====下面的地址分别是智控台/单模块MCP接入点地址====
|
||||
250705 INFO-智控台MCP参数配置: http://172.22.0.2:8004/mcp_endpoint/health?key=abc
|
||||
250705 INFO-单模块部署MCP接入点: ws://172.22.0.2:8004/mcp_endpoint/mcp/?token=def
|
||||
250705 INFO-=====请根据具体部署选择使用,请勿泄露给任何人======
|
||||
```
|
||||
|
||||
请你把两个接口地址复制出来:
|
||||
|
||||
由于你是docker部署,切不可直接使用上面的地址!
|
||||
|
||||
由于你是docker部署,切不可直接使用上面的地址!
|
||||
|
||||
由于你是docker部署,切不可直接使用上面的地址!
|
||||
|
||||
你先把地址复制出来,放在一个草稿里,你要知道你的电脑的局域网ip是什么,例如我的电脑局域网ip是`192.168.1.25`,那么
|
||||
原来我的接口地址
|
||||
```
|
||||
智控台MCP参数配置: http://172.22.0.2:8004/mcp_endpoint/health?key=abc
|
||||
单模块部署MCP接入点: ws://172.22.0.2:8004/mcp_endpoint/mcp/?token=def
|
||||
```
|
||||
就要改成
|
||||
```
|
||||
智控台MCP参数配置: http://192.168.1.25:8004/mcp_endpoint/health?key=abc
|
||||
单模块部署MCP接入点: ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=def
|
||||
```
|
||||
|
||||
改好后,请使用浏览器直接访问`智控台MCP参数配置`。当浏览器出现类似这样的代码,说明是成功了。
|
||||
```
|
||||
{"result":{"status":"success","connections":{"tool_connections":0,"robot_connections":0,"total_connections":0}},"error":null,"id":null,"jsonrpc":"2.0"}
|
||||
```
|
||||
|
||||
请你保留好上面两个`接口地址`,下一步要用到。
|
||||
|
||||
# 2、全模块部署时,怎么配置MCP接入点
|
||||
|
||||
如果你是全模块部署,使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。
|
||||
|
||||
然后搜索参数`server.mcp_endpoint`,此时,它的值应该是`null`值。
|
||||
点击修改按钮,把上一步得来的`智控台MCP参数配置`粘贴到`参数值`里。然后保存。
|
||||
|
||||
如果能保存成功,说明一切顺利,你可以去智能体查看效果了。如果不成功,说明智控台无法访问mcp接入点,很大概率是网络防火墙,或者没有填写正确的局域网ip。
|
||||
|
||||
# 3、单模块部署时,怎么配置MCP接入点
|
||||
|
||||
如果你是单模块部署,找到你的配置文件`data/.config.yaml`。
|
||||
在配置文件搜索`mcp_endpoint`,如果没有找到,你就增加`mcp_endpoint`配置。类似我是就是这样
|
||||
```
|
||||
server:
|
||||
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
|
||||
http_port: 8002
|
||||
log:
|
||||
log_level: INFO
|
||||
|
||||
# 此处可能还更多配置..
|
||||
|
||||
mcp_endpoint: 你的接入点websocket地址
|
||||
```
|
||||
这时,请你把`如何部署MCP接入点这个服务`中得到的`单模块部署MCP接入点` 粘贴到 `mcp_endpoint`中。类似这样
|
||||
|
||||
```
|
||||
server:
|
||||
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
|
||||
http_port: 8002
|
||||
log:
|
||||
log_level: INFO
|
||||
|
||||
# 此处可能还更多配置
|
||||
|
||||
mcp_endpoint: ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=def
|
||||
```
|
||||
|
||||
配置好后,启动单模块会输出如下的日志。
|
||||
```
|
||||
250705[__main__]-INFO-初始化组件: vad成功 SileroVAD
|
||||
250705[__main__]-INFO-初始化组件: asr成功 FunASRServer
|
||||
250705[__main__]-INFO-OTA接口是 http://192.168.1.25:8002/xiaozhi/ota/
|
||||
250705[__main__]-INFO-视觉分析接口是 http://192.168.1.25:8002/mcp/vision/explain
|
||||
250705[__main__]-INFO-mcp接入点是 ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
|
||||
250705[__main__]-INFO-Websocket地址是 ws://192.168.1.25:8000/xiaozhi/v1/
|
||||
250705[__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
250705[__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
250705[__main__]-INFO-=============================================================
|
||||
```
|
||||
|
||||
如上,如果能输出类似的`mcp接入点是`中`ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc`说明配置成功了。
|
||||
|
||||
@@ -1,92 +1,36 @@
|
||||
# MCP 接入点部署使用指南
|
||||
# MCP 接入点使用指南
|
||||
|
||||
本教程包含2个部分
|
||||
- 1、如何开启mcp接入点
|
||||
- 2、如何为智能体接入一个简单的mcp功能,如计算器功能
|
||||
本教程以虾哥开源的mcp计算器功能为示例,介绍如何将自己自定义的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。
|
||||
本教程的前提是,你的`xiaozhi-server`已经启用了mcp接入点功能,如果你还没启用,可以先根据[这个教程](./mcp-endpoint-enable.md)启用。
|
||||
|
||||
# 如何为智能体接入一个简单的mcp功能,如计算器功能
|
||||
|
||||
如果以上步骤顺利,你可以进入智能体管理,点击`配置角色`,在`意图识别`的右边,有一个`编辑功能`的按钮。
|
||||
### 如果你是全模块部署
|
||||
如果你是全模块部署,你可以进入智控台,智能体管理,点击`配置角色`,在`意图识别`的右边,有一个`编辑功能`的按钮。
|
||||
|
||||
点击这个按钮。在弹出的页面里,位于底部,会有`MCP接入点`,正常来说,会显示这个智能体的`MCP接入点地址`,接下来,我们来给这个智能体扩展一个基于MCP技术的计算器的功能。
|
||||
|
||||
这个`MCP接入点地址`很重要,你等一下会用到。
|
||||
|
||||
### 如果你是单模块部署
|
||||
如果你是单模块部署,且你已经在配置文件里配置了MCP接入点地址,那么正常来说,单模块部署启动的时候,会输出如下的日志。
|
||||
```
|
||||
250705[__main__]-INFO-初始化组件: vad成功 SileroVAD
|
||||
250705[__main__]-INFO-初始化组件: asr成功 FunASRServer
|
||||
250705[__main__]-INFO-OTA接口是 http://192.168.1.25:8002/xiaozhi/ota/
|
||||
250705[__main__]-INFO-视觉分析接口是 http://192.168.1.25:8002/mcp/vision/explain
|
||||
250705[__main__]-INFO-mcp接入点是 ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
|
||||
250705[__main__]-INFO-Websocket地址是 ws://192.168.1.25:8000/xiaozhi/v1/
|
||||
250705[__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
250705[__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
250705[__main__]-INFO-=============================================================
|
||||
```
|
||||
|
||||
如上,输出`mcp接入点是`中`ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc`就是你的`MCP接入点地址`。
|
||||
|
||||
这个`MCP接入点地址`很重要,你等一下会用到。
|
||||
|
||||
## 第一步 下载虾哥MCP计算器项目代码
|
||||
|
||||
浏览器打开虾哥写的[计算器项目](https://github.com/78/mcp-calculator),
|
||||
@@ -114,13 +58,13 @@ pip install -r requirements.txt
|
||||
|
||||
例如我的智能体的mcp地址是
|
||||
```
|
||||
ws://192.168.4.7:8004/mcp_endpoint/mcp/?token=abc
|
||||
ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
|
||||
```
|
||||
|
||||
开始输入命令
|
||||
|
||||
```bash
|
||||
export MCP_ENDPOINT=ws://192.168.4.7:8004/mcp_endpoint/mcp/?token=abc
|
||||
export MCP_ENDPOINT=ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
|
||||
```
|
||||
|
||||
输入完后,启动程序
|
||||
@@ -129,6 +73,22 @@ export MCP_ENDPOINT=ws://192.168.4.7:8004/mcp_endpoint/mcp/?token=abc
|
||||
python mcp_pipe.py calculator.py
|
||||
```
|
||||
|
||||
### 如果你是智控台部署
|
||||
如果你是智控台部署,启动完后,你再进入智控台,点击刷新MCP的接入状态,就会看到你扩展的功能列表了。
|
||||
|
||||
启动完后,你再进入智控台,点击刷新MCP的接入状态,就会看到你扩展的功能列表了。
|
||||
### 如果你是单模块部署
|
||||
如果你是单模块部署,当设备连接后,会输出类似的日志,说明成功了
|
||||
|
||||
```
|
||||
250705 -INFO-正在初始化MCP接入点: wss://2662r3426b.vicp.fun/mcp_e
|
||||
250705 -INFO-发送MCP接入点初始化消息
|
||||
250705 -INFO-MCP接入点连接成功
|
||||
250705 -INFO-MCP接入点初始化成功
|
||||
250705 -INFO-统一工具处理器初始化完成
|
||||
250705 -INFO-MCP接入点服务器信息: name=Calculator, version=1.9.4
|
||||
250705 -INFO-MCP接入点支持的工具数量: 1
|
||||
250705 -INFO-所有MCP接入点工具已获取,客户端准备就绪
|
||||
250705 -INFO-工具缓存已刷新
|
||||
250705 -INFO-当前支持的函数列表: [ 'get_time', 'get_lunar', 'play_music', 'get_weather', 'handle_exit_intent', 'calculator']
|
||||
```
|
||||
如果包含了 `'calculator'`,说明设备将可以根据意图识别,调用计算器这个工具。
|
||||
+21
-15
@@ -258,17 +258,17 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
/**
|
||||
* 构建模块配置
|
||||
*
|
||||
* @param prompt 提示词
|
||||
* @param voice 音色
|
||||
* @param referenceAudio 参考音频路径
|
||||
* @param referenceText 参考文本
|
||||
* @param vadModelId VAD模型ID
|
||||
* @param asrModelId ASR模型ID
|
||||
* @param llmModelId LLM模型ID
|
||||
* @param ttsModelId TTS模型ID
|
||||
* @param memModelId 记忆模型ID
|
||||
* @param intentModelId 意图模型ID
|
||||
* @param result 结果Map
|
||||
* @param prompt 提示词
|
||||
* @param voice 音色
|
||||
* @param referenceAudio 参考音频路径
|
||||
* @param referenceText 参考文本
|
||||
* @param vadModelId VAD模型ID
|
||||
* @param asrModelId ASR模型ID
|
||||
* @param llmModelId LLM模型ID
|
||||
* @param ttsModelId TTS模型ID
|
||||
* @param memModelId 记忆模型ID
|
||||
* @param intentModelId 意图模型ID
|
||||
* @param result 结果Map
|
||||
*/
|
||||
private void buildModuleConfig(
|
||||
String assistantName,
|
||||
@@ -298,14 +298,20 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
continue;
|
||||
}
|
||||
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache);
|
||||
if (model == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> typeConfig = new HashMap<>();
|
||||
if (model.getConfigJson() != null) {
|
||||
typeConfig.put(model.getId(), model.getConfigJson());
|
||||
// 如果是TTS类型,添加private_voice属性
|
||||
if ("TTS".equals(modelTypes[i])){
|
||||
if (voice != null) ((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
|
||||
if (referenceAudio != null) ((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
|
||||
if (referenceText != null) ((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
|
||||
if ("TTS".equals(modelTypes[i])) {
|
||||
if (voice != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
|
||||
if (referenceAudio != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
|
||||
if (referenceText != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
|
||||
}
|
||||
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型
|
||||
if ("Intent".equals(modelTypes[i])) {
|
||||
|
||||
@@ -51,13 +51,12 @@ public class OTAController {
|
||||
if (StringUtils.isBlank(clientId)) {
|
||||
clientId = deviceId;
|
||||
}
|
||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||
boolean macAddressValid = isMacAddressValid(macAddress);
|
||||
boolean macAddressValid = isMacAddressValid(deviceId);
|
||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||
if (!macAddressValid) {
|
||||
return createResponse(DeviceReportRespDTO.createError("Invalid device ID"));
|
||||
}
|
||||
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
|
||||
return createResponse(deviceService.checkDeviceActive(deviceId, clientId, deviceReportReqDTO));
|
||||
}
|
||||
|
||||
@Operation(summary = "设备快速检查激活状态")
|
||||
|
||||
+1
-1
@@ -218,7 +218,7 @@ public class SysParamsController {
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
throw new RenException("MCP接口访问失败,状态码:" + response.getStatusCode());
|
||||
}
|
||||
// 检查响应内容是否包含OTA相关信息
|
||||
// 检查响应内容是否包含mcp相关信息
|
||||
String body = response.getBody();
|
||||
if (body == null || !body.contains("success")) {
|
||||
throw new RenException("MCP接口返回内容格式不正确,可能不是一个真实的MCP接口");
|
||||
|
||||
@@ -110,8 +110,10 @@
|
||||
<div class="url-header">
|
||||
<div class="address-desc">
|
||||
<span>以下是智能体的MCP接入点地址。</span>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-enable.md"
|
||||
target="_blank" class="doc-link">如何部署MCP接入点</a> |
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md"
|
||||
target="_blank" class="doc-link">查看接入点使用文档</a>
|
||||
target="_blank" class="doc-link">如何接入MCP功能</a>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-model="mcpUrl" readonly class="url-input">
|
||||
|
||||
@@ -5,7 +5,7 @@ import asyncio
|
||||
from aioconsole import ainput
|
||||
from config.settings import load_config
|
||||
from config.logger import setup_logging
|
||||
from core.utils.util import get_local_ip
|
||||
from core.utils.util import get_local_ip, validate_mcp_endpoint
|
||||
from core.http_server import SimpleHttpServer
|
||||
from core.websocket_server import WebSocketServer
|
||||
from core.utils.util import check_ffmpeg_installed
|
||||
@@ -77,6 +77,17 @@ async def main():
|
||||
get_local_ip(),
|
||||
port,
|
||||
)
|
||||
mcp_endpoint = config.get("mcp_endpoint", None)
|
||||
if mcp_endpoint is not None and "你" not in mcp_endpoint:
|
||||
# 校验MCP接入点格式
|
||||
if validate_mcp_endpoint(mcp_endpoint):
|
||||
logger.bind(tag=TAG).info("mcp接入点是\t{}", mcp_endpoint)
|
||||
# 将mcp计入点地址转成调用点
|
||||
mcp_endpoint = mcp_endpoint.replace("/mcp/", "/call/")
|
||||
config["mcp_endpoint"] = mcp_endpoint
|
||||
else:
|
||||
logger.bind(tag=TAG).error("mcp接入点不符合规范")
|
||||
config["mcp_endpoint"] = "你的接入点 websocket地址"
|
||||
|
||||
# 获取WebSocket配置,使用安全的默认值
|
||||
websocket_port = 8000
|
||||
|
||||
@@ -104,7 +104,8 @@ wakeup_words:
|
||||
- "喵喵同学"
|
||||
- "小滨小滨"
|
||||
- "小冰小冰"
|
||||
# MCP接入点地址
|
||||
# MCP接入点地址,地址格式为:ws://你的mcp接入点ip或者域名:端口号/mcp/?token=你的token
|
||||
# 详细教程 https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md
|
||||
mcp_endpoint: 你的接入点 websocket地址
|
||||
# 插件的基础配置
|
||||
plugins:
|
||||
|
||||
@@ -214,7 +214,17 @@ class ConnectionHandler:
|
||||
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
||||
return
|
||||
finally:
|
||||
await self._save_and_close(ws)
|
||||
try:
|
||||
await self._save_and_close(ws)
|
||||
except Exception as final_error:
|
||||
self.logger.bind(tag=TAG).error(f"最终清理时出错: {final_error}")
|
||||
# 确保即使保存记忆失败,也要关闭连接
|
||||
try:
|
||||
await self.close(ws)
|
||||
except Exception as close_error:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"强制关闭连接时出错: {close_error}"
|
||||
)
|
||||
|
||||
async def _save_and_close(self, ws):
|
||||
"""保存记忆并关闭连接"""
|
||||
@@ -232,7 +242,10 @@ class ConnectionHandler:
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||
finally:
|
||||
loop.close()
|
||||
try:
|
||||
loop.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 启动线程保存记忆,不等待完成
|
||||
threading.Thread(target=save_memory_task, daemon=True).start()
|
||||
@@ -240,7 +253,12 @@ class ConnectionHandler:
|
||||
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||
finally:
|
||||
# 立即关闭连接,不等待记忆保存完成
|
||||
await self.close(ws)
|
||||
try:
|
||||
await self.close(ws)
|
||||
except Exception as close_error:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"保存记忆后关闭连接失败: {close_error}"
|
||||
)
|
||||
|
||||
async def _route_message(self, message):
|
||||
"""消息路由"""
|
||||
@@ -837,13 +855,22 @@ class ConnectionHandler:
|
||||
"""资源清理方法"""
|
||||
try:
|
||||
# 取消超时任务
|
||||
if self.timeout_task:
|
||||
if self.timeout_task and not self.timeout_task.done():
|
||||
self.timeout_task.cancel()
|
||||
try:
|
||||
await self.timeout_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self.timeout_task = None
|
||||
|
||||
# 清理工具处理器资源
|
||||
if hasattr(self, "func_handler") and self.func_handler:
|
||||
await self.func_handler.cleanup()
|
||||
try:
|
||||
await self.func_handler.cleanup()
|
||||
except Exception as cleanup_error:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"清理工具处理器时出错: {cleanup_error}"
|
||||
)
|
||||
|
||||
# 触发停止事件
|
||||
if self.stop_event:
|
||||
@@ -853,19 +880,58 @@ class ConnectionHandler:
|
||||
self.clear_queues()
|
||||
|
||||
# 关闭WebSocket连接
|
||||
if ws:
|
||||
await ws.close()
|
||||
elif self.websocket:
|
||||
await self.websocket.close()
|
||||
try:
|
||||
if ws:
|
||||
# 安全地检查WebSocket状态并关闭
|
||||
try:
|
||||
if hasattr(ws, "closed") and not ws.closed:
|
||||
await ws.close()
|
||||
elif hasattr(ws, "state") and ws.state.name != "CLOSED":
|
||||
await ws.close()
|
||||
else:
|
||||
# 如果没有closed属性,直接尝试关闭
|
||||
await ws.close()
|
||||
except Exception:
|
||||
# 如果关闭失败,忽略错误
|
||||
pass
|
||||
elif self.websocket:
|
||||
try:
|
||||
if (
|
||||
hasattr(self.websocket, "closed")
|
||||
and not self.websocket.closed
|
||||
):
|
||||
await self.websocket.close()
|
||||
elif (
|
||||
hasattr(self.websocket, "state")
|
||||
and self.websocket.state.name != "CLOSED"
|
||||
):
|
||||
await self.websocket.close()
|
||||
else:
|
||||
# 如果没有closed属性,直接尝试关闭
|
||||
await self.websocket.close()
|
||||
except Exception:
|
||||
# 如果关闭失败,忽略错误
|
||||
pass
|
||||
except Exception as ws_error:
|
||||
self.logger.bind(tag=TAG).error(f"关闭WebSocket连接时出错: {ws_error}")
|
||||
|
||||
# 最后关闭线程池(避免阻塞)
|
||||
if self.executor:
|
||||
self.executor.shutdown(wait=False)
|
||||
try:
|
||||
self.executor.shutdown(wait=False)
|
||||
except Exception as executor_error:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"关闭线程池时出错: {executor_error}"
|
||||
)
|
||||
self.executor = None
|
||||
|
||||
self.logger.bind(tag=TAG).info("连接资源已释放")
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"关闭连接时出错: {e}")
|
||||
finally:
|
||||
# 确保停止事件被设置
|
||||
if self.stop_event:
|
||||
self.stop_event.set()
|
||||
|
||||
def clear_queues(self):
|
||||
"""清空所有任务队列"""
|
||||
@@ -922,9 +988,19 @@ class ConnectionHandler:
|
||||
):
|
||||
if not self.stop_event.is_set():
|
||||
self.logger.bind(tag=TAG).info("连接超时,准备关闭")
|
||||
await self.close(self.websocket)
|
||||
# 设置停止事件,防止重复处理
|
||||
self.stop_event.set()
|
||||
# 使用 try-except 包装关闭操作,确保不会因为异常而阻塞
|
||||
try:
|
||||
await self.close(self.websocket)
|
||||
except Exception as close_error:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"超时关闭连接时出错: {close_error}"
|
||||
)
|
||||
break
|
||||
# 每10秒检查一次,避免过于频繁
|
||||
await asyncio.sleep(10)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"超时检查任务出错: {e}")
|
||||
finally:
|
||||
self.logger.bind(tag=TAG).info("超时检查任务已退出")
|
||||
|
||||
@@ -982,3 +982,28 @@ def sanitize_tool_name(name: str) -> str:
|
||||
"""Sanitize tool names for OpenAI compatibility."""
|
||||
# 支持中文、英文字母、数字、下划线和连字符
|
||||
return re.sub(r"[^a-zA-Z0-9_\-\u4e00-\u9fff]", "_", name)
|
||||
|
||||
|
||||
def validate_mcp_endpoint(mcp_endpoint: str) -> bool:
|
||||
"""
|
||||
校验MCP接入点格式
|
||||
|
||||
Args:
|
||||
mcp_endpoint: MCP接入点字符串
|
||||
|
||||
Returns:
|
||||
bool: 是否有效
|
||||
"""
|
||||
# 1. 检查是否以ws开头
|
||||
if not mcp_endpoint.startswith("ws"):
|
||||
return False
|
||||
|
||||
# 2. 检查是否包含key、call字样
|
||||
if "key" in mcp_endpoint.lower() or "call" in mcp_endpoint.lower():
|
||||
return False
|
||||
|
||||
# 3. 检查是否包含/mcp/字样
|
||||
if "/mcp/" not in mcp_endpoint:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -57,8 +57,25 @@ class WebSocketServer:
|
||||
self.active_connections.add(handler)
|
||||
try:
|
||||
await handler.handle_connection(websocket)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"处理连接时出错: {e}")
|
||||
finally:
|
||||
# 确保从活动连接集合中移除
|
||||
self.active_connections.discard(handler)
|
||||
# 强制关闭连接(如果还没有关闭的话)
|
||||
try:
|
||||
# 安全地检查WebSocket状态并关闭
|
||||
if hasattr(websocket, "closed") and not websocket.closed:
|
||||
await websocket.close()
|
||||
elif hasattr(websocket, "state") and websocket.state.name != "CLOSED":
|
||||
await websocket.close()
|
||||
else:
|
||||
# 如果没有closed属性,直接尝试关闭
|
||||
await websocket.close()
|
||||
except Exception as close_error:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"服务器端强制关闭连接时出错: {close_error}"
|
||||
)
|
||||
|
||||
async def _http_response(self, websocket, request_headers):
|
||||
# 检查是否为 WebSocket 升级请求
|
||||
|
||||
Reference in New Issue
Block a user