mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-14 04:13:39 +00:00
为MCP服务器添加文件操作、图片搜索、PlantUML生成、网页搜索、终端命令、文档解析等实用功能
This commit is contained in:
@@ -15,9 +15,9 @@ spring:
|
|||||||
master:
|
master:
|
||||||
type: ${spring.datasource.type}
|
type: ${spring.datasource.type}
|
||||||
driverClassName: com.mysql.cj.jdbc.Driver
|
driverClassName: com.mysql.cj.jdbc.Driver
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
||||||
username: root
|
username: root
|
||||||
password: root
|
password: 1234
|
||||||
|
|
||||||
hikari:
|
hikari:
|
||||||
# 最大连接池数量
|
# 最大连接池数量
|
||||||
|
|||||||
@@ -47,6 +47,39 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Hutool 工具类 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>5.8.25</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- OkHttp for HTTP requests -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>4.12.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- PlantUML -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sourceforge.plantuml</groupId>
|
||||||
|
<artifactId>plantuml</artifactId>
|
||||||
|
<version>1.2024.3</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring AI Tika for document parsing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-tika-document-reader</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package org.ruoyi.mcpserve.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具配置属性类
|
||||||
|
*
|
||||||
|
* @author OpenX
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "tools")
|
||||||
|
public class ToolsProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pexels图片搜索配置
|
||||||
|
*/
|
||||||
|
private Pexels pexels = new Pexels();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tavily搜索配置
|
||||||
|
*/
|
||||||
|
private Tavily tavily = new Tavily();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件操作配置
|
||||||
|
*/
|
||||||
|
private FileConfig file = new FileConfig();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Pexels {
|
||||||
|
/**
|
||||||
|
* Pexels API密钥
|
||||||
|
*/
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API地址
|
||||||
|
*/
|
||||||
|
private String apiUrl = "https://api.pexels.com/v1/search";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Tavily {
|
||||||
|
/**
|
||||||
|
* Tavily API密钥
|
||||||
|
*/
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API地址
|
||||||
|
*/
|
||||||
|
private String baseUrl = "https://api.tavily.com/search";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class FileConfig {
|
||||||
|
/**
|
||||||
|
* 文件保存目录
|
||||||
|
*/
|
||||||
|
private String saveDir = "./tmp";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,57 @@
|
|||||||
package org.ruoyi.mcpserve.service;
|
package org.ruoyi.mcpserve.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.http.HttpUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import net.sourceforge.plantuml.FileFormat;
|
||||||
|
import net.sourceforge.plantuml.FileFormatOption;
|
||||||
|
import net.sourceforge.plantuml.SourceStringReader;
|
||||||
|
import okhttp3.*;
|
||||||
|
import org.ruoyi.mcpserve.config.ToolsProperties;
|
||||||
|
import org.springframework.ai.document.Document;
|
||||||
|
import org.springframework.ai.reader.tika.TikaDocumentReader;
|
||||||
import org.springframework.ai.tool.annotation.Tool;
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
import org.springframework.ai.tool.annotation.ToolParam;
|
import org.springframework.ai.tool.annotation.ToolParam;
|
||||||
|
import org.springframework.core.io.UrlResource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.UUID;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author ageer
|
* MCP工具服务类
|
||||||
|
* 整合了文件操作、图片搜索、PlantUML生成、网页搜索、终端命令、文档解析等功能
|
||||||
|
*
|
||||||
|
* @author ageer,OpenX
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ToolService {
|
public class ToolService {
|
||||||
|
|
||||||
|
private final ToolsProperties toolsProperties;
|
||||||
|
private final OkHttpClient httpClient;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public ToolService(ToolsProperties toolsProperties) {
|
||||||
|
this.toolsProperties = toolsProperties;
|
||||||
|
this.httpClient = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 基础工具 ====================
|
||||||
|
|
||||||
@Tool(description = "获取一个指定前缀的随机数")
|
@Tool(description = "获取一个指定前缀的随机数")
|
||||||
public String add(@ToolParam(description = "字符前缀") String prefix) {
|
public String add(@ToolParam(description = "字符前缀") String prefix) {
|
||||||
// 定义日期格式
|
// 定义日期格式
|
||||||
@@ -31,4 +67,268 @@ public class ToolService {
|
|||||||
public LocalDateTime getCurrentTime() {
|
public LocalDateTime getCurrentTime() {
|
||||||
return LocalDateTime.now();
|
return LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 文件操作工具 ====================
|
||||||
|
|
||||||
|
@Tool(description = "Read content from a file")
|
||||||
|
public String readFile(@ToolParam(description = "Name of the file to read") String fileName) {
|
||||||
|
String fileDir = toolsProperties.getFile().getSaveDir() + "/file";
|
||||||
|
String filePath = fileDir + "/" + fileName;
|
||||||
|
try {
|
||||||
|
return FileUtil.readUtf8String(filePath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Error reading file: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tool(description = "Write content to a file")
|
||||||
|
public String writeFile(
|
||||||
|
@ToolParam(description = "Name of the file to write") String fileName,
|
||||||
|
@ToolParam(description = "Content to write to the file") String content) {
|
||||||
|
String fileDir = toolsProperties.getFile().getSaveDir() + "/file";
|
||||||
|
String filePath = fileDir + "/" + fileName;
|
||||||
|
try {
|
||||||
|
FileUtil.mkdir(fileDir);
|
||||||
|
FileUtil.writeUtf8String(content, filePath);
|
||||||
|
return "File written successfully to: " + filePath;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Error writing to file: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 图片搜索工具 ====================
|
||||||
|
|
||||||
|
@Tool(description = "Search for images from Pexels")
|
||||||
|
public String searchImage(@ToolParam(description = "Image search keywords") String query) {
|
||||||
|
try {
|
||||||
|
String apiKey = toolsProperties.getPexels().getApiKey();
|
||||||
|
String apiUrl = toolsProperties.getPexels().getApiUrl();
|
||||||
|
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("Authorization", apiKey);
|
||||||
|
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("query", query);
|
||||||
|
|
||||||
|
String response = HttpUtil.createGet(apiUrl)
|
||||||
|
.addHeaders(headers)
|
||||||
|
.form(params)
|
||||||
|
.execute()
|
||||||
|
.body();
|
||||||
|
|
||||||
|
List<String> images = JSONUtil.parseObj(response)
|
||||||
|
.getJSONArray("photos")
|
||||||
|
.stream()
|
||||||
|
.map(photoObj -> (JSONObject) photoObj)
|
||||||
|
.map(photoObj -> photoObj.getJSONObject("src"))
|
||||||
|
.map(photo -> photo.getStr("medium"))
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return String.join(",", images);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Error search image: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PlantUML工具 ====================
|
||||||
|
|
||||||
|
@Tool(description = "Generate a PlantUML diagram and return SVG code")
|
||||||
|
public String generatePlantUmlSvg(
|
||||||
|
@ToolParam(description = "UML diagram source code") String umlCode) {
|
||||||
|
try {
|
||||||
|
if (umlCode == null || umlCode.trim().isEmpty()) {
|
||||||
|
return "Error: UML代码不能为空";
|
||||||
|
}
|
||||||
|
|
||||||
|
System.setProperty("PLANTUML_LIMIT_SIZE", "32768");
|
||||||
|
System.setProperty("java.awt.headless", "true");
|
||||||
|
|
||||||
|
String normalizedUmlCode = normalizeUmlCode(umlCode);
|
||||||
|
|
||||||
|
SourceStringReader reader = new SourceStringReader(normalizedUmlCode);
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
reader.generateImage(outputStream, new FileFormatOption(FileFormat.SVG));
|
||||||
|
|
||||||
|
byte[] svgBytes = outputStream.toByteArray();
|
||||||
|
if (svgBytes.length == 0) {
|
||||||
|
return "Error: 生成的SVG内容为空,请检查UML语法是否正确";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(svgBytes, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Error generating PlantUML: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeUmlCode(String umlCode) {
|
||||||
|
umlCode = umlCode.trim();
|
||||||
|
if (umlCode.contains("@startuml")) {
|
||||||
|
int startIndex = umlCode.indexOf("@startuml");
|
||||||
|
int endIndex = umlCode.lastIndexOf("@enduml");
|
||||||
|
if (endIndex > startIndex) {
|
||||||
|
String startPart = umlCode.substring(startIndex);
|
||||||
|
int firstNewLine = startPart.indexOf('\n');
|
||||||
|
String content = firstNewLine > 0 ? startPart.substring(firstNewLine + 1) : "";
|
||||||
|
if (content.contains("@enduml")) {
|
||||||
|
content = content.substring(0, content.lastIndexOf("@enduml")).trim();
|
||||||
|
}
|
||||||
|
umlCode = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder normalizedCode = new StringBuilder();
|
||||||
|
normalizedCode.append("@startuml\n");
|
||||||
|
normalizedCode.append("!pragma layout smetana\n");
|
||||||
|
normalizedCode.append("skinparam charset UTF-8\n");
|
||||||
|
normalizedCode.append("skinparam defaultFontName SimHei\n");
|
||||||
|
normalizedCode.append("skinparam defaultFontSize 12\n");
|
||||||
|
normalizedCode.append("skinparam dpi 150\n");
|
||||||
|
normalizedCode.append("\n");
|
||||||
|
normalizedCode.append(umlCode);
|
||||||
|
if (!umlCode.endsWith("\n")) {
|
||||||
|
normalizedCode.append("\n");
|
||||||
|
}
|
||||||
|
normalizedCode.append("@enduml");
|
||||||
|
return normalizedCode.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 网页搜索工具 ====================
|
||||||
|
|
||||||
|
@Tool(description = "Search for information from web search engines")
|
||||||
|
public String webSearch(
|
||||||
|
@ToolParam(description = "Search query text") String query,
|
||||||
|
@ToolParam(description = "Max results count") int maxResults) {
|
||||||
|
List<Map<String, String>> results = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
String apiKey = toolsProperties.getTavily().getApiKey();
|
||||||
|
String baseUrl = toolsProperties.getTavily().getBaseUrl();
|
||||||
|
|
||||||
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
|
requestBody.put("query", query);
|
||||||
|
requestBody.put("max_results", maxResults);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(baseUrl)
|
||||||
|
.post(RequestBody.create(MediaType.parse("application/json"),
|
||||||
|
objectMapper.writeValueAsString(requestBody)))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
return "搜索请求失败: " + response;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode jsonNode = objectMapper.readTree(response.body().string()).get("results");
|
||||||
|
if (jsonNode != null && !jsonNode.isEmpty()) {
|
||||||
|
jsonNode.forEach(data -> {
|
||||||
|
Map<String, String> processedResult = new HashMap<>();
|
||||||
|
processedResult.put("title", data.get("title").asText());
|
||||||
|
processedResult.put("url", data.get("url").asText());
|
||||||
|
processedResult.put("content", data.get("content").asText());
|
||||||
|
results.add(processedResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "搜索时发生错误: " + e.getMessage();
|
||||||
|
}
|
||||||
|
return JSONUtil.toJsonStr(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 终端命令工具 ====================
|
||||||
|
|
||||||
|
@Tool(description = "Execute a command in the terminal")
|
||||||
|
public String executeTerminalCommand(
|
||||||
|
@ToolParam(description = "Command to execute in the terminal") String command) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
try {
|
||||||
|
String projectRoot = System.getProperty("user.dir");
|
||||||
|
String fileDir = toolsProperties.getFile().getSaveDir() + "/file";
|
||||||
|
File workingDir = new File(projectRoot, fileDir);
|
||||||
|
|
||||||
|
if (!workingDir.exists()) {
|
||||||
|
workingDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessBuilder processBuilder;
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
if (os.contains("win")) {
|
||||||
|
processBuilder = new ProcessBuilder("cmd.exe", "/c", command);
|
||||||
|
} else {
|
||||||
|
processBuilder = new ProcessBuilder("/bin/sh", "-c", command);
|
||||||
|
}
|
||||||
|
processBuilder.directory(workingDir);
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
output.append(line).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
if (exitCode != 0) {
|
||||||
|
output.append("Command execution failed with exit code: ").append(exitCode);
|
||||||
|
}
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
output.append("Error executing command: ").append(e.getMessage());
|
||||||
|
}
|
||||||
|
return output.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 文档解析工具 ====================
|
||||||
|
|
||||||
|
@Tool(description = "Parse the content of a document from URL")
|
||||||
|
public String parseDocumentFromUrl(
|
||||||
|
@ToolParam(description = "URL of the document to parse") String fileUrl) {
|
||||||
|
try {
|
||||||
|
TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(new UrlResource(fileUrl));
|
||||||
|
List<Document> documents = tikaDocumentReader.read();
|
||||||
|
if (documents.isEmpty()) {
|
||||||
|
return "No content found in the document.";
|
||||||
|
}
|
||||||
|
return documents.get(0).getText();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Error parsing document: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 网页内容加载工具 ====================
|
||||||
|
|
||||||
|
@Tool(description = "Load and extract text content from a web page URL")
|
||||||
|
public String loadWebPage(@ToolParam(description = "The URL of the web page to load") String url) {
|
||||||
|
if (url == null || url.trim().isEmpty()) {
|
||||||
|
return "Error: URL is empty. Please provide a valid URL.";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
return "Error: Failed to load web page, status: " + response.code();
|
||||||
|
}
|
||||||
|
|
||||||
|
String html = response.body().string();
|
||||||
|
// 简单的HTML文本提取
|
||||||
|
String text = html.replaceAll("<script[^>]*>[\\s\\S]*?</script>", "")
|
||||||
|
.replaceAll("<style[^>]*>[\\s\\S]*?</style>", "")
|
||||||
|
.replaceAll("<[^>]+>", " ")
|
||||||
|
.replaceAll("\\s+", " ")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Error loading web page: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,15 @@ spring:
|
|||||||
name: ruoyi-mcp-serve
|
name: ruoyi-mcp-serve
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|
||||||
|
# 工具配置
|
||||||
|
tools:
|
||||||
|
pexels:
|
||||||
|
api-key: your-pexels-api-key #key获取地址: https://www.pexels.com/zh-cn/api/key
|
||||||
|
api-url: https://api.pexels.com/v1/search
|
||||||
|
tavily:
|
||||||
|
api-key: your-tavily-api-key #key获取地址: https://app.tavily.com/home
|
||||||
|
base-url: https://api.tavily.com/search
|
||||||
|
file:
|
||||||
|
save-dir: ./tmp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user