mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Merge branch 'newsnow' into main
This commit is contained in:
@@ -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.8";
|
||||
public static final String VERSION = "0.6.1";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -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<String> getAgentMcpAccessAddress(@PathVariable("agentId") String agentId) {
|
||||
// 获取当前用户
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
|
||||
// 检查权限
|
||||
if (!agentService.checkAgentPermission(agentId, user.getId())) {
|
||||
return new Result<String>().error("没有权限查看该智能体的MCP接入点地址");
|
||||
}
|
||||
String agentMcpAccessAddress = agentMcpAccessPointService.getAgentMcpAccessAddress(agentId);
|
||||
if (agentMcpAccessAddress == null) {
|
||||
return new Result<String>().ok("请联系管理员进入参数管理配置mcp接入点地址");
|
||||
}
|
||||
return new Result<String>().ok(agentMcpAccessAddress);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取智能体的Mcp工具列表")
|
||||
@GetMapping("/tools/{agentId}")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<String>> getAgentMcpToolsList(@PathVariable("agentId") String agentId) {
|
||||
// 获取当前用户
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
|
||||
// 检查权限
|
||||
if (!agentService.checkAgentPermission(agentId, user.getId())) {
|
||||
return new Result<List<String>>().error("没有权限查看该智能体的MCP工具列表");
|
||||
}
|
||||
List<String> agentMcpToolsList = agentMcpAccessPointService.getAgentMcpToolsList(agentId);
|
||||
return new Result<List<String>>().ok(agentMcpToolsList);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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<String> getAgentMcpToolsList(String id);
|
||||
}
|
||||
+180
@@ -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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -1,6 +1,10 @@
|
||||
package xiaozhi.modules.config.service.impl;
|
||||
|
||||
import java.util.*;
|
||||
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;
|
||||
@@ -15,6 +19,7 @@ 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;
|
||||
@@ -39,6 +44,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private final RedisUtils redisUtils;
|
||||
private final TimbreService timbreService;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
||||
|
||||
@Override
|
||||
public Object getConfig(Boolean isCache) {
|
||||
@@ -144,6 +150,12 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
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(
|
||||
|
||||
+32
@@ -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<Void>();
|
||||
@@ -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<String> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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接入点地址');
|
||||
@@ -225,4 +225,11 @@ databaseChangeLog:
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202506251619.sql
|
||||
path: classpath:db/changelog/202506251619.sql
|
||||
- changeSet:
|
||||
id: 202506261637
|
||||
author: hrz
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202506261637.sql
|
||||
|
||||
@@ -1 +1 @@
|
||||
redis.call('FLUSHALL')
|
||||
redis.call('FLUSHDB')
|
||||
Reference in New Issue
Block a user