mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-03-13 20:53:42 +08:00
Revert "为MCP服务器添加文件操作、图片搜索、PlantUML生成、网页搜索、终端命令、文档解析等实用功能"
This reverts commit 77aeabb4be.
This commit is contained in:
@@ -15,9 +15,9 @@ spring:
|
||||
master:
|
||||
type: ${spring.datasource.type}
|
||||
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
|
||||
password: 1234
|
||||
password: root
|
||||
|
||||
hikari:
|
||||
# 最大连接池数量
|
||||
|
||||
@@ -47,39 +47,6 @@
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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,57 +1,21 @@
|
||||
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.ToolParam;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
/**
|
||||
* MCP工具服务类
|
||||
* 整合了文件操作、图片搜索、PlantUML生成、网页搜索、终端命令、文档解析等功能
|
||||
*
|
||||
* @author ageer,OpenX
|
||||
* @author ageer
|
||||
*/
|
||||
@Service
|
||||
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 = "获取一个指定前缀的随机数")
|
||||
public String add(@ToolParam(description = "字符前缀") String prefix) {
|
||||
// 定义日期格式
|
||||
@@ -67,268 +31,4 @@ public class ToolService {
|
||||
public LocalDateTime getCurrentTime() {
|
||||
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,15 +7,4 @@ spring:
|
||||
name: ruoyi-mcp-serve
|
||||
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