ソースを参照

新增实用本地tool

Grizzly 5 ヶ月 前
コミット
3d366e8d31

+ 4 - 7
src/main/java/edu/nju/software/aipaasagent/agent/core/reactagent/ToolCallAgent.java

@@ -239,9 +239,6 @@ public class ToolCallAgent extends ReActAgent {
                 log.info("[ToolCallAgent] 流式 - 存储本轮开始消息到 Redis - chatId: {}, messagesCount: {}", 
                         chatId, roundStartMessages.size());
                 chatMemory.add(chatId, roundStartMessages);
-                
-                // 发射 thinking_start 事件
-                emitEvent(StreamEvent.thinkingStart());
 
                 // 2. 执行 ReAct 循环
                 for (int i = 0; i < maxSteps && state != AgentState.FINISHED; i++) {
@@ -361,9 +358,6 @@ public class ToolCallAgent extends ReActAgent {
                 log.info("[ToolCallAgent] SSE流式 - 存储本轮开始消息到 Redis - chatId: {}, messagesCount: {}", 
                         chatId, roundStartMessages.size());
                 chatMemory.add(chatId, roundStartMessages);
-                
-                // 发射 thinking_start 事件
-                emitEvent(StreamEvent.thinkingStart());
 
                 // 2. 执行 ReAct 循环
                 for (int i = 0; i < maxSteps && state != AgentState.FINISHED; i++) {
@@ -419,7 +413,8 @@ public class ToolCallAgent extends ReActAgent {
      */
     private boolean executeStreamStep(String userPrompt, String chatId, Sinks.Many<StreamEvent> sink) {
         try {
-            // Think 阶段
+            // Think 阶段 - 每次都发射 thinking_start
+            emitEvent(StreamEvent.thinkingStart());
             boolean shouldAct = think(userPrompt, chatId);
             if (!shouldAct) {
                 log.info("[ToolCallAgent] 无需行动,直接返回结果");
@@ -435,6 +430,8 @@ public class ToolCallAgent extends ReActAgent {
 
             // Act 阶段
             String actResult = act(userPrompt, chatId);
+            // Act 完成后发射 thinking_end
+            emitEvent(StreamEvent.thinkingEnd());
             emitEvent(StreamEvent.contentChunk("Step " + currentStep + ": " + actResult));
 
             return state != AgentState.FINISHED;

+ 174 - 0
src/main/java/edu/nju/software/aipaasagent/mcp/tool/examples/ResourceDownloadTool.java

@@ -0,0 +1,174 @@
+package edu.nju.software.aipaasagent.mcp.tool.examples;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.http.HttpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+
+/**
+ * Hutool资源下载工具
+ * 使用 Hutool 进行文件下载和资源操作
+ */
+@Slf4j
+@Component
+public class ResourceDownloadTool {
+
+    /**
+     * 下载网络文件
+     *
+     * @param url 文件URL
+     * @param savePath 保存路径(可选)
+     * @return 下载结果
+     */
+    @Tool(name = "downloadFile", description = "下载网络文件到本地。参数url: 文件URL,参数savePath: 保存路径(可选)")
+    public String downloadFile(String url, String savePath) {
+        log.info("下载文件: {}, 保存到: {}", url, savePath);
+        
+        try {
+            String destPath;
+            if (savePath == null || savePath.isEmpty()) {
+                String fileName = URLUtil.getPath(url);
+                if (fileName != null) {
+                    fileName = new File(fileName).getName();
+                }
+                if (fileName == null || fileName.isEmpty()) {
+                    fileName = "download_" + System.currentTimeMillis();
+                }
+                destPath = System.getProperty("user.dir") + File.separator + fileName;
+            } else {
+                destPath = savePath;
+            }
+            
+            long file = HttpUtil.downloadFile(url, destPath);
+            
+            String result = String.format(
+                "文件下载成功\n保存路径: %s\n文件大小: %s",
+                    file,
+                FileUtil.readableFileSize(file)
+            );
+            
+            log.info(result);
+            return result;
+        } catch (Exception e) {
+            String error = "文件下载失败: " + e.getMessage();
+            log.error(error, e);
+            return error;
+        }
+    }
+
+    /**
+     * 获取文件大小
+     *
+     * @param filePath 文件路径
+     * @return 文件大小信息
+     */
+    @Tool(name = "getFileSize", description = "获取文件大小。参数filePath: 文件路径")
+    public String getFileSize(String filePath) {
+        log.info("获取文件大小: {}", filePath);
+        
+        try {
+            File file = new File(filePath);
+            if (!file.exists()) {
+                return "文件不存在: " + filePath;
+            }
+            
+            String result = String.format(
+                "文件路径: %s\n文件大小: %s (%,d 字节)",
+                file.getAbsolutePath(),
+                FileUtil.readableFileSize(file.length()),
+                file.length()
+            );
+            
+            log.info(result);
+            return result;
+        } catch (Exception e) {
+            return "获取文件大小失败: " + e.getMessage();
+        }
+    }
+
+    /**
+     * 读取文本文件内容
+     *
+     * @param filePath 文件路径
+     * @return 文件内容
+     */
+    @Tool(name = "readTextFile", description = "读取文本文件内容。参数filePath: 文件路径")
+    public String readTextFile(String filePath) {
+        log.info("读取文本文件: {}", filePath);
+        
+        try {
+            File file = new File(filePath);
+            if (!file.exists()) {
+                return "文件不存在: " + filePath;
+            }
+            
+            String content = FileUtil.readUtf8String(file);
+            log.info("文件读取成功,内容长度: {} 字符", content.length());
+            
+            return content.length() > 10000 ? 
+                content.substring(0, 10000) + "\n\n[内容已截断,完整内容过长]" : 
+                content;
+        } catch (Exception e) {
+            return "读取文件失败: " + e.getMessage();
+        }
+    }
+
+    /**
+     * 写入文本文件
+     *
+     * @param filePath 文件路径
+     * @param content 要写入的内容
+     * @return 写入结果
+     */
+    @Tool(name = "writeTextFile", description = "写入文本文件。参数filePath: 文件路径,参数content: 要写入的内容")
+    public String writeTextFile(String filePath, String content) {
+        log.info("写入文本文件: {}", filePath);
+        
+        try {
+            FileUtil.writeUtf8String(content, filePath);
+            
+            String result = String.format(
+                "文件写入成功\n文件路径: %s\n内容长度: %d 字符",
+                filePath,
+                content.length()
+            );
+            
+            log.info(result);
+            return result;
+        } catch (Exception e) {
+            return "写入文件失败: " + e.getMessage();
+        }
+    }
+
+    /**
+     * 删除文件
+     *
+     * @param filePath 文件路径
+     * @return 删除结果
+     */
+    @Tool(name = "deleteFile", description = "删除文件。参数filePath: 文件路径")
+    public String deleteFile(String filePath) {
+        log.info("删除文件: {}", filePath);
+        
+        try {
+            File file = new File(filePath);
+            if (!file.exists()) {
+                return "文件不存在: " + filePath;
+            }
+            
+            boolean deleted = FileUtil.del(file);
+            String result = deleted ? 
+                "文件删除成功: " + filePath : 
+                "文件删除失败: " + filePath;
+            
+            log.info(result);
+            return result;
+        } catch (Exception e) {
+            return "删除文件失败: " + e.getMessage();
+        }
+    }
+}

+ 107 - 0
src/main/java/edu/nju/software/aipaasagent/mcp/tool/examples/TerminalTool.java

@@ -0,0 +1,107 @@
+package edu.nju.software.aipaasagent.mcp.tool.examples;
+
+import cn.hutool.core.io.IoUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.stereotype.Component;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 终端操作工具
+ * 使用 ProcessBuilder 执行系统命令
+ */
+@Slf4j
+@Component
+public class TerminalTool {
+
+    /**
+     * 执行终端命令
+     *
+     * @param command 要执行的命令
+     * @return 命令执行结果
+     */
+    @Tool(name = "executeCommand", description = "执行终端命令。参数command: 要执行的命令字符串")
+    public String executeCommand(String command) {
+        log.info("执行终端命令: {}", command);
+        
+        try {
+            ProcessBuilder processBuilder;
+            String os = System.getProperty("os.name").toLowerCase();
+            
+            if (os.contains("win")) {
+                processBuilder = new ProcessBuilder("cmd.exe", "/c", command);
+            } else {
+                processBuilder = new ProcessBuilder("bash", "-c", command);
+            }
+            
+            processBuilder.redirectErrorStream(true);
+            Process process = processBuilder.start();
+            
+            boolean completed = process.waitFor(60, TimeUnit.SECONDS);
+            
+            if (!completed) {
+                process.destroy();
+                return "命令执行超时(60秒)";
+            }
+            
+            String output = IoUtil.read(process.getInputStream(), StandardCharsets.UTF_8);
+            int exitCode = process.exitValue();
+            
+            String result = String.format(
+                "命令执行完成\n退出码: %d\n输出:\n%s",
+                exitCode,
+                output.length() > 5000 ? output.substring(0, 5000) + "\n\n[输出已截断]" : output
+            );
+            
+            log.info("命令执行完成,退出码: {}", exitCode);
+            return result;
+        } catch (Exception e) {
+            String error = "命令执行异常: " + e.getMessage();
+            log.error(error, e);
+            return error;
+        }
+    }
+
+    /**
+     * 获取当前工作目录
+     *
+     * @return 当前工作目录路径
+     */
+    @Tool(name = "getCurrentDirectory", description = "获取当前工作目录的路径。无参数")
+    public String getCurrentDirectory() {
+        String dir = System.getProperty("user.dir");
+        log.info("当前工作目录: {}", dir);
+        return dir;
+    }
+
+    /**
+     * 列出目录内容
+     *
+     * @param directoryPath 目录路径(可选,默认为当前目录)
+     * @return 目录内容列表
+     */
+    @Tool(name = "listDirectory", description = "列出指定目录的内容。参数directoryPath: 目录路径(可选,留空则为当前目录)")
+    public String listDirectory(String directoryPath) {
+        String dir = directoryPath != null && !directoryPath.isEmpty() ? directoryPath : System.getProperty("user.dir");
+        log.info("列出目录内容: {}", dir);
+        
+        try {
+            String command;
+            String os = System.getProperty("os.name").toLowerCase();
+            
+            if (os.contains("win")) {
+                command = "dir \"" + dir + "\"";
+            } else {
+                command = "ls -la \"" + dir + "\"";
+            }
+            
+            return executeCommand(command);
+        } catch (Exception e) {
+            return "列出目录失败: " + e.getMessage();
+        }
+    }
+}

+ 158 - 0
src/main/java/edu/nju/software/aipaasagent/mcp/tool/examples/UtilityTools.java

@@ -0,0 +1,158 @@
+package edu.nju.software.aipaasagent.mcp.tool.examples;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * 通用实用工具类
+ * 包含日期、字符串、加密等常用工具
+ */
+@Slf4j
+@Component
+public class UtilityTools {
+
+    /**
+     * 获取当前日期时间
+     *
+     * @return 当前日期时间字符串
+     */
+    @Tool(name = "getCurrentDateTime", description = "获取当前日期时间。无参数")
+    public String getCurrentDateTime() {
+        String now = DateUtil.now();
+        log.info("当前日期时间: {}", now);
+        return "当前日期时间: " + now;
+    }
+
+    /**
+     * 格式化日期
+     *
+     * @param timestamp 时间戳(毫秒)
+     * @param format 格式化字符串(可选,默认 yyyy-MM-dd HH:mm:ss)
+     * @return 格式化后的日期
+     */
+    @Tool(name = "formatDate", description = "格式化时间戳为日期字符串。参数timestamp: 时间戳(毫秒),参数format: 格式字符串(可选)")
+    public String formatDate(Long timestamp, String format) {
+        String pattern = (format != null && !format.isEmpty()) ? format : "yyyy-MM-dd HH:mm:ss";
+        try {
+            Date date = new Date(timestamp);
+            String result = DateUtil.format(date, pattern);
+            log.info("格式化日期: {} -> {}", timestamp, result);
+            return result;
+        } catch (Exception e) {
+            return "日期格式化失败: " + e.getMessage();
+        }
+    }
+
+    /**
+     * 生成UUID
+     *
+     * @return UUID字符串
+     */
+    @Tool(name = "generateUUID", description = "生成一个UUID。无参数")
+    public String generateUUID() {
+        String uuid = IdUtil.fastSimpleUUID();
+        log.info("生成UUID: {}", uuid);
+        return uuid;
+    }
+
+    /**
+     * 生成随机数
+     *
+     * @param min 最小值(可选,默认0)
+     * @param max 最大值(可选,默认100)
+     * @return 随机数
+     */
+    @Tool(name = "generateRandomNumber", description = "生成指定范围内的随机整数。参数min: 最小值(可选),参数max: 最大值(可选)")
+    public String generateRandomNumber(Integer min, Integer max) {
+        int minVal = min != null ? min : 0;
+        int maxVal = max != null ? max : 100;
+        
+        if (minVal > maxVal) {
+            return "错误:最小值不能大于最大值";
+        }
+        
+        int random = RandomUtil.randomInt(minVal, maxVal + 1);
+        String result = String.format("生成随机数 [%d, %d]: %d", minVal, maxVal, random);
+        log.info(result);
+        return result;
+    }
+
+    /**
+     * 计算MD5哈希
+     *
+     * @param text 要哈希的文本
+     * @return MD5哈希值
+     */
+    @Tool(name = "calculateMD5", description = "计算文本的MD5哈希值。参数text: 要哈希的文本")
+    public String calculateMD5(String text) {
+        if (text == null || text.isEmpty()) {
+            return "错误:输入文本不能为空";
+        }
+        
+        String md5 = DigestUtil.md5Hex(text);
+        log.info("计算MD5: {} -> {}", text.substring(0, Math.min(20, text.length())), md5);
+        return md5;
+    }
+
+    /**
+     * 计算SHA-256哈希
+     *
+     * @param text 要哈希的文本
+     * @return SHA-256哈希值
+     */
+    @Tool(name = "calculateSHA256", description = "计算文本的SHA-256哈希值。参数text: 要哈希的文本")
+    public String calculateSHA256(String text) {
+        if (text == null || text.isEmpty()) {
+            return "错误:输入文本不能为空";
+        }
+        
+        String sha256 = DigestUtil.sha256Hex(text);
+        log.info("计算SHA-256: {} -> {}", text.substring(0, Math.min(20, text.length())), sha256);
+        return sha256;
+    }
+
+    /**
+     * 字符串Base64编码
+     *
+     * @param text 要编码的文本
+     * @return Base64编码字符串
+     */
+    @Tool(name = "base64Encode", description = "对文本进行Base64编码。参数text: 要编码的文本")
+    public String base64Encode(String text) {
+        if (text == null) {
+            return "错误:输入文本不能为空";
+        }
+        
+        String encoded = cn.hutool.core.codec.Base64.encode(text);
+        log.info("Base64编码完成");
+        return encoded;
+    }
+
+    /**
+     * 字符串Base64解码
+     *
+     * @param encodedText Base64编码字符串
+     * @return 解码后的文本
+     */
+    @Tool(name = "base64Decode", description = "对Base64编码字符串进行解码。参数encodedText: Base64编码字符串")
+    public String base64Decode(String encodedText) {
+        if (encodedText == null || encodedText.isEmpty()) {
+            return "错误:输入不能为空";
+        }
+        
+        try {
+            String decoded = cn.hutool.core.codec.Base64.decodeStr(encodedText);
+            log.info("Base64解码完成");
+            return decoded;
+        } catch (Exception e) {
+            return "Base64解码失败: " + e.getMessage();
+        }
+    }
+}

+ 110 - 0
src/main/java/edu/nju/software/aipaasagent/mcp/tool/examples/WebScraperTool.java

@@ -0,0 +1,110 @@
+package edu.nju.software.aipaasagent.mcp.tool.examples;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.stereotype.Component;
+
+/**
+ * 网页抓取工具
+ * 使用 Hutool 进行网页内容抓取
+ */
+@Slf4j
+@Component
+public class WebScraperTool {
+
+    /**
+     * 抓取网页内容
+     *
+     * @param url 网页URL
+     * @return 网页内容
+     */
+    @Tool(name = "fetchWebPage", description = "抓取指定网页的内容。参数url: 要抓取的网页URL")
+    public String fetchWebPage(String url) {
+        log.info("开始抓取网页: {}", url);
+        
+        try {
+            HttpResponse response = HttpRequest.get(url)
+                    .timeout(30000)
+                    .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+                    .execute();
+            
+            if (response.isOk()) {
+                String content = response.body();
+                log.info("网页抓取成功,内容长度: {} 字符", content.length());
+                return content.length() > 10000 ? content.substring(0, 10000) + "\n\n[内容已截断,完整内容过长]" : content;
+            } else {
+                String error = "网页抓取失败,HTTP状态码: " + response.getStatus();
+                log.error(error);
+                return error;
+            }
+        } catch (Exception e) {
+            String error = "网页抓取异常: " + e.getMessage();
+            log.error(error, e);
+            return error;
+        }
+    }
+
+    /**
+     * 抓取网页标题
+     *
+     * @param url 网页URL
+     * @return 网页标题
+     */
+    @Tool(name = "fetchWebTitle", description = "获取指定网页的标题。参数url: 要获取标题的网页URL")
+    public String fetchWebTitle(String url) {
+        log.info("获取网页标题: {}", url);
+        
+        try {
+            HttpResponse response = HttpRequest.get(url)
+                    .timeout(30000)
+                    .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+                    .execute();
+            
+            if (response.isOk()) {
+                String content = response.body();
+                String title = extractTitle(content);
+                log.info("网页标题: {}", title);
+                return title != null ? title : "未找到网页标题";
+            } else {
+                return "获取网页标题失败,HTTP状态码: " + response.getStatus();
+            }
+        } catch (Exception e) {
+            return "获取网页标题异常: " + e.getMessage();
+        }
+    }
+
+    private String extractTitle(String html) {
+        int titleStart = html.indexOf("<title>");
+        int titleEnd = html.indexOf("</title>");
+        if (titleStart != -1 && titleEnd != -1 && titleEnd > titleStart) {
+            return html.substring(titleStart + 7, titleEnd).trim();
+        }
+        return null;
+    }
+
+    /**
+     * 获取网页HTTP状态码
+     *
+     * @param url 网页URL
+     * @return HTTP状态码
+     */
+    @Tool(name = "checkWebStatus", description = "检查指定网页的HTTP状态码。参数url: 要检查的网页URL")
+    public String checkWebStatus(String url) {
+        log.info("检查网页状态: {}", url);
+        
+        try {
+            HttpResponse response = HttpRequest.head(url)
+                    .timeout(10000)
+                    .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+                    .execute();
+            
+            String result = "URL: " + url + "\nHTTP状态码: " + response.getStatus();
+            log.info(result);
+            return result;
+        } catch (Exception e) {
+            return "检查网页状态异常: " + e.getMessage();
+        }
+    }
+}

+ 26 - 2
src/main/resources/agent-config.yml

@@ -38,9 +38,33 @@ react:
 mcp:
   # SeeCoder MCP 工具列表
   onlineTool:
-  #本地的
+  # 本地工具列表
   localTools: #plugin
-    - terminate
+    # 系统核心工具
+    - doTerminate
+    # 网页抓取工具
+    - fetchWebPage
+    - fetchWebTitle
+    - checkWebStatus
+    # 终端操作工具
+    - executeCommand
+    - getCurrentDirectory
+    - listDirectory
+    # 资源下载工具
+    - downloadFile
+    - getFileSize
+    - readTextFile
+    - writeTextFile
+    - deleteFile
+    # 实用工具
+    - getCurrentDateTime
+    - formatDate
+    - generateUUID
+    - generateRandomNumber
+    - calculateMD5
+    - calculateSHA256
+    - base64Encode
+    - base64Decode
   # MCP 策略配置
   policy:
     # 模式:auto(自动)、force(强制)、intelligence(智能),disable(关闭)