mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-03-27 19:43:42 +08:00
ai编程助手
This commit is contained in:
164
ruoyi-extend/ruoyi-ai-copilot/pom.xml
Normal file
164
ruoyi-extend/ruoyi-ai-copilot/pom.xml
Normal file
@@ -0,0 +1,164 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>ruoyi-ai-copilot</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>copilot</name>
|
||||
<description>SpringAI - Alibaba - Copilot</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spring-boot.version>3.4.5</spring-boot.version>
|
||||
<spring-ai.version>1.0.0</spring-ai.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starters -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- AspectJ Runtime -->
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjrt</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjweaver</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Schema Validation -->
|
||||
<dependency>
|
||||
<groupId>com.networknt</groupId>
|
||||
<artifactId>json-schema-validator</artifactId>
|
||||
<version>1.0.87</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Diff Utils -->
|
||||
<dependency>
|
||||
<groupId>io.github.java-diff-utils</groupId>
|
||||
<artifactId>java-diff-utils</artifactId>
|
||||
<version>4.12</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 添加Maven编译器插件配置 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<parameters>true</parameters>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
</project>
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.example.demo;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.util.BrowserUtil;
|
||||
|
||||
/**
|
||||
* 主要功能:
|
||||
* 1. 文件读取、写入、编辑
|
||||
* 2. 目录列表和结构查看
|
||||
* 4. 连续性文件操作
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties(AppProperties.class)
|
||||
@EnableAspectJAutoProxy
|
||||
public class CopilotApplication {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CopilotApplication.class);
|
||||
|
||||
@Autowired
|
||||
private AppProperties appProperties;
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CopilotApplication.class, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用启动完成后的事件监听器
|
||||
* 自动打开浏览器访问应用首页
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationReady() {
|
||||
AppProperties.Browser browserConfig = appProperties.getBrowser();
|
||||
|
||||
if (!browserConfig.isAutoOpen()) {
|
||||
logger.info("Browser auto-open is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取实际的服务器端口
|
||||
String port = environment.getProperty("server.port", "8080");
|
||||
String actualUrl = browserConfig.getUrl().replace("${server.port:8080}", port);
|
||||
|
||||
logger.info("Application started successfully!");
|
||||
logger.info("Preparing to open browser in {} seconds...", browserConfig.getDelaySeconds());
|
||||
|
||||
// 在新线程中延迟打开浏览器,避免阻塞主线程
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(browserConfig.getDelaySeconds() * 1000L);
|
||||
|
||||
if (BrowserUtil.isValidUrl(actualUrl)) {
|
||||
boolean success = BrowserUtil.openBrowser(actualUrl);
|
||||
if (success) {
|
||||
logger.info("✅ Browser opened successfully: {}", actualUrl);
|
||||
System.out.println("🌐 Web interface opened: " + actualUrl);
|
||||
} else {
|
||||
logger.warn("❌ Failed to open browser automatically");
|
||||
System.out.println("⚠️ Please manually open: " + actualUrl);
|
||||
}
|
||||
} else {
|
||||
logger.error("❌ Invalid URL: {}", actualUrl);
|
||||
System.out.println("⚠️ Invalid URL configured: " + actualUrl);
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Browser opening was interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 应用配置属性
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "app")
|
||||
public class AppProperties {
|
||||
|
||||
private Workspace workspace = new Workspace();
|
||||
private Security security = new Security();
|
||||
private Tools tools = new Tools();
|
||||
private Browser browser = new Browser();
|
||||
|
||||
// Getters and Setters
|
||||
public Workspace getWorkspace() { return workspace; }
|
||||
public void setWorkspace(Workspace workspace) { this.workspace = workspace; }
|
||||
|
||||
public Security getSecurity() { return security; }
|
||||
public void setSecurity(Security security) { this.security = security; }
|
||||
|
||||
public Tools getTools() { return tools; }
|
||||
public void setTools(Tools tools) { this.tools = tools; }
|
||||
|
||||
public Browser getBrowser() { return browser; }
|
||||
public void setBrowser(Browser browser) { this.browser = browser; }
|
||||
|
||||
/**
|
||||
* 工作空间配置
|
||||
*/
|
||||
public static class Workspace {
|
||||
// 使用 Paths.get() 和 File.separator 实现跨平台兼容
|
||||
private String rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
||||
private long maxFileSize = 10485760L; // 10MB
|
||||
private List<String> allowedExtensions = List.of(
|
||||
".txt", ".md", ".java", ".js", ".ts", ".json", ".xml",
|
||||
".yml", ".yaml", ".properties", ".html", ".css", ".sql"
|
||||
);
|
||||
|
||||
// Getters and Setters
|
||||
public String getRootDirectory() { return rootDirectory; }
|
||||
public void setRootDirectory(String rootDirectory) {
|
||||
// 确保设置的路径也是跨平台兼容的
|
||||
this.rootDirectory = Paths.get(rootDirectory).toString();
|
||||
}
|
||||
|
||||
public long getMaxFileSize() { return maxFileSize; }
|
||||
public void setMaxFileSize(long maxFileSize) { this.maxFileSize = maxFileSize; }
|
||||
|
||||
public List<String> getAllowedExtensions() { return allowedExtensions; }
|
||||
public void setAllowedExtensions(List<String> allowedExtensions) { this.allowedExtensions = allowedExtensions; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全配置
|
||||
*/
|
||||
public static class Security {
|
||||
private ApprovalMode approvalMode = ApprovalMode.DEFAULT;
|
||||
private List<String> dangerousCommands = List.of("rm", "del", "format", "fdisk", "mkfs");
|
||||
|
||||
// Getters and Setters
|
||||
public ApprovalMode getApprovalMode() { return approvalMode; }
|
||||
public void setApprovalMode(ApprovalMode approvalMode) { this.approvalMode = approvalMode; }
|
||||
|
||||
public List<String> getDangerousCommands() { return dangerousCommands; }
|
||||
public void setDangerousCommands(List<String> dangerousCommands) { this.dangerousCommands = dangerousCommands; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具配置
|
||||
*/
|
||||
public static class Tools {
|
||||
private ToolConfig readFile = new ToolConfig(true);
|
||||
private ToolConfig writeFile = new ToolConfig(true);
|
||||
private ToolConfig editFile = new ToolConfig(true);
|
||||
private ToolConfig listDirectory = new ToolConfig(true);
|
||||
private ToolConfig shell = new ToolConfig(true);
|
||||
|
||||
// Getters and Setters
|
||||
public ToolConfig getReadFile() { return readFile; }
|
||||
public void setReadFile(ToolConfig readFile) { this.readFile = readFile; }
|
||||
|
||||
public ToolConfig getWriteFile() { return writeFile; }
|
||||
public void setWriteFile(ToolConfig writeFile) { this.writeFile = writeFile; }
|
||||
|
||||
public ToolConfig getEditFile() { return editFile; }
|
||||
public void setEditFile(ToolConfig editFile) { this.editFile = editFile; }
|
||||
|
||||
public ToolConfig getListDirectory() { return listDirectory; }
|
||||
public void setListDirectory(ToolConfig listDirectory) { this.listDirectory = listDirectory; }
|
||||
|
||||
public ToolConfig getShell() { return shell; }
|
||||
public void setShell(ToolConfig shell) { this.shell = shell; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具配置
|
||||
*/
|
||||
public static class ToolConfig {
|
||||
private boolean enabled;
|
||||
|
||||
public ToolConfig() {}
|
||||
public ToolConfig(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器配置
|
||||
*/
|
||||
public static class Browser {
|
||||
private boolean autoOpen = true;
|
||||
private String url = "http://localhost:8080";
|
||||
private int delaySeconds = 2;
|
||||
|
||||
// Getters and Setters
|
||||
public boolean isAutoOpen() { return autoOpen; }
|
||||
public void setAutoOpen(boolean autoOpen) { this.autoOpen = autoOpen; }
|
||||
|
||||
public String getUrl() { return url; }
|
||||
public void setUrl(String url) { this.url = url; }
|
||||
|
||||
public int getDelaySeconds() { return delaySeconds; }
|
||||
public void setDelaySeconds(int delaySeconds) { this.delaySeconds = delaySeconds; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批模式
|
||||
*/
|
||||
public enum ApprovalMode {
|
||||
DEFAULT, // 默认模式,危险操作需要确认
|
||||
AUTO_EDIT, // 自动编辑模式,文件编辑不需要确认
|
||||
YOLO // 完全自动模式,所有操作都不需要确认
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import com.example.demo.service.ToolExecutionLogger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 自定义工具执行监听器
|
||||
* 提供中文日志和详细的文件操作信息记录
|
||||
*
|
||||
* 注意:Spring AI 1.0.0使用@Tool注解来定义工具,不需要ToolCallbackProvider接口
|
||||
* 这个类主要用于工具执行的日志记录和监控
|
||||
*/
|
||||
@Component
|
||||
public class CustomToolExecutionMonitor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CustomToolExecutionMonitor.class);
|
||||
|
||||
@Autowired
|
||||
private ToolExecutionLogger executionLogger;
|
||||
|
||||
/**
|
||||
* 记录工具执行开始
|
||||
*/
|
||||
public long logToolStart(String toolName, String description, String parameters) {
|
||||
String fileInfo = extractFileInfo(toolName, parameters);
|
||||
long callId = executionLogger.logToolStart(toolName, description,
|
||||
String.format("参数: %s | 文件信息: %s", parameters, fileInfo));
|
||||
|
||||
logger.debug("🚀 [Spring AI] 开始执行工具: {} | 文件/目录: {}", toolName, fileInfo);
|
||||
return callId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具执行成功
|
||||
*/
|
||||
public void logToolSuccess(long callId, String toolName, String result, long executionTime, String parameters) {
|
||||
String fileInfo = extractFileInfo(toolName, parameters);
|
||||
logger.debug("✅ [Spring AI] 工具执行成功: {} | 耗时: {}ms | 文件/目录: {}",
|
||||
toolName, executionTime, fileInfo);
|
||||
executionLogger.logToolSuccess(callId, toolName, result, executionTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具执行失败
|
||||
*/
|
||||
public void logToolError(long callId, String toolName, String errorMessage, long executionTime, String parameters) {
|
||||
String fileInfo = extractFileInfo(toolName, parameters);
|
||||
logger.error("❌ [Spring AI] 工具执行失败: {} | 耗时: {}ms | 文件/目录: {} | 错误: {}",
|
||||
toolName, executionTime, fileInfo, errorMessage);
|
||||
executionLogger.logToolError(callId, toolName, errorMessage, executionTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取文件信息用于日志记录
|
||||
*/
|
||||
private String extractFileInfo(String toolName, String arguments) {
|
||||
try {
|
||||
switch (toolName) {
|
||||
case "readFile":
|
||||
case "read_file":
|
||||
return extractPathFromArgs(arguments, "absolutePath", "filePath");
|
||||
case "writeFile":
|
||||
case "write_file":
|
||||
return extractPathFromArgs(arguments, "filePath");
|
||||
case "editFile":
|
||||
case "edit_file":
|
||||
return extractPathFromArgs(arguments, "filePath");
|
||||
case "listDirectory":
|
||||
return extractPathFromArgs(arguments, "directoryPath", "path");
|
||||
case "analyzeProject":
|
||||
case "analyze_project":
|
||||
return extractPathFromArgs(arguments, "projectPath");
|
||||
case "scaffoldProject":
|
||||
case "scaffold_project":
|
||||
return extractPathFromArgs(arguments, "projectPath");
|
||||
case "smartEdit":
|
||||
case "smart_edit":
|
||||
return extractPathFromArgs(arguments, "projectPath");
|
||||
default:
|
||||
return "未知文件路径";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "解析文件路径失败: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从参数中提取路径
|
||||
*/
|
||||
private String extractPathFromArgs(String arguments, String... pathKeys) {
|
||||
for (String key : pathKeys) {
|
||||
String pattern = "\"" + key + "\"\\s*:\\s*\"([^\"]+)\"";
|
||||
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher m = p.matcher(arguments);
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
}
|
||||
return "未找到路径参数";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
/**
|
||||
* 处理超时异常
|
||||
*/
|
||||
@ExceptionHandler({TimeoutException.class, AsyncRequestTimeoutException.class})
|
||||
public ResponseEntity<ErrorResponse> handleTimeoutException(Exception e, WebRequest request) {
|
||||
logger.error("Request timeout occurred", e);
|
||||
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"TIMEOUT_ERROR",
|
||||
"Request timed out. The operation took too long to complete.",
|
||||
"Please try again with a simpler request or check your network connection."
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理AI相关异常
|
||||
*/
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e, WebRequest request) {
|
||||
logger.error("Runtime exception occurred", e);
|
||||
|
||||
// 检查是否是AI调用相关的异常
|
||||
String message = e.getMessage();
|
||||
if (message != null && (message.contains("tool") || message.contains("function") || message.contains("AI"))) {
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"AI_TOOL_ERROR",
|
||||
"An error occurred during AI tool execution: " + message,
|
||||
"The AI encountered an issue while processing your request. Please try rephrasing your request or try again."
|
||||
);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
||||
}
|
||||
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"RUNTIME_ERROR",
|
||||
"An unexpected error occurred: " + message,
|
||||
"Please try again. If the problem persists, contact support."
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理所有其他异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGenericException(Exception e, WebRequest request) {
|
||||
logger.error("Unexpected exception occurred", e);
|
||||
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"INTERNAL_ERROR",
|
||||
"An internal server error occurred",
|
||||
"Something went wrong on our end. Please try again later."
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应类
|
||||
*/
|
||||
public static class ErrorResponse {
|
||||
private String errorCode;
|
||||
private String message;
|
||||
private String suggestion;
|
||||
private long timestamp;
|
||||
|
||||
public ErrorResponse(String errorCode, String message, String suggestion) {
|
||||
this.errorCode = errorCode;
|
||||
this.message = message;
|
||||
this.suggestion = suggestion;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
public String getErrorCode() { return errorCode; }
|
||||
public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
|
||||
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
|
||||
public String getSuggestion() { return suggestion; }
|
||||
public void setSuggestion(String suggestion) { this.suggestion = suggestion; }
|
||||
|
||||
public long getTimestamp() { return timestamp; }
|
||||
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 日志配置类
|
||||
* 确保日志目录存在并记录应用启动信息
|
||||
*/
|
||||
@Configuration
|
||||
public class LoggingConfiguration {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LoggingConfiguration.class);
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationReady() {
|
||||
// 确保日志目录存在
|
||||
File logsDir = new File("logs");
|
||||
if (!logsDir.exists()) {
|
||||
boolean created = logsDir.mkdirs();
|
||||
if (created) {
|
||||
logger.info("📁 创建日志目录: {}", logsDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
// 记录应用启动信息
|
||||
String startTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
logger.info("🎉 ========================================");
|
||||
logger.info("🚀 (♥◠‿◠)ノ゙ AI Copilot启动成功 ლ(´ڡ`ლ)゙");
|
||||
logger.info("🕐 启动时间: {}", startTime);
|
||||
logger.info("📝 日志级别: DEBUG (工具调用详细日志已启用)");
|
||||
logger.info("📁 日志文件: logs/copilot-file-ops.log");
|
||||
logger.info("🔧 支持的工具:");
|
||||
logger.info(" 📖 read_file - 读取文件内容");
|
||||
logger.info(" ✏️ write_file - 写入文件内容");
|
||||
logger.info(" 📝 edit_file - 编辑文件内容");
|
||||
logger.info(" 🔍 analyze_project - 分析项目结构");
|
||||
logger.info(" 🏗️ scaffold_project - 创建项目脚手架");
|
||||
logger.info(" 🧠 smart_edit - 智能编辑项目");
|
||||
logger.info("🎉 ========================================");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import com.example.demo.schema.SchemaValidator;
|
||||
import com.example.demo.tools.*;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Spring AI 配置 - 使用Spring AI 1.0.0规范
|
||||
*/
|
||||
@Configuration
|
||||
public class SpringAIConfiguration {
|
||||
|
||||
@Bean
|
||||
public ChatClient chatClient(ChatModel chatModel,
|
||||
FileOperationTools fileOperationTools,
|
||||
SmartEditTool smartEditTool,
|
||||
AnalyzeProjectTool analyzeProjectTool,
|
||||
ProjectScaffoldTool projectScaffoldTool,
|
||||
AppProperties appProperties) {
|
||||
// 动态获取工作目录路径
|
||||
String workspaceDir = appProperties.getWorkspace().getRootDirectory();
|
||||
|
||||
return ChatClient.builder(chatModel)
|
||||
.defaultSystem("""
|
||||
You are an expert software development assistant with access to file system tools.
|
||||
You excel at creating complete, well-structured projects through systematic execution of multiple related tasks.
|
||||
|
||||
# CORE BEHAVIOR:
|
||||
- When given a complex task (like "create a web project"), break it down into ALL necessary steps
|
||||
- Execute MULTIPLE tool calls in sequence to complete the entire task
|
||||
- Don't stop after just one file - create the complete project structure
|
||||
- Always verify your work by reading files after creating them
|
||||
- Continue working until the ENTIRE task is complete
|
||||
|
||||
# TASK EXECUTION STRATEGY:
|
||||
1. **Plan First**: Mentally outline all files and directories needed
|
||||
2. **Execute Systematically**: Use tools in logical sequence to build the complete solution
|
||||
3. **Verify Progress**: Read files after creation to ensure correctness
|
||||
4. **Continue Until Complete**: Don't stop until the entire requested project/task is finished
|
||||
5. **Signal Continuation**: Use phrases like "Next, I will...", "Now I'll...", "Let me..." to indicate ongoing work
|
||||
|
||||
# AVAILABLE TOOLS:
|
||||
- readFile: Read file contents (supports pagination)
|
||||
- writeFile: Create or overwrite files
|
||||
- editFile: Edit files by replacing specific text
|
||||
- listDirectory: List directory contents (supports recursive)
|
||||
- analyzeProject: Analyze existing projects to understand structure and dependencies
|
||||
- smartEdit: Intelligently edit projects based on natural language descriptions
|
||||
- scaffoldProject: Create new projects with standard structure and templates
|
||||
|
||||
# CRITICAL RULES:
|
||||
- ALWAYS use absolute paths starting with the workspace directory: """ + workspaceDir + """
|
||||
- Use proper path separators for the current operating system
|
||||
- For complex requests, execute 5-15 tool calls to create a complete solution
|
||||
- Use continuation phrases to signal you have more work to do
|
||||
- If creating a project, make it production-ready with proper structure
|
||||
- Continue working until you've delivered a complete, functional result
|
||||
- Only say "completed" or "finished" when the ENTIRE task is truly done
|
||||
- The tools will show both full paths and relative paths - this helps users locate files
|
||||
- Always mention the full path when describing what you've created
|
||||
|
||||
# PATH EXAMPLES:
|
||||
- Correct absolute path format:+ workspaceDir + + file separator + filename
|
||||
- Always ensure paths are within the workspace directory
|
||||
- Use the system's native path separators
|
||||
|
||||
# CONTINUATION SIGNALS:
|
||||
Use these phrases when you have more work to do:
|
||||
- "Next, I will create..."
|
||||
- "Now I'll add..."
|
||||
- "Let me now..."
|
||||
- "Moving on to..."
|
||||
- "I'll proceed to..."
|
||||
|
||||
Remember: Your goal is to deliver COMPLETE solutions through continuous execution!
|
||||
""")
|
||||
.defaultTools(fileOperationTools, smartEditTool, analyzeProjectTool, projectScaffoldTool)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 为所有工具注入Schema验证器
|
||||
*/
|
||||
@Autowired
|
||||
public void configureTools(List<BaseTool<?>> tools, SchemaValidator schemaValidator) {
|
||||
tools.forEach(tool -> tool.setSchemaValidator(schemaValidator));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
/**
|
||||
* 任务上下文持有者
|
||||
* 使用ThreadLocal存储当前任务ID,供AOP切面使用
|
||||
*/
|
||||
public class TaskContextHolder {
|
||||
|
||||
private static final ThreadLocal<String> taskIdHolder = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 设置当前任务ID
|
||||
*/
|
||||
public static void setCurrentTaskId(String taskId) {
|
||||
taskIdHolder.set(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前任务ID
|
||||
*/
|
||||
public static String getCurrentTaskId() {
|
||||
return taskIdHolder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前任务ID
|
||||
*/
|
||||
public static void clearCurrentTaskId() {
|
||||
taskIdHolder.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有当前任务ID
|
||||
*/
|
||||
public static boolean hasCurrentTaskId() {
|
||||
return taskIdHolder.get() != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import com.example.demo.service.ToolExecutionLogger;
|
||||
import com.example.demo.service.LogStreamService;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 工具调用日志切面
|
||||
* 拦截 Spring AI 的工具调用并提供中文日志
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class ToolCallLoggingAspect {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ToolCallLoggingAspect.class);
|
||||
|
||||
@Autowired
|
||||
private ToolExecutionLogger executionLogger;
|
||||
|
||||
@Autowired
|
||||
private LogStreamService logStreamService;
|
||||
|
||||
/**
|
||||
* 拦截使用@Tool注解的方法执行
|
||||
*/
|
||||
@Around("@annotation(org.springframework.ai.tool.annotation.Tool)")
|
||||
public Object interceptToolAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
Object[] args = joinPoint.getArgs();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
|
||||
|
||||
// 详细的参数信息
|
||||
String parametersInfo = formatMethodParameters(args);
|
||||
String fileInfo = extractFileInfoFromMethodArgs(methodName, args);
|
||||
|
||||
logger.debug("🚀 [Spring AI @Tool] 执行工具: {}.{} | 参数: {} | 文件/目录: {}",
|
||||
className, methodName, parametersInfo, fileInfo);
|
||||
|
||||
// 获取当前任务ID (从线程本地变量或其他方式)
|
||||
String taskId = getCurrentTaskId();
|
||||
|
||||
// 推送工具开始执行事件
|
||||
if (taskId != null) {
|
||||
String startMessage = generateStartMessage(methodName, fileInfo);
|
||||
logStreamService.pushToolStart(taskId, methodName, fileInfo, startMessage);
|
||||
}
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
Object result = joinPoint.proceed();
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
logger.debug("✅ [Spring AI @Tool] 工具执行成功: {}.{} | 耗时: {}ms | 文件/目录: {} | 参数: {}",
|
||||
className, methodName, executionTime, fileInfo, parametersInfo);
|
||||
|
||||
// 推送工具执行成功事件
|
||||
if (taskId != null) {
|
||||
String successMessage = generateSuccessMessage(methodName, fileInfo, result, executionTime);
|
||||
logStreamService.pushToolSuccess(taskId, methodName, fileInfo, successMessage, executionTime);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Throwable e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
logger.error("❌ [Spring AI @Tool] 工具执行失败: {}.{} | 耗时: {}ms | 文件/目录: {} | 参数: {} | 错误: {}",
|
||||
className, methodName, executionTime, fileInfo, parametersInfo, e.getMessage());
|
||||
|
||||
// 推送工具执行失败事件
|
||||
if (taskId != null) {
|
||||
String errorMessage = generateErrorMessage(methodName, fileInfo, e.getMessage());
|
||||
logStreamService.pushToolError(taskId, methodName, fileInfo, errorMessage, executionTime);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化方法参数为可读字符串
|
||||
*/
|
||||
private String formatMethodParameters(Object[] args) {
|
||||
if (args == null || args.length == 0) {
|
||||
return "无参数";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
Object arg = args[i];
|
||||
if (arg == null) {
|
||||
sb.append("null");
|
||||
} else if (arg instanceof String) {
|
||||
String str = (String) arg;
|
||||
// 如果字符串太长,截断显示
|
||||
if (str.length() > 100) {
|
||||
sb.append("\"").append(str.substring(0, 100)).append("...\"");
|
||||
} else {
|
||||
sb.append("\"").append(str).append("\"");
|
||||
}
|
||||
} else {
|
||||
sb.append(arg.toString());
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从方法参数中直接提取文件信息
|
||||
*/
|
||||
private String extractFileInfoFromMethodArgs(String methodName, Object[] args) {
|
||||
if (args == null || args.length == 0) {
|
||||
return "无参数";
|
||||
}
|
||||
|
||||
try {
|
||||
switch (methodName) {
|
||||
case "readFile":
|
||||
// readFile(String absolutePath, Integer offset, Integer limit)
|
||||
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
|
||||
|
||||
case "writeFile":
|
||||
// writeFile(String filePath, String content)
|
||||
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
|
||||
|
||||
case "editFile":
|
||||
// editFile(String filePath, String oldText, String newText)
|
||||
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
|
||||
|
||||
case "listDirectory":
|
||||
// listDirectory(String directoryPath, Boolean recursive)
|
||||
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
|
||||
|
||||
case "analyzeProject":
|
||||
// analyzeProject(String projectPath, ...)
|
||||
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
|
||||
|
||||
case "scaffoldProject":
|
||||
// scaffoldProject(String projectName, String projectType, String projectPath, ...)
|
||||
return args.length > 2 && args[2] != null ? args[2].toString() : "未指定路径";
|
||||
|
||||
case "smartEdit":
|
||||
// smartEdit(String projectPath, ...)
|
||||
return args.length > 0 && args[0] != null ? args[0].toString() : "未指定路径";
|
||||
|
||||
default:
|
||||
// 对于未知方法,尝试从第一个参数中提取路径
|
||||
if (args.length > 0 && args[0] != null) {
|
||||
String firstArg = args[0].toString();
|
||||
if (firstArg.contains("/") || firstArg.contains("\\")) {
|
||||
return firstArg;
|
||||
}
|
||||
}
|
||||
return "未识别的工具类型";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "解析参数失败: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从参数字符串中提取文件信息(备用方法)
|
||||
*/
|
||||
private String extractFileInfoFromArgs(String toolName, String arguments) {
|
||||
try {
|
||||
switch (toolName) {
|
||||
case "readFile":
|
||||
case "read_file":
|
||||
return extractPathFromString(arguments, "absolutePath", "filePath");
|
||||
case "writeFile":
|
||||
case "write_file":
|
||||
case "editFile":
|
||||
case "edit_file":
|
||||
return extractPathFromString(arguments, "filePath");
|
||||
case "listDirectory":
|
||||
return extractPathFromString(arguments, "directoryPath", "path");
|
||||
case "analyzeProject":
|
||||
case "analyze_project":
|
||||
case "scaffoldProject":
|
||||
case "scaffold_project":
|
||||
case "smartEdit":
|
||||
case "smart_edit":
|
||||
return extractPathFromString(arguments, "projectPath");
|
||||
default:
|
||||
return "未指定文件路径";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "解析文件路径失败";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从字符串中提取路径
|
||||
*/
|
||||
private String extractPathFromString(String text, String... pathKeys) {
|
||||
for (String key : pathKeys) {
|
||||
// JSON 格式
|
||||
Pattern jsonPattern = Pattern.compile("\"" + key + "\"\\s*:\\s*\"([^\"]+)\"");
|
||||
Matcher jsonMatcher = jsonPattern.matcher(text);
|
||||
if (jsonMatcher.find()) {
|
||||
return jsonMatcher.group(1);
|
||||
}
|
||||
|
||||
// 键值对格式
|
||||
Pattern kvPattern = Pattern.compile(key + "=([^,\\s\\]]+)");
|
||||
Matcher kvMatcher = kvPattern.matcher(text);
|
||||
if (kvMatcher.find()) {
|
||||
return kvMatcher.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
return "未找到路径";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前任务ID
|
||||
* 从线程本地变量或请求上下文中获取
|
||||
*/
|
||||
private String getCurrentTaskId() {
|
||||
// 这里需要从某个地方获取当前任务ID
|
||||
// 可以从ThreadLocal、RequestAttributes或其他方式获取
|
||||
try {
|
||||
// 临时实现:从线程名或其他方式获取
|
||||
return TaskContextHolder.getCurrentTaskId();
|
||||
} catch (Exception e) {
|
||||
logger.debug("无法获取当前任务ID: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成工具开始执行消息
|
||||
*/
|
||||
private String generateStartMessage(String toolName, String fileInfo) {
|
||||
switch (toolName) {
|
||||
case "readFile":
|
||||
return "正在读取文件: " + getFileName(fileInfo);
|
||||
case "writeFile":
|
||||
return "正在写入文件: " + getFileName(fileInfo);
|
||||
case "editFile":
|
||||
return "正在编辑文件: " + getFileName(fileInfo);
|
||||
case "listDirectory":
|
||||
return "正在列出目录: " + fileInfo;
|
||||
case "analyzeProject":
|
||||
return "正在分析项目: " + fileInfo;
|
||||
case "scaffoldProject":
|
||||
return "正在创建项目脚手架: " + fileInfo;
|
||||
case "smartEdit":
|
||||
return "正在智能编辑项目: " + fileInfo;
|
||||
default:
|
||||
return "正在执行工具: " + toolName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成工具执行成功消息
|
||||
*/
|
||||
private String generateSuccessMessage(String toolName, String fileInfo, Object result, long executionTime) {
|
||||
String fileName = getFileName(fileInfo);
|
||||
switch (toolName) {
|
||||
case "readFile":
|
||||
return String.format("已读取文件 %s (耗时 %dms)", fileName, executionTime);
|
||||
case "writeFile":
|
||||
return String.format("已写入文件 %s (耗时 %dms)", fileName, executionTime);
|
||||
case "editFile":
|
||||
return String.format("已编辑文件 %s (耗时 %dms)", fileName, executionTime);
|
||||
case "listDirectory":
|
||||
return String.format("已列出目录 %s (耗时 %dms)", fileInfo, executionTime);
|
||||
case "analyzeProject":
|
||||
return String.format("已分析项目 %s (耗时 %dms)", fileInfo, executionTime);
|
||||
case "scaffoldProject":
|
||||
return String.format("已创建项目脚手架 %s (耗时 %dms)", fileInfo, executionTime);
|
||||
case "smartEdit":
|
||||
return String.format("已智能编辑项目 %s (耗时 %dms)", fileInfo, executionTime);
|
||||
default:
|
||||
return String.format("工具 %s 执行成功 (耗时 %dms)", toolName, executionTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成工具执行失败消息
|
||||
*/
|
||||
private String generateErrorMessage(String toolName, String fileInfo, String errorMsg) {
|
||||
String fileName = getFileName(fileInfo);
|
||||
return String.format("工具 %s 执行失败: %s (文件: %s)", toolName, errorMsg, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件路径中提取文件名
|
||||
*/
|
||||
private String getFileName(String filePath) {
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
return "未知文件";
|
||||
}
|
||||
|
||||
// 处理Windows和Unix路径
|
||||
int lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
|
||||
if (lastSlash >= 0 && lastSlash < filePath.length() - 1) {
|
||||
return filePath.substring(lastSlash + 1);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.dto.ChatRequestDto;
|
||||
import com.example.demo.service.ContinuousConversationService;
|
||||
import com.example.demo.service.ToolExecutionLogger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 聊天控制器
|
||||
* 处理与AI的对话和工具调用
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class ChatController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ChatController.class);
|
||||
|
||||
private final ChatClient chatClient;
|
||||
private final ContinuousConversationService continuousConversationService;
|
||||
private final ToolExecutionLogger executionLogger;
|
||||
|
||||
// 简单的会话存储(生产环境应该使用数据库或Redis)
|
||||
private final List<Message> conversationHistory = new ArrayList<>();
|
||||
|
||||
public ChatController(ChatClient chatClient, ContinuousConversationService continuousConversationService, ToolExecutionLogger executionLogger) {
|
||||
this.chatClient = chatClient;
|
||||
this.continuousConversationService = continuousConversationService;
|
||||
this.executionLogger = executionLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给AI - 支持连续工具调用
|
||||
*/
|
||||
// 在现有ChatController中修改sendMessage方法
|
||||
|
||||
@PostMapping("/message")
|
||||
public Mono<ChatResponseDto> sendMessage(@RequestBody ChatRequestDto request) {
|
||||
return Mono.fromCallable(() -> {
|
||||
try {
|
||||
logger.info("💬 ========== 新的聊天请求 ==========");
|
||||
logger.info("📝 用户消息: {}", request.getMessage());
|
||||
logger.info("🕐 请求时间: {}", java.time.LocalDateTime.now());
|
||||
|
||||
// 智能判断是否需要工具调用
|
||||
boolean needsToolExecution = continuousConversationService.isLikelyToNeedTools(request.getMessage());
|
||||
logger.info("🔍 工具需求分析: {}", needsToolExecution ? "可能需要工具" : "简单对话");
|
||||
|
||||
if (needsToolExecution) {
|
||||
// 需要工具调用的复杂任务 - 使用异步模式
|
||||
String taskId = continuousConversationService.startTask(request.getMessage());
|
||||
logger.info("🆔 任务ID: {}", taskId);
|
||||
|
||||
// 记录任务开始
|
||||
executionLogger.logToolStatistics(); // 显示当前统计
|
||||
|
||||
// 异步执行连续对话
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
logger.info("🚀 开始异步执行连续对话任务: {}", taskId);
|
||||
continuousConversationService.executeContinuousConversation(
|
||||
taskId, request.getMessage(), conversationHistory
|
||||
);
|
||||
logger.info("✅ 连续对话任务完成: {}", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.error("❌ 异步对话执行错误: {}", e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
|
||||
// 返回异步任务响应
|
||||
ChatResponseDto responseDto = new ChatResponseDto();
|
||||
responseDto.setTaskId(taskId);
|
||||
responseDto.setMessage("任务已启动,正在处理中...");
|
||||
responseDto.setSuccess(true);
|
||||
responseDto.setAsyncTask(true);
|
||||
|
||||
logger.info("📤 返回响应: taskId={}, 异步任务已启动", taskId);
|
||||
return responseDto;
|
||||
} else {
|
||||
// 简单对话 - 使用流式模式
|
||||
logger.info("🔄 执行流式对话处理");
|
||||
|
||||
// 返回流式响应标识,让前端建立流式连接
|
||||
ChatResponseDto responseDto = new ChatResponseDto();
|
||||
responseDto.setMessage("开始流式对话...");
|
||||
responseDto.setSuccess(true);
|
||||
responseDto.setAsyncTask(false); // 关键:设置为false,表示不是工具任务
|
||||
responseDto.setStreamResponse(true); // 新增:标识为流式响应
|
||||
responseDto.setTotalTurns(1);
|
||||
|
||||
logger.info("📤 返回流式响应标识");
|
||||
return responseDto;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error processing chat message", e);
|
||||
ChatResponseDto errorResponse = new ChatResponseDto();
|
||||
errorResponse.setMessage("Error: " + e.getMessage());
|
||||
errorResponse.setSuccess(false);
|
||||
return errorResponse;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 流式聊天 - 真正的流式实现
|
||||
*/
|
||||
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<String> streamMessage(@RequestBody ChatRequestDto request) {
|
||||
logger.info("🌊 开始流式对话: {}", request.getMessage());
|
||||
|
||||
return Flux.create(sink -> {
|
||||
try {
|
||||
UserMessage userMessage = new UserMessage(request.getMessage());
|
||||
conversationHistory.add(userMessage);
|
||||
|
||||
// 使用Spring AI的流式API
|
||||
Flux<String> contentStream = chatClient.prompt()
|
||||
.messages(conversationHistory)
|
||||
.stream()
|
||||
.content();
|
||||
|
||||
// 订阅流式内容并转发给前端
|
||||
contentStream
|
||||
.doOnNext(content -> {
|
||||
logger.debug("📨 流式内容片段: {}", content);
|
||||
// 发送SSE格式的数据
|
||||
sink.next("data: " + content + "\n\n");
|
||||
})
|
||||
.doOnComplete(() -> {
|
||||
logger.info("✅ 流式对话完成");
|
||||
sink.next("data: [DONE]\n\n");
|
||||
sink.complete();
|
||||
})
|
||||
.doOnError(error -> {
|
||||
logger.error("❌ 流式对话错误: {}", error.getMessage());
|
||||
sink.error(error);
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("❌ 流式对话启动失败: {}", e.getMessage());
|
||||
sink.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除对话历史
|
||||
*/
|
||||
@PostMapping("/clear")
|
||||
public Mono<Map<String, String>> clearHistory() {
|
||||
conversationHistory.clear();
|
||||
logger.info("Conversation history cleared");
|
||||
return Mono.just(Map.of("status", "success", "message", "Conversation history cleared"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话历史
|
||||
*/
|
||||
@GetMapping("/history")
|
||||
public Mono<List<MessageDto>> getHistory() {
|
||||
List<MessageDto> history = conversationHistory.stream()
|
||||
.map(message -> {
|
||||
MessageDto dto = new MessageDto();
|
||||
dto.setContent(message.getText());
|
||||
dto.setRole(message instanceof UserMessage ? "user" : "assistant");
|
||||
return dto;
|
||||
})
|
||||
.toList();
|
||||
|
||||
return Mono.just(history);
|
||||
}
|
||||
|
||||
// 注意:Spring AI 1.0.0 使用不同的函数调用方式
|
||||
// 函数需要在配置中注册,而不是在运行时动态创建
|
||||
|
||||
public static class ChatResponseDto {
|
||||
private String taskId;
|
||||
private String message;
|
||||
private boolean success;
|
||||
private boolean asyncTask;
|
||||
private boolean streamResponse; // 新增:标识是否为流式响应
|
||||
private int totalTurns;
|
||||
private boolean reachedMaxTurns;
|
||||
private String stopReason;
|
||||
private long totalDurationMs;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
|
||||
public boolean isSuccess() { return success; }
|
||||
public void setSuccess(boolean success) { this.success = success; }
|
||||
|
||||
public boolean isAsyncTask() {
|
||||
return asyncTask;
|
||||
}
|
||||
|
||||
public void setAsyncTask(boolean asyncTask) {
|
||||
this.asyncTask = asyncTask;
|
||||
}
|
||||
|
||||
public boolean isStreamResponse() {
|
||||
return streamResponse;
|
||||
}
|
||||
|
||||
public void setStreamResponse(boolean streamResponse) {
|
||||
this.streamResponse = streamResponse;
|
||||
}
|
||||
|
||||
public int getTotalTurns() { return totalTurns; }
|
||||
public void setTotalTurns(int totalTurns) { this.totalTurns = totalTurns; }
|
||||
|
||||
public boolean isReachedMaxTurns() { return reachedMaxTurns; }
|
||||
public void setReachedMaxTurns(boolean reachedMaxTurns) { this.reachedMaxTurns = reachedMaxTurns; }
|
||||
|
||||
public String getStopReason() { return stopReason; }
|
||||
public void setStopReason(String stopReason) { this.stopReason = stopReason; }
|
||||
|
||||
public long getTotalDurationMs() { return totalDurationMs; }
|
||||
public void setTotalDurationMs(long totalDurationMs) { this.totalDurationMs = totalDurationMs; }
|
||||
}
|
||||
|
||||
public static class MessageDto {
|
||||
private String content;
|
||||
private String role;
|
||||
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String role) { this.role = role; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.service.LogStreamService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* SSE日志流控制器
|
||||
* 提供SSE连接端点
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/logs")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class LogStreamController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LogStreamController.class);
|
||||
|
||||
@Autowired
|
||||
private LogStreamService logStreamService;
|
||||
|
||||
/**
|
||||
* 建立SSE连接
|
||||
* 前端通过此端点建立实时日志推送连接
|
||||
*/
|
||||
@GetMapping(value = "/stream/{taskId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter streamLogs(@PathVariable("taskId") String taskId) {
|
||||
logger.info("🔗 收到SSE连接请求: taskId={}", taskId);
|
||||
|
||||
try {
|
||||
SseEmitter emitter = logStreamService.createConnection(taskId);
|
||||
logger.info("✅ SSE连接建立成功: taskId={}", taskId);
|
||||
return emitter;
|
||||
} catch (Exception e) {
|
||||
logger.error("❌ SSE连接建立失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
throw new RuntimeException("Failed to create SSE connection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭SSE连接
|
||||
*/
|
||||
@PostMapping("/close/{taskId}")
|
||||
public void closeConnection(@PathVariable("taskId") String taskId) {
|
||||
logger.info("🔚 收到关闭SSE连接请求: taskId={}", taskId);
|
||||
logStreamService.closeConnection(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
@GetMapping("/status")
|
||||
public ConnectionStatus getConnectionStatus() {
|
||||
int activeConnections = logStreamService.getActiveConnectionCount();
|
||||
logger.debug("📊 当前活跃SSE连接数: {}", activeConnections);
|
||||
|
||||
ConnectionStatus status = new ConnectionStatus();
|
||||
status.setActiveConnections(activeConnections);
|
||||
status.setStatus("OK");
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态DTO
|
||||
*/
|
||||
public static class ConnectionStatus {
|
||||
private int activeConnections;
|
||||
private String status;
|
||||
|
||||
public int getActiveConnections() {
|
||||
return activeConnections;
|
||||
}
|
||||
|
||||
public void setActiveConnections(int activeConnections) {
|
||||
this.activeConnections = activeConnections;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.model.TaskStatus;
|
||||
import com.example.demo.service.ContinuousConversationService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/task")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class TaskStatusController {
|
||||
|
||||
private final ContinuousConversationService conversationService;
|
||||
|
||||
public TaskStatusController(ContinuousConversationService conversationService) {
|
||||
this.conversationService = conversationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务状态
|
||||
*/
|
||||
@GetMapping("/status/{taskId}")
|
||||
public Mono<TaskStatusDto> getTaskStatus(@PathVariable("taskId") String taskId) {
|
||||
return Mono.fromCallable(() -> {
|
||||
TaskStatus status = conversationService.getTaskStatus(taskId);
|
||||
if (status == null) {
|
||||
throw new RuntimeException("Task not found: " + taskId);
|
||||
}
|
||||
|
||||
TaskStatusDto dto = new TaskStatusDto();
|
||||
dto.setTaskId(status.getTaskId());
|
||||
dto.setStatus(status.getStatus());
|
||||
dto.setCurrentAction(status.getCurrentAction());
|
||||
dto.setSummary(status.getSummary());
|
||||
dto.setCurrentTurn(status.getCurrentTurn());
|
||||
dto.setTotalEstimatedTurns(status.getTotalEstimatedTurns());
|
||||
dto.setProgressPercentage(status.getProgressPercentage());
|
||||
dto.setElapsedTime(status.getElapsedTime());
|
||||
dto.setErrorMessage(status.getErrorMessage());
|
||||
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话结果
|
||||
*/
|
||||
@GetMapping("/result/{taskId}")
|
||||
public Mono<ConversationResultDto> getConversationResult(@PathVariable("taskId") String taskId) {
|
||||
return Mono.fromCallable(() -> {
|
||||
ContinuousConversationService.ConversationResult result = conversationService.getConversationResult(taskId);
|
||||
if (result == null) {
|
||||
throw new RuntimeException("Conversation result not found: " + taskId);
|
||||
}
|
||||
|
||||
ConversationResultDto dto = new ConversationResultDto();
|
||||
dto.setTaskId(taskId);
|
||||
dto.setFullResponse(result.getFullResponse());
|
||||
dto.setTurnResponses(result.getTurnResponses());
|
||||
dto.setTotalTurns(result.getTotalTurns());
|
||||
dto.setReachedMaxTurns(result.isReachedMaxTurns());
|
||||
dto.setStopReason(result.getStopReason());
|
||||
dto.setTotalDurationMs(result.getTotalDurationMs());
|
||||
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
// DTO类
|
||||
public static class TaskStatusDto {
|
||||
private String taskId;
|
||||
private String status;
|
||||
private String currentAction;
|
||||
private String summary;
|
||||
private int currentTurn;
|
||||
private int totalEstimatedTurns;
|
||||
private double progressPercentage;
|
||||
private long elapsedTime;
|
||||
private String errorMessage;
|
||||
|
||||
// Getters and Setters
|
||||
public String getTaskId() { return taskId; }
|
||||
public void setTaskId(String taskId) { this.taskId = taskId; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public String getCurrentAction() { return currentAction; }
|
||||
public void setCurrentAction(String currentAction) { this.currentAction = currentAction; }
|
||||
|
||||
public String getSummary() { return summary; }
|
||||
public void setSummary(String summary) { this.summary = summary; }
|
||||
|
||||
public int getCurrentTurn() { return currentTurn; }
|
||||
public void setCurrentTurn(int currentTurn) { this.currentTurn = currentTurn; }
|
||||
|
||||
public int getTotalEstimatedTurns() { return totalEstimatedTurns; }
|
||||
public void setTotalEstimatedTurns(int totalEstimatedTurns) { this.totalEstimatedTurns = totalEstimatedTurns; }
|
||||
|
||||
public double getProgressPercentage() { return progressPercentage; }
|
||||
public void setProgressPercentage(double progressPercentage) { this.progressPercentage = progressPercentage; }
|
||||
|
||||
public long getElapsedTime() { return elapsedTime; }
|
||||
public void setElapsedTime(long elapsedTime) { this.elapsedTime = elapsedTime; }
|
||||
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||
}
|
||||
|
||||
// 对话结果DTO类
|
||||
public static class ConversationResultDto {
|
||||
private String taskId;
|
||||
private String fullResponse;
|
||||
private java.util.List<String> turnResponses;
|
||||
private int totalTurns;
|
||||
private boolean reachedMaxTurns;
|
||||
private String stopReason;
|
||||
private long totalDurationMs;
|
||||
|
||||
// Getters and Setters
|
||||
public String getTaskId() { return taskId; }
|
||||
public void setTaskId(String taskId) { this.taskId = taskId; }
|
||||
|
||||
public String getFullResponse() { return fullResponse; }
|
||||
public void setFullResponse(String fullResponse) { this.fullResponse = fullResponse; }
|
||||
|
||||
public java.util.List<String> getTurnResponses() { return turnResponses; }
|
||||
public void setTurnResponses(java.util.List<String> turnResponses) { this.turnResponses = turnResponses; }
|
||||
|
||||
public int getTotalTurns() { return totalTurns; }
|
||||
public void setTotalTurns(int totalTurns) { this.totalTurns = totalTurns; }
|
||||
|
||||
public boolean isReachedMaxTurns() { return reachedMaxTurns; }
|
||||
public void setReachedMaxTurns(boolean reachedMaxTurns) { this.reachedMaxTurns = reachedMaxTurns; }
|
||||
|
||||
public String getStopReason() { return stopReason; }
|
||||
public void setStopReason(String stopReason) { this.stopReason = stopReason; }
|
||||
|
||||
public long getTotalDurationMs() { return totalDurationMs; }
|
||||
public void setTotalDurationMs(long totalDurationMs) { this.totalDurationMs = totalDurationMs; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* Web页面控制器
|
||||
*/
|
||||
@Controller
|
||||
public class WebController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String index() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理favicon.ico请求,避免404错误
|
||||
*/
|
||||
@GetMapping("/favicon.ico")
|
||||
public ResponseEntity<Void> favicon() {
|
||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
/**
|
||||
* 聊天请求数据传输对象
|
||||
*/
|
||||
public class ChatRequestDto {
|
||||
private String message;
|
||||
private String sessionId; // 可选:用于会话管理
|
||||
|
||||
public ChatRequestDto() {
|
||||
}
|
||||
|
||||
public ChatRequestDto(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ChatRequestDto(String message, String sessionId) {
|
||||
this.message = message;
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChatRequestDto{" +
|
||||
"message='" + message + '\'' +
|
||||
", sessionId='" + sessionId + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Project context information
|
||||
* Contains complete project analysis results for AI understanding
|
||||
*/
|
||||
public class ProjectContext {
|
||||
private Path projectRoot;
|
||||
private ProjectType projectType;
|
||||
private ProjectStructure projectStructure;
|
||||
private List<DependencyInfo> dependencies;
|
||||
private List<ConfigFile> configFiles;
|
||||
private CodeStatistics codeStatistics;
|
||||
private Map<String, Object> metadata;
|
||||
private String contextSummary;
|
||||
|
||||
public ProjectContext() {
|
||||
this.dependencies = new ArrayList<>();
|
||||
this.configFiles = new ArrayList<>();
|
||||
this.metadata = new HashMap<>();
|
||||
}
|
||||
|
||||
public ProjectContext(Path projectRoot) {
|
||||
this();
|
||||
this.projectRoot = projectRoot;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Dependency information class
|
||||
*/
|
||||
public static class DependencyInfo {
|
||||
private String name;
|
||||
private String version;
|
||||
private String type; // "compile", "test", "runtime", etc.
|
||||
private String scope;
|
||||
private boolean isDirectDependency;
|
||||
|
||||
public DependencyInfo(String name, String version, String type) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
this.type = type;
|
||||
this.isDirectDependency = true;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getVersion() { return version; }
|
||||
public void setVersion(String version) { this.version = version; }
|
||||
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
|
||||
public String getScope() { return scope; }
|
||||
public void setScope(String scope) { this.scope = scope; }
|
||||
|
||||
public boolean isDirectDependency() { return isDirectDependency; }
|
||||
public void setDirectDependency(boolean directDependency) { isDirectDependency = directDependency; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s:%s (%s)", name, version, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration file information class
|
||||
*/
|
||||
public static class ConfigFile {
|
||||
private String fileName;
|
||||
private String relativePath;
|
||||
private String fileType; // "properties", "yaml", "json", "xml", etc.
|
||||
private Map<String, Object> keySettings;
|
||||
private boolean isMainConfig;
|
||||
|
||||
public ConfigFile(String fileName, String relativePath, String fileType) {
|
||||
this.fileName = fileName;
|
||||
this.relativePath = relativePath;
|
||||
this.fileType = fileType;
|
||||
this.keySettings = new HashMap<>();
|
||||
this.isMainConfig = false;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getFileName() { return fileName; }
|
||||
public void setFileName(String fileName) { this.fileName = fileName; }
|
||||
|
||||
public String getRelativePath() { return relativePath; }
|
||||
public void setRelativePath(String relativePath) { this.relativePath = relativePath; }
|
||||
|
||||
public String getFileType() { return fileType; }
|
||||
public void setFileType(String fileType) { this.fileType = fileType; }
|
||||
|
||||
public Map<String, Object> getKeySettings() { return keySettings; }
|
||||
public void setKeySettings(Map<String, Object> keySettings) { this.keySettings = keySettings; }
|
||||
|
||||
public boolean isMainConfig() { return isMainConfig; }
|
||||
public void setMainConfig(boolean mainConfig) { isMainConfig = mainConfig; }
|
||||
|
||||
public void addSetting(String key, Object value) {
|
||||
this.keySettings.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Code statistics information class
|
||||
*/
|
||||
public static class CodeStatistics {
|
||||
private int totalLines;
|
||||
private int codeLines;
|
||||
private int commentLines;
|
||||
private int blankLines;
|
||||
private Map<String, Integer> languageLines;
|
||||
private int totalClasses;
|
||||
private int totalMethods;
|
||||
private int totalFunctions;
|
||||
|
||||
public CodeStatistics() {
|
||||
this.languageLines = new HashMap<>();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public int getTotalLines() { return totalLines; }
|
||||
public void setTotalLines(int totalLines) { this.totalLines = totalLines; }
|
||||
|
||||
public int getCodeLines() { return codeLines; }
|
||||
public void setCodeLines(int codeLines) { this.codeLines = codeLines; }
|
||||
|
||||
public int getCommentLines() { return commentLines; }
|
||||
public void setCommentLines(int commentLines) { this.commentLines = commentLines; }
|
||||
|
||||
public int getBlankLines() { return blankLines; }
|
||||
public void setBlankLines(int blankLines) { this.blankLines = blankLines; }
|
||||
|
||||
public Map<String, Integer> getLanguageLines() { return languageLines; }
|
||||
public void setLanguageLines(Map<String, Integer> languageLines) { this.languageLines = languageLines; }
|
||||
|
||||
public int getTotalClasses() { return totalClasses; }
|
||||
public void setTotalClasses(int totalClasses) { this.totalClasses = totalClasses; }
|
||||
|
||||
public int getTotalMethods() { return totalMethods; }
|
||||
public void setTotalMethods(int totalMethods) { this.totalMethods = totalMethods; }
|
||||
|
||||
public int getTotalFunctions() { return totalFunctions; }
|
||||
public void setTotalFunctions(int totalFunctions) { this.totalFunctions = totalFunctions; }
|
||||
|
||||
public void addLanguageLines(String language, int lines) {
|
||||
this.languageLines.put(language, this.languageLines.getOrDefault(language, 0) + lines);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate project context summary
|
||||
*/
|
||||
public String generateContextSummary() {
|
||||
StringBuilder summary = new StringBuilder();
|
||||
|
||||
// Basic information
|
||||
summary.append("=== PROJECT CONTEXT ===\n");
|
||||
summary.append("Project: ").append(projectRoot != null ? projectRoot.getFileName() : "Unknown").append("\n");
|
||||
summary.append("Type: ").append(projectType != null ? projectType.getDisplayName() : "Unknown").append("\n");
|
||||
summary.append("Language: ").append(projectType != null ? projectType.getPrimaryLanguage() : "Unknown").append("\n");
|
||||
summary.append("Package Manager: ").append(projectType != null ? projectType.getPackageManager() : "Unknown").append("\n\n");
|
||||
|
||||
// Project structure
|
||||
if (projectStructure != null) {
|
||||
summary.append("=== PROJECT STRUCTURE ===\n");
|
||||
summary.append(projectStructure.getStructureSummary()).append("\n");
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
if (!dependencies.isEmpty()) {
|
||||
summary.append("=== DEPENDENCIES ===\n");
|
||||
dependencies.stream()
|
||||
.filter(DependencyInfo::isDirectDependency)
|
||||
.limit(10)
|
||||
.forEach(dep -> summary.append("- ").append(dep.toString()).append("\n"));
|
||||
if (dependencies.size() > 10) {
|
||||
summary.append("... and ").append(dependencies.size() - 10).append(" more dependencies\n");
|
||||
}
|
||||
summary.append("\n");
|
||||
}
|
||||
|
||||
// Configuration files
|
||||
if (!configFiles.isEmpty()) {
|
||||
summary.append("=== CONFIGURATION FILES ===\n");
|
||||
configFiles.stream()
|
||||
.filter(ConfigFile::isMainConfig)
|
||||
.forEach(config -> summary.append("- ").append(config.getFileName())
|
||||
.append(" (").append(config.getFileType()).append(")\n"));
|
||||
summary.append("\n");
|
||||
}
|
||||
|
||||
// Code statistics
|
||||
if (codeStatistics != null) {
|
||||
summary.append("=== CODE STATISTICS ===\n");
|
||||
summary.append("Total Lines: ").append(codeStatistics.getTotalLines()).append("\n");
|
||||
summary.append("Code Lines: ").append(codeStatistics.getCodeLines()).append("\n");
|
||||
if (codeStatistics.getTotalClasses() > 0) {
|
||||
summary.append("Classes: ").append(codeStatistics.getTotalClasses()).append("\n");
|
||||
}
|
||||
if (codeStatistics.getTotalMethods() > 0) {
|
||||
summary.append("Methods: ").append(codeStatistics.getTotalMethods()).append("\n");
|
||||
}
|
||||
summary.append("\n");
|
||||
}
|
||||
|
||||
this.contextSummary = summary.toString();
|
||||
return this.contextSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dependency summary
|
||||
*/
|
||||
public String getDependencySummary() {
|
||||
if (dependencies.isEmpty()) {
|
||||
return "No dependencies found";
|
||||
}
|
||||
|
||||
return dependencies.stream()
|
||||
.filter(DependencyInfo::isDirectDependency)
|
||||
.limit(5)
|
||||
.map(DependencyInfo::getName)
|
||||
.reduce((a, b) -> a + ", " + b)
|
||||
.orElse("No direct dependencies");
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Path getProjectRoot() { return projectRoot; }
|
||||
public void setProjectRoot(Path projectRoot) { this.projectRoot = projectRoot; }
|
||||
|
||||
public ProjectType getProjectType() { return projectType; }
|
||||
public void setProjectType(ProjectType projectType) { this.projectType = projectType; }
|
||||
|
||||
public ProjectStructure getProjectStructure() { return projectStructure; }
|
||||
public void setProjectStructure(ProjectStructure projectStructure) { this.projectStructure = projectStructure; }
|
||||
|
||||
public List<DependencyInfo> getDependencies() { return dependencies; }
|
||||
public void setDependencies(List<DependencyInfo> dependencies) { this.dependencies = dependencies; }
|
||||
|
||||
public List<ConfigFile> getConfigFiles() { return configFiles; }
|
||||
public void setConfigFiles(List<ConfigFile> configFiles) { this.configFiles = configFiles; }
|
||||
|
||||
public CodeStatistics getCodeStatistics() { return codeStatistics; }
|
||||
public void setCodeStatistics(CodeStatistics codeStatistics) { this.codeStatistics = codeStatistics; }
|
||||
|
||||
public Map<String, Object> getMetadata() { return metadata; }
|
||||
public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; }
|
||||
|
||||
public String getContextSummary() { return contextSummary; }
|
||||
public void setContextSummary(String contextSummary) { this.contextSummary = contextSummary; }
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Project structure information model
|
||||
* Contains project directory structure, file statistics and other information
|
||||
*/
|
||||
public class ProjectStructure {
|
||||
private Path projectRoot;
|
||||
private ProjectType projectType;
|
||||
private List<DirectoryInfo> directories;
|
||||
private Map<String, Integer> fileTypeCount;
|
||||
private List<String> keyFiles;
|
||||
private int totalFiles;
|
||||
private int totalDirectories;
|
||||
private long totalSize;
|
||||
|
||||
public ProjectStructure() {
|
||||
this.directories = new ArrayList<>();
|
||||
this.fileTypeCount = new HashMap<>();
|
||||
this.keyFiles = new ArrayList<>();
|
||||
}
|
||||
|
||||
public ProjectStructure(Path projectRoot, ProjectType projectType) {
|
||||
this();
|
||||
this.projectRoot = projectRoot;
|
||||
this.projectType = projectType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory information inner class
|
||||
*/
|
||||
public static class DirectoryInfo {
|
||||
private String name;
|
||||
private String relativePath;
|
||||
private int fileCount;
|
||||
private List<String> files;
|
||||
private boolean isImportant; // Whether it's an important directory (like src, test, etc.)
|
||||
|
||||
public DirectoryInfo(String name, String relativePath) {
|
||||
this.name = name;
|
||||
this.relativePath = relativePath;
|
||||
this.files = new ArrayList<>();
|
||||
this.isImportant = false;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getRelativePath() { return relativePath; }
|
||||
public void setRelativePath(String relativePath) { this.relativePath = relativePath; }
|
||||
|
||||
public int getFileCount() { return fileCount; }
|
||||
public void setFileCount(int fileCount) { this.fileCount = fileCount; }
|
||||
|
||||
public List<String> getFiles() { return files; }
|
||||
public void setFiles(List<String> files) { this.files = files; }
|
||||
|
||||
public boolean isImportant() { return isImportant; }
|
||||
public void setImportant(boolean important) { isImportant = important; }
|
||||
|
||||
public void addFile(String fileName) {
|
||||
this.files.add(fileName);
|
||||
this.fileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add directory information
|
||||
*/
|
||||
public void addDirectory(DirectoryInfo directoryInfo) {
|
||||
this.directories.add(directoryInfo);
|
||||
this.totalDirectories++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add file type statistics
|
||||
*/
|
||||
public void addFileType(String extension, int count) {
|
||||
this.fileTypeCount.put(extension, this.fileTypeCount.getOrDefault(extension, 0) + count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add key file
|
||||
*/
|
||||
public void addKeyFile(String fileName) {
|
||||
if (!this.keyFiles.contains(fileName)) {
|
||||
this.keyFiles.add(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project structure summary
|
||||
*/
|
||||
public String getStructureSummary() {
|
||||
StringBuilder summary = new StringBuilder();
|
||||
summary.append("Project: ").append(projectRoot != null ? projectRoot.getFileName() : "Unknown").append("\n");
|
||||
summary.append("Type: ").append(projectType != null ? projectType.getDisplayName() : "Unknown").append("\n");
|
||||
summary.append("Directories: ").append(totalDirectories).append("\n");
|
||||
summary.append("Files: ").append(totalFiles).append("\n");
|
||||
|
||||
if (!keyFiles.isEmpty()) {
|
||||
summary.append("Key Files: ").append(String.join(", ", keyFiles)).append("\n");
|
||||
}
|
||||
|
||||
if (!fileTypeCount.isEmpty()) {
|
||||
summary.append("File Types: ");
|
||||
fileTypeCount.entrySet().stream()
|
||||
.sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue()))
|
||||
.limit(5)
|
||||
.forEach(entry -> summary.append(entry.getKey()).append("(").append(entry.getValue()).append(") "));
|
||||
summary.append("\n");
|
||||
}
|
||||
|
||||
return summary.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get important directories list
|
||||
*/
|
||||
public List<DirectoryInfo> getImportantDirectories() {
|
||||
return directories.stream()
|
||||
.filter(DirectoryInfo::isImportant)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark important directories based on project type
|
||||
*/
|
||||
public void markImportantDirectories() {
|
||||
if (projectType == null) return;
|
||||
|
||||
for (DirectoryInfo dir : directories) {
|
||||
String dirName = dir.getName().toLowerCase();
|
||||
|
||||
// Common important directories
|
||||
if (dirName.equals("src") || dirName.equals("source") ||
|
||||
dirName.equals("test") || dirName.equals("tests") ||
|
||||
dirName.equals("config") || dirName.equals("conf") ||
|
||||
dirName.equals("docs") || dirName.equals("doc")) {
|
||||
dir.setImportant(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Project type specific important directories
|
||||
switch (projectType) {
|
||||
case JAVA_MAVEN:
|
||||
case JAVA_GRADLE:
|
||||
case SPRING_BOOT:
|
||||
if (dirName.equals("main") || dirName.equals("resources") ||
|
||||
dirName.equals("webapp") || dirName.equals("target") ||
|
||||
dirName.equals("build")) {
|
||||
dir.setImportant(true);
|
||||
}
|
||||
break;
|
||||
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
if (dirName.equals("node_modules") || dirName.equals("public") ||
|
||||
dirName.equals("dist") || dirName.equals("build") ||
|
||||
dirName.equals("components") || dirName.equals("pages")) {
|
||||
dir.setImportant(true);
|
||||
}
|
||||
break;
|
||||
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
if (dirName.equals("venv") || dirName.equals("env") ||
|
||||
dirName.equals("__pycache__") || dirName.equals("migrations") ||
|
||||
dirName.equals("static") || dirName.equals("templates")) {
|
||||
dir.setImportant(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Path getProjectRoot() { return projectRoot; }
|
||||
public void setProjectRoot(Path projectRoot) { this.projectRoot = projectRoot; }
|
||||
|
||||
public ProjectType getProjectType() { return projectType; }
|
||||
public void setProjectType(ProjectType projectType) { this.projectType = projectType; }
|
||||
|
||||
public List<DirectoryInfo> getDirectories() { return directories; }
|
||||
public void setDirectories(List<DirectoryInfo> directories) { this.directories = directories; }
|
||||
|
||||
public Map<String, Integer> getFileTypeCount() { return fileTypeCount; }
|
||||
public void setFileTypeCount(Map<String, Integer> fileTypeCount) { this.fileTypeCount = fileTypeCount; }
|
||||
|
||||
public List<String> getKeyFiles() { return keyFiles; }
|
||||
public void setKeyFiles(List<String> keyFiles) { this.keyFiles = keyFiles; }
|
||||
|
||||
public int getTotalFiles() { return totalFiles; }
|
||||
public void setTotalFiles(int totalFiles) { this.totalFiles = totalFiles; }
|
||||
|
||||
public int getTotalDirectories() { return totalDirectories; }
|
||||
public void setTotalDirectories(int totalDirectories) { this.totalDirectories = totalDirectories; }
|
||||
|
||||
public long getTotalSize() { return totalSize; }
|
||||
public void setTotalSize(long totalSize) { this.totalSize = totalSize; }
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
/**
|
||||
* Project type enumeration
|
||||
* Supports mainstream project type detection
|
||||
*/
|
||||
public enum ProjectType {
|
||||
// Java projects
|
||||
JAVA_MAVEN("Java Maven", "pom.xml", "Maven-based Java project"),
|
||||
JAVA_GRADLE("Java Gradle", "build.gradle", "Gradle-based Java project"),
|
||||
SPRING_BOOT("Spring Boot", "pom.xml", "Spring Boot application"),
|
||||
|
||||
// JavaScript/Node.js projects
|
||||
NODE_JS("Node.js", "package.json", "Node.js project"),
|
||||
REACT("React", "package.json", "React application"),
|
||||
VUE("Vue.js", "package.json", "Vue.js application"),
|
||||
ANGULAR("Angular", "package.json", "Angular application"),
|
||||
NEXT_JS("Next.js", "package.json", "Next.js application"),
|
||||
|
||||
// Python projects
|
||||
PYTHON("Python", "requirements.txt", "Python project"),
|
||||
DJANGO("Django", "manage.py", "Django web application"),
|
||||
FLASK("Flask", "app.py", "Flask web application"),
|
||||
FASTAPI("FastAPI", "main.py", "FastAPI application"),
|
||||
|
||||
// Other project types
|
||||
DOTNET("ASP.NET", "*.csproj", ".NET project"),
|
||||
GO("Go", "go.mod", "Go project"),
|
||||
RUST("Rust", "Cargo.toml", "Rust project"),
|
||||
PHP("PHP", "composer.json", "PHP project"),
|
||||
|
||||
// Web frontend
|
||||
HTML_STATIC("Static HTML", "index.html", "Static HTML website"),
|
||||
|
||||
// Unknown type
|
||||
UNKNOWN("Unknown", "", "Unknown project type");
|
||||
|
||||
private final String displayName;
|
||||
private final String keyFile;
|
||||
private final String description;
|
||||
|
||||
ProjectType(String displayName, String keyFile, String description) {
|
||||
this.displayName = displayName;
|
||||
this.keyFile = keyFile;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getKeyFile() {
|
||||
return keyFile;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a Java project
|
||||
*/
|
||||
public boolean isJavaProject() {
|
||||
return this == JAVA_MAVEN || this == JAVA_GRADLE || this == SPRING_BOOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a JavaScript project
|
||||
*/
|
||||
public boolean isJavaScriptProject() {
|
||||
return this == NODE_JS || this == REACT || this == VUE ||
|
||||
this == ANGULAR || this == NEXT_JS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a Python project
|
||||
*/
|
||||
public boolean isPythonProject() {
|
||||
return this == PYTHON || this == DJANGO || this == FLASK || this == FASTAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a Web project
|
||||
*/
|
||||
public boolean isWebProject() {
|
||||
return isJavaScriptProject() || this == HTML_STATIC ||
|
||||
this == DJANGO || this == FLASK || this == FASTAPI || this == SPRING_BOOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary programming language of the project
|
||||
*/
|
||||
public String getPrimaryLanguage() {
|
||||
if (isJavaProject()) return "Java";
|
||||
if (isJavaScriptProject()) return "JavaScript";
|
||||
if (isPythonProject()) return "Python";
|
||||
|
||||
switch (this) {
|
||||
case DOTNET: return "C#";
|
||||
case GO: return "Go";
|
||||
case RUST: return "Rust";
|
||||
case PHP: return "PHP";
|
||||
case HTML_STATIC: return "HTML";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recommended package manager
|
||||
*/
|
||||
public String getPackageManager() {
|
||||
switch (this) {
|
||||
case JAVA_MAVEN:
|
||||
case SPRING_BOOT:
|
||||
return "Maven";
|
||||
case JAVA_GRADLE:
|
||||
return "Gradle";
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
return "npm/yarn";
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
return "pip";
|
||||
case DOTNET:
|
||||
return "NuGet";
|
||||
case GO:
|
||||
return "go mod";
|
||||
case RUST:
|
||||
return "Cargo";
|
||||
case PHP:
|
||||
return "Composer";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class TaskStatus {
|
||||
private String taskId;
|
||||
private String status; // RUNNING, COMPLETED, ERROR
|
||||
private String currentAction;
|
||||
private String summary;
|
||||
private int currentTurn;
|
||||
private int totalEstimatedTurns;
|
||||
private long startTime;
|
||||
private long lastUpdateTime;
|
||||
private List<String> actionHistory;
|
||||
private String errorMessage;
|
||||
private double progressPercentage;
|
||||
|
||||
public TaskStatus(String taskId) {
|
||||
this.taskId = taskId;
|
||||
this.status = "RUNNING";
|
||||
this.startTime = System.currentTimeMillis();
|
||||
this.lastUpdateTime = this.startTime;
|
||||
this.actionHistory = new ArrayList<>();
|
||||
this.progressPercentage = 0.0;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getTaskId() { return taskId; }
|
||||
public void setTaskId(String taskId) { this.taskId = taskId; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
this.lastUpdateTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public String getCurrentAction() { return currentAction; }
|
||||
public void setCurrentAction(String currentAction) {
|
||||
this.currentAction = currentAction;
|
||||
this.lastUpdateTime = System.currentTimeMillis();
|
||||
if (currentAction != null && !currentAction.trim().isEmpty()) {
|
||||
this.actionHistory.add(currentAction);
|
||||
}
|
||||
}
|
||||
|
||||
public String getSummary() { return summary; }
|
||||
public void setSummary(String summary) { this.summary = summary; }
|
||||
|
||||
public int getCurrentTurn() { return currentTurn; }
|
||||
public void setCurrentTurn(int currentTurn) {
|
||||
this.currentTurn = currentTurn;
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
public int getTotalEstimatedTurns() { return totalEstimatedTurns; }
|
||||
public void setTotalEstimatedTurns(int totalEstimatedTurns) {
|
||||
this.totalEstimatedTurns = totalEstimatedTurns;
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
public long getStartTime() { return startTime; }
|
||||
public long getLastUpdateTime() { return lastUpdateTime; }
|
||||
|
||||
public List<String> getActionHistory() { return actionHistory; }
|
||||
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||
|
||||
public double getProgressPercentage() { return progressPercentage; }
|
||||
|
||||
private void updateProgress() {
|
||||
if (totalEstimatedTurns > 0) {
|
||||
this.progressPercentage = Math.min(100.0, (double) currentTurn / totalEstimatedTurns * 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
public long getElapsedTime() {
|
||||
return System.currentTimeMillis() - startTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.example.demo.schema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* JSON Schema definition class
|
||||
* Used to define tool parameter structure and validation rules
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class JsonSchema {
|
||||
|
||||
private String type;
|
||||
private String description;
|
||||
private String pattern;
|
||||
private Number minimum;
|
||||
private Number maximum;
|
||||
private List<Object> enumValues;
|
||||
|
||||
@JsonProperty("properties")
|
||||
private Map<String, JsonSchema> properties;
|
||||
|
||||
@JsonProperty("required")
|
||||
private List<String> requiredFields;
|
||||
|
||||
@JsonProperty("items")
|
||||
private JsonSchema items;
|
||||
|
||||
// Constructor
|
||||
public JsonSchema() {}
|
||||
|
||||
// Static factory methods
|
||||
public static JsonSchema object() {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "object";
|
||||
schema.properties = new HashMap<>();
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema string(String description) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "string";
|
||||
schema.description = description;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema number(String description) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "number";
|
||||
schema.description = description;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema integer(String description) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "integer";
|
||||
schema.description = description;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema bool(String description) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "boolean";
|
||||
schema.description = description;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema array(JsonSchema items) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "array";
|
||||
schema.items = items;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema array(String description, JsonSchema items) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "array";
|
||||
schema.description = description;
|
||||
schema.items = items;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// Fluent methods
|
||||
public JsonSchema addProperty(String name, JsonSchema property) {
|
||||
if (this.properties == null) {
|
||||
this.properties = new HashMap<>();
|
||||
}
|
||||
this.properties.put(name, property);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema required(String... fields) {
|
||||
this.requiredFields = Arrays.asList(fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema pattern(String pattern) {
|
||||
this.pattern = pattern;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema minimum(Number minimum) {
|
||||
this.minimum = minimum;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema maximum(Number maximum) {
|
||||
this.maximum = maximum;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema enumValues(Object... values) {
|
||||
this.enumValues = Arrays.asList(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public String getPattern() { return pattern; }
|
||||
public void setPattern(String pattern) { this.pattern = pattern; }
|
||||
|
||||
public Number getMinimum() { return minimum; }
|
||||
public void setMinimum(Number minimum) { this.minimum = minimum; }
|
||||
|
||||
public Number getMaximum() { return maximum; }
|
||||
public void setMaximum(Number maximum) { this.maximum = maximum; }
|
||||
|
||||
public List<Object> getEnumValues() { return enumValues; }
|
||||
public void setEnumValues(List<Object> enumValues) { this.enumValues = enumValues; }
|
||||
|
||||
public Map<String, JsonSchema> getProperties() { return properties; }
|
||||
public void setProperties(Map<String, JsonSchema> properties) { this.properties = properties; }
|
||||
|
||||
public List<String> getRequiredFields() { return requiredFields; }
|
||||
public void setRequiredFields(List<String> requiredFields) { this.requiredFields = requiredFields; }
|
||||
|
||||
public JsonSchema getItems() { return items; }
|
||||
public void setItems(JsonSchema items) { this.items = items; }
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.example.demo.schema;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.networknt.schema.JsonSchemaFactory;
|
||||
import com.networknt.schema.SpecVersion;
|
||||
import com.networknt.schema.ValidationMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JSON Schema validator
|
||||
* Used to validate tool parameters against defined schema
|
||||
*/
|
||||
@Component
|
||||
public class SchemaValidator {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SchemaValidator.class);
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final JsonSchemaFactory schemaFactory;
|
||||
|
||||
public SchemaValidator() {
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate data against schema
|
||||
*
|
||||
* @param schema JSON Schema definition
|
||||
* @param data Data to validate
|
||||
* @return Validation error message, null means validation passed
|
||||
*/
|
||||
public String validate(JsonSchema schema, Object data) {
|
||||
try {
|
||||
// Convert custom JsonSchema to standard JSON Schema string
|
||||
String schemaJson = objectMapper.writeValueAsString(schema);
|
||||
logger.debug("Schema JSON: {}", schemaJson);
|
||||
|
||||
// Create JSON Schema validator
|
||||
com.networknt.schema.JsonSchema jsonSchema = schemaFactory.getSchema(schemaJson);
|
||||
|
||||
// Convert data to JsonNode
|
||||
String dataJson = objectMapper.writeValueAsString(data);
|
||||
JsonNode dataNode = objectMapper.readTree(dataJson);
|
||||
logger.debug("Data JSON: {}", dataJson);
|
||||
|
||||
// Execute validation
|
||||
Set<ValidationMessage> errors = jsonSchema.validate(dataNode);
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
logger.debug("Schema validation passed");
|
||||
return null; // Validation passed
|
||||
} else {
|
||||
String errorMessage = errors.stream()
|
||||
.map(ValidationMessage::getMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
logger.warn("Schema validation failed: {}", errorMessage);
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
String errorMessage = "Schema validation error: " + e.getMessage();
|
||||
logger.error(errorMessage, e);
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple type validation (fallback solution)
|
||||
* Used when JSON Schema validation fails
|
||||
*/
|
||||
public String validateSimple(JsonSchema schema, Object data) {
|
||||
if (schema == null || data == null) {
|
||||
return "Schema or data is null";
|
||||
}
|
||||
|
||||
// Basic type checking
|
||||
String expectedType = schema.getType();
|
||||
if (expectedType != null) {
|
||||
String actualType = getDataType(data);
|
||||
if (!isTypeCompatible(expectedType, actualType)) {
|
||||
return String.format("Type mismatch: expected %s, got %s", expectedType, actualType);
|
||||
}
|
||||
}
|
||||
|
||||
// Required field checking (only for object type)
|
||||
if ("object".equals(expectedType) && schema.getRequiredFields() != null) {
|
||||
if (!(data instanceof java.util.Map)) {
|
||||
return "Expected object type for required field validation";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Map<String, Object> dataMap = (java.util.Map<String, Object>) data;
|
||||
|
||||
for (String requiredField : schema.getRequiredFields()) {
|
||||
if (!dataMap.containsKey(requiredField) || dataMap.get(requiredField) == null) {
|
||||
return "Missing required field: " + requiredField;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Validation passed
|
||||
}
|
||||
|
||||
private String getDataType(Object data) {
|
||||
if (data == null) return "null";
|
||||
if (data instanceof String) return "string";
|
||||
if (data instanceof Integer || data instanceof Long) return "integer";
|
||||
if (data instanceof Number) return "number";
|
||||
if (data instanceof Boolean) return "boolean";
|
||||
if (data instanceof java.util.List) return "array";
|
||||
if (data instanceof java.util.Map) return "object";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private boolean isTypeCompatible(String expectedType, String actualType) {
|
||||
if (expectedType.equals(actualType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Number type compatibility
|
||||
if ("number".equals(expectedType) && "integer".equals(actualType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.config.TaskContextHolder;
|
||||
import com.example.demo.model.TaskStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 连续对话服务
|
||||
*/
|
||||
@Service
|
||||
public class ContinuousConversationService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ContinuousConversationService.class);
|
||||
|
||||
private final ChatClient chatClient;
|
||||
private final NextSpeakerService nextSpeakerService;
|
||||
|
||||
@Autowired
|
||||
private LogStreamService logStreamService;
|
||||
|
||||
// 最大轮数限制,防止无限循环
|
||||
private static final int MAX_TURNS = 20;
|
||||
|
||||
// 单轮对话超时时间(毫秒)
|
||||
private static final long TURN_TIMEOUT_MS = 60_000; // 60秒
|
||||
|
||||
// 总对话超时时间(毫秒)
|
||||
private static final long TOTAL_TIMEOUT_MS = 10 * 60_000; // 10分钟
|
||||
|
||||
// 继续对话的提示语
|
||||
private static final String[] CONTINUE_PROMPTS = {
|
||||
"Continue with the next steps to complete the task.",
|
||||
"Please proceed with the remaining work.",
|
||||
"What's the next step? Please continue.",
|
||||
"Keep going with the task.",
|
||||
"Continue the implementation."
|
||||
};
|
||||
|
||||
// 在现有的ContinuousConversationService中添加以下改进
|
||||
|
||||
// 添加依赖注入
|
||||
private final TaskSummaryService taskSummaryService;
|
||||
private final Map<String, TaskStatus> taskStatusMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, ConversationResult> conversationResults = new ConcurrentHashMap<>();
|
||||
|
||||
// 修改构造函数
|
||||
public ContinuousConversationService(ChatClient chatClient,
|
||||
NextSpeakerService nextSpeakerService,
|
||||
TaskSummaryService taskSummaryService) {
|
||||
this.chatClient = chatClient;
|
||||
this.nextSpeakerService = nextSpeakerService;
|
||||
this.taskSummaryService = taskSummaryService;
|
||||
}
|
||||
|
||||
// 添加任务状态管理方法
|
||||
public TaskStatus getTaskStatus(String taskId) {
|
||||
return taskStatusMap.get(taskId);
|
||||
}
|
||||
|
||||
// 获取对话结果
|
||||
public ConversationResult getConversationResult(String taskId) {
|
||||
return conversationResults.get(taskId);
|
||||
}
|
||||
|
||||
// 存储对话结果
|
||||
private void storeConversationResult(String taskId, ConversationResult result) {
|
||||
conversationResults.put(taskId, result);
|
||||
}
|
||||
|
||||
public String startTask(String initialMessage) {
|
||||
String taskId = UUID.randomUUID().toString();
|
||||
TaskStatus status = new TaskStatus(taskId);
|
||||
|
||||
// 估算任务复杂度
|
||||
int estimatedTurns = taskSummaryService.estimateTaskComplexity(initialMessage);
|
||||
status.setTotalEstimatedTurns(estimatedTurns);
|
||||
status.setCurrentAction("开始分析任务...");
|
||||
|
||||
taskStatusMap.put(taskId, status);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能判断用户消息是否可能需要工具调用
|
||||
* 用于决定是否使用异步模式和显示工具执行状态
|
||||
*/
|
||||
public boolean isLikelyToNeedTools(String message) {
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String lowerMessage = message.toLowerCase().trim();
|
||||
|
||||
// 明确的简单对话模式 - 不需要工具
|
||||
String[] simplePatterns = {
|
||||
"你好", "hello", "hi", "嗨", "哈喽",
|
||||
"谢谢", "thank you", "thanks", "感谢",
|
||||
"再见", "goodbye", "bye", "拜拜",
|
||||
"好的", "ok", "okay", "行", "可以",
|
||||
"不用了", "算了", "没事", "不需要",
|
||||
"怎么样", "如何", "什么意思", "是什么",
|
||||
"介绍一下", "解释一下", "说明一下"
|
||||
};
|
||||
|
||||
// 检查是否是简单问候或确认
|
||||
for (String pattern : simplePatterns) {
|
||||
if (lowerMessage.equals(pattern) ||
|
||||
(lowerMessage.length() <= 10 && lowerMessage.contains(pattern))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 明确需要工具的关键词
|
||||
String[] toolRequiredPatterns = {
|
||||
"创建", "create", "新建", "生成", "建立",
|
||||
"编辑", "edit", "修改", "更新", "改变",
|
||||
"删除", "delete", "移除", "清除",
|
||||
"文件", "file", "目录", "folder", "项目", "project",
|
||||
"代码", "code", "程序", "script", "函数", "function",
|
||||
"分析", "analyze", "检查", "查看", "读取", "read",
|
||||
"写入", "write", "保存", "save",
|
||||
"搜索", "search", "查找", "find",
|
||||
"下载", "download", "获取", "fetch",
|
||||
"安装", "install", "配置", "config",
|
||||
"运行", "run", "执行", "execute",
|
||||
"测试", "test", "调试", "debug"
|
||||
};
|
||||
|
||||
// 检查是否包含工具相关关键词
|
||||
for (String pattern : toolRequiredPatterns) {
|
||||
if (lowerMessage.contains(pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 基于消息长度和复杂度的启发式判断
|
||||
// 长消息更可能需要工具处理
|
||||
if (message.length() > 50) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 包含路径、URL、代码片段等的消息
|
||||
if (lowerMessage.contains("/") || lowerMessage.contains("\\") ||
|
||||
lowerMessage.contains("http") || lowerMessage.contains("www") ||
|
||||
lowerMessage.contains("{") || lowerMessage.contains("}") ||
|
||||
lowerMessage.contains("<") || lowerMessage.contains(">")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 默认情况下,对于不确定的消息,倾向于不使用工具
|
||||
// 这样可以避免不必要的工具准备状态显示
|
||||
return false;
|
||||
}
|
||||
|
||||
// 修改executeContinuousConversation方法
|
||||
public ConversationResult executeContinuousConversation(String taskId, String initialMessage, List<Message> conversationHistory) {
|
||||
TaskStatus taskStatus = taskStatusMap.get(taskId);
|
||||
if (taskStatus == null) {
|
||||
throw new IllegalArgumentException("Task not found: " + taskId);
|
||||
}
|
||||
|
||||
// 设置任务上下文,供AOP切面使用
|
||||
TaskContextHolder.setCurrentTaskId(taskId);
|
||||
|
||||
long conversationStartTime = System.currentTimeMillis();
|
||||
logger.info("Starting continuous conversation with message: {}", initialMessage);
|
||||
|
||||
// 更新任务状态
|
||||
taskStatus.setCurrentAction("开始处理对话...");
|
||||
taskStatus.setCurrentTurn(0);
|
||||
|
||||
// 创建工作副本
|
||||
List<Message> workingHistory = new ArrayList<>(conversationHistory);
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
List<String> turnResponses = new ArrayList<>();
|
||||
|
||||
// 添加初始用户消息
|
||||
UserMessage userMessage = new UserMessage(initialMessage);
|
||||
workingHistory.add(userMessage);
|
||||
|
||||
int turnCount = 0;
|
||||
boolean shouldContinue = true;
|
||||
String stopReason = null;
|
||||
|
||||
try {
|
||||
while (shouldContinue && turnCount < MAX_TURNS) {
|
||||
turnCount++;
|
||||
logger.debug("Executing conversation turn: {}", turnCount);
|
||||
|
||||
// 更新任务状态
|
||||
taskStatus.setCurrentTurn(turnCount);
|
||||
taskStatus.setCurrentAction(String.format("执行第 %d 轮对话...", turnCount));
|
||||
|
||||
// 检查总超时
|
||||
long elapsedTime = System.currentTimeMillis() - conversationStartTime;
|
||||
if (elapsedTime > TOTAL_TIMEOUT_MS) {
|
||||
logger.warn("Conversation timed out after {}ms", elapsedTime);
|
||||
stopReason = "Total conversation timeout exceeded";
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
// 执行单轮对话
|
||||
TurnResult turnResult = executeSingleTurn(workingHistory, turnCount);
|
||||
|
||||
if (!turnResult.isSuccess()) {
|
||||
logger.error("Turn {} failed: {}", turnCount, turnResult.getErrorMessage());
|
||||
stopReason = "Turn execution failed: " + turnResult.getErrorMessage();
|
||||
break;
|
||||
}
|
||||
|
||||
// 添加响应到历史
|
||||
String responseText = turnResult.getResponse();
|
||||
if (responseText != null && !responseText.trim().isEmpty()) {
|
||||
AssistantMessage assistantMessage = new AssistantMessage(responseText);
|
||||
workingHistory.add(assistantMessage);
|
||||
|
||||
// 累积响应
|
||||
if (fullResponse.length() > 0) {
|
||||
fullResponse.append("\n\n");
|
||||
}
|
||||
fullResponse.append(responseText);
|
||||
turnResponses.add(responseText);
|
||||
|
||||
// 更新任务状态 - 显示当前响应的简短摘要
|
||||
String responseSummary = responseText.length() > 100 ?
|
||||
responseText.substring(0, 100) + "..." : responseText;
|
||||
taskStatus.setCurrentAction(String.format("第 %d 轮完成: %s", turnCount, responseSummary));
|
||||
}
|
||||
|
||||
// 判断是否应该继续
|
||||
taskStatus.setCurrentAction(String.format("分析第 %d 轮结果,判断是否继续...", turnCount));
|
||||
shouldContinue = shouldContinueConversation(workingHistory, turnCount, responseText);
|
||||
|
||||
if (shouldContinue && turnCount < MAX_TURNS) {
|
||||
// 添加继续提示
|
||||
String continuePrompt = getContinuePrompt(turnCount);
|
||||
UserMessage continueMessage = new UserMessage(continuePrompt);
|
||||
workingHistory.add(continueMessage);
|
||||
logger.debug("Added continue prompt for turn {}: {}", turnCount + 1, continuePrompt);
|
||||
taskStatus.setCurrentAction(String.format("准备第 %d 轮对话...", turnCount + 1));
|
||||
} else {
|
||||
taskStatus.setCurrentAction("对话即将结束...");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error in conversation turn {}: {}", turnCount, e.getMessage(), e);
|
||||
stopReason = "Exception in turn " + turnCount + ": " + e.getMessage();
|
||||
|
||||
// 添加错误信息到响应中
|
||||
String errorMessage = String.format("❌ Error in turn %d: %s", turnCount, e.getMessage());
|
||||
if (fullResponse.length() > 0) {
|
||||
fullResponse.append("\n\n");
|
||||
}
|
||||
fullResponse.append(errorMessage);
|
||||
turnResponses.add(errorMessage);
|
||||
|
||||
// 更新任务状态为错误
|
||||
taskStatus.setStatus("FAILED");
|
||||
taskStatus.setErrorMessage(e.getMessage());
|
||||
taskStatus.setCurrentAction("执行出错: " + e.getMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
long totalDuration = System.currentTimeMillis() - conversationStartTime;
|
||||
logger.info("Continuous conversation completed after {} turns in {}ms. Stop reason: {}",
|
||||
turnCount, totalDuration, stopReason);
|
||||
|
||||
// 创建结果对象
|
||||
ConversationResult result = new ConversationResult(
|
||||
fullResponse.toString(),
|
||||
turnResponses,
|
||||
workingHistory,
|
||||
turnCount,
|
||||
turnCount >= MAX_TURNS,
|
||||
stopReason,
|
||||
totalDuration
|
||||
);
|
||||
|
||||
// 更新任务状态为完成
|
||||
taskStatus.setStatus("COMPLETED");
|
||||
taskStatus.setCurrentAction("对话完成");
|
||||
String summary = String.format("对话完成,共 %d 轮,耗时 %.1f 秒",
|
||||
turnCount, totalDuration / 1000.0);
|
||||
if (stopReason != null) {
|
||||
summary += ",停止原因: " + stopReason;
|
||||
}
|
||||
taskStatus.setSummary(summary);
|
||||
|
||||
// 存储结果到任务状态中
|
||||
storeConversationResult(taskId, result);
|
||||
|
||||
// 推送任务完成事件
|
||||
logStreamService.pushTaskComplete(taskId);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 处理整个对话过程中的异常
|
||||
logger.error("Fatal error in continuous conversation: {}", e.getMessage(), e);
|
||||
taskStatus.setStatus("FAILED");
|
||||
taskStatus.setErrorMessage("Fatal error: " + e.getMessage());
|
||||
taskStatus.setCurrentAction("执行失败");
|
||||
throw e;
|
||||
} finally {
|
||||
// 清理任务上下文
|
||||
TaskContextHolder.clearCurrentTaskId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单轮对话
|
||||
*/
|
||||
private TurnResult executeSingleTurn(List<Message> conversationHistory, int turnNumber) {
|
||||
long turnStartTime = System.currentTimeMillis();
|
||||
try {
|
||||
logger.debug("Executing turn {} with {} messages in history", turnNumber, conversationHistory.size());
|
||||
|
||||
// 调用AI(这里可以添加超时控制,但Spring AI目前不直接支持)
|
||||
ChatResponse response = chatClient.prompt()
|
||||
.messages(conversationHistory)
|
||||
.call()
|
||||
.chatResponse();
|
||||
|
||||
// 处理响应
|
||||
Generation generation = response.getResult();
|
||||
AssistantMessage assistantMessage = generation.getOutput();
|
||||
String responseText = assistantMessage.getText();
|
||||
|
||||
long turnDuration = System.currentTimeMillis() - turnStartTime;
|
||||
logger.debug("Turn {} completed in {}ms, response length: {} characters",
|
||||
turnNumber, turnDuration, responseText != null ? responseText.length() : 0);
|
||||
|
||||
return new TurnResult(true, responseText, null);
|
||||
|
||||
} catch (Exception e) {
|
||||
long turnDuration = System.currentTimeMillis() - turnStartTime;
|
||||
logger.error("Failed to execute turn {} after {}ms: {}", turnNumber, turnDuration, e.getMessage(), e);
|
||||
return new TurnResult(false, null, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该继续对话 - 优化版本
|
||||
*/
|
||||
private boolean shouldContinueConversation(List<Message> conversationHistory, int turnCount, String lastResponse) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 达到最大轮数
|
||||
if (turnCount >= MAX_TURNS) {
|
||||
logger.debug("Reached maximum turns ({}), stopping conversation", MAX_TURNS);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 响应为空
|
||||
if (lastResponse == null || lastResponse.trim().isEmpty()) {
|
||||
logger.debug("Empty response, stopping conversation");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 优化:首先使用增强的内容分析判断
|
||||
boolean contentSuggestsContinue = nextSpeakerService.shouldContinueBasedOnContent(lastResponse);
|
||||
logger.debug("Content analysis result: {}", contentSuggestsContinue);
|
||||
|
||||
// 如果内容分析明确建议停止,直接停止(避免LLM调用)
|
||||
if (!contentSuggestsContinue) {
|
||||
logger.debug("Content analysis suggests stopping, skipping LLM check");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 优化:对于文件编辑等明确的工具调用场景,可以基于简单规则继续
|
||||
if (isObviousToolCallScenario(lastResponse, turnCount)) {
|
||||
logger.debug("Obvious tool call scenario detected, continuing without LLM check");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 只有在不确定的情况下才使用智能判断服务(包含LLM调用)
|
||||
try {
|
||||
NextSpeakerService.NextSpeakerResponse nextSpeaker =
|
||||
nextSpeakerService.checkNextSpeaker(conversationHistory);
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.debug("Next speaker check completed in {}ms, result: {}", duration, nextSpeaker);
|
||||
|
||||
return nextSpeaker.isModelNext();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to check next speaker, defaulting to stop: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是明显的工具调用场景
|
||||
*/
|
||||
private boolean isObviousToolCallScenario(String lastResponse, int turnCount) {
|
||||
if (lastResponse == null) return false;
|
||||
|
||||
String lowerResponse = lastResponse.toLowerCase();
|
||||
|
||||
// 如果是前几轮且包含工具调用成功的标志,很可能需要继续
|
||||
if (turnCount <= 10) {
|
||||
String[] toolCallIndicators = {
|
||||
"successfully created",
|
||||
"successfully updated",
|
||||
"file created",
|
||||
"file updated",
|
||||
"✅",
|
||||
"created file",
|
||||
"updated file",
|
||||
"next, i'll",
|
||||
"now i'll",
|
||||
"let me create",
|
||||
"let me edit"
|
||||
};
|
||||
|
||||
for (String indicator : toolCallIndicators) {
|
||||
if (lowerResponse.contains(indicator)) {
|
||||
// 但如果同时包含明确的完成信号,则不继续
|
||||
String[] completionSignals = {
|
||||
"all files created", "project complete", "setup complete",
|
||||
"everything is ready", "task completed", "all done"
|
||||
};
|
||||
|
||||
boolean hasCompletionSignal = false;
|
||||
for (String signal : completionSignals) {
|
||||
if (lowerResponse.contains(signal)) {
|
||||
hasCompletionSignal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCompletionSignal) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取继续对话的提示语
|
||||
*/
|
||||
private String getContinuePrompt(int turnNumber) {
|
||||
int index = (turnNumber - 1) % CONTINUE_PROMPTS.length;
|
||||
return CONTINUE_PROMPTS[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 单轮对话结果
|
||||
*/
|
||||
public static class TurnResult {
|
||||
private final boolean success;
|
||||
private final String response;
|
||||
private final String errorMessage;
|
||||
|
||||
public TurnResult(boolean success, String response, String errorMessage) {
|
||||
this.success = success;
|
||||
this.response = response;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public boolean isSuccess() { return success; }
|
||||
public String getResponse() { return response; }
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 连续对话结果
|
||||
*/
|
||||
public static class ConversationResult {
|
||||
private final String fullResponse;
|
||||
private final List<String> turnResponses;
|
||||
private final List<Message> finalHistory;
|
||||
private final int totalTurns;
|
||||
private final boolean reachedMaxTurns;
|
||||
private final String stopReason;
|
||||
private final long totalDurationMs;
|
||||
|
||||
public ConversationResult(String fullResponse, List<String> turnResponses,
|
||||
List<Message> finalHistory, int totalTurns, boolean reachedMaxTurns,
|
||||
String stopReason, long totalDurationMs) {
|
||||
this.fullResponse = fullResponse;
|
||||
this.turnResponses = turnResponses;
|
||||
this.finalHistory = finalHistory;
|
||||
this.totalTurns = totalTurns;
|
||||
this.reachedMaxTurns = reachedMaxTurns;
|
||||
this.stopReason = stopReason;
|
||||
this.totalDurationMs = totalDurationMs;
|
||||
}
|
||||
|
||||
public String getFullResponse() { return fullResponse; }
|
||||
public List<String> getTurnResponses() { return turnResponses; }
|
||||
public List<Message> getFinalHistory() { return finalHistory; }
|
||||
public int getTotalTurns() { return totalTurns; }
|
||||
public boolean isReachedMaxTurns() { return reachedMaxTurns; }
|
||||
public String getStopReason() { return stopReason; }
|
||||
public long getTotalDurationMs() { return totalDurationMs; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
/**
|
||||
* 日志事件基类
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class LogEvent {
|
||||
|
||||
private String type;
|
||||
private String taskId;
|
||||
private String message;
|
||||
private String timestamp;
|
||||
|
||||
// Constructors
|
||||
public LogEvent() {}
|
||||
|
||||
public LogEvent(String type, String taskId, String message, String timestamp) {
|
||||
this.type = type;
|
||||
this.taskId = taskId;
|
||||
this.message = message;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
// Static factory methods
|
||||
public static LogEvent createConnectionEvent(String taskId) {
|
||||
LogEvent event = new LogEvent();
|
||||
event.setType("CONNECTION_ESTABLISHED");
|
||||
event.setTaskId(taskId);
|
||||
event.setMessage("SSE连接已建立");
|
||||
event.setTimestamp(java.time.LocalDateTime.now().format(
|
||||
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
return event;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(String timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LogEvent{" +
|
||||
"type='" + type + '\'' +
|
||||
", taskId='" + taskId + '\'' +
|
||||
", message='" + message + '\'' +
|
||||
", timestamp='" + timestamp + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* SSE日志推送服务
|
||||
* 负责将AOP日志实时推送到前端
|
||||
*/
|
||||
@Service
|
||||
public class LogStreamService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LogStreamService.class);
|
||||
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
// 活跃的SSE连接 taskId -> SseEmitter
|
||||
private final Map<String, SseEmitter> activeConnections = new ConcurrentHashMap<>();
|
||||
|
||||
// JSON序列化器
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 建立SSE连接
|
||||
*/
|
||||
public SseEmitter createConnection(String taskId) {
|
||||
logger.info("🔗 建立SSE连接: taskId={}", taskId);
|
||||
|
||||
SseEmitter emitter = new SseEmitter(0L); // 无超时
|
||||
|
||||
// 设置连接事件处理
|
||||
emitter.onCompletion(() -> {
|
||||
logger.info("✅ SSE连接完成: taskId={}", taskId);
|
||||
activeConnections.remove(taskId);
|
||||
});
|
||||
|
||||
emitter.onTimeout(() -> {
|
||||
logger.warn("⏰ SSE连接超时: taskId={}", taskId);
|
||||
activeConnections.remove(taskId);
|
||||
});
|
||||
|
||||
emitter.onError((ex) -> {
|
||||
logger.error("❌ SSE连接错误: taskId={}, error={}", taskId, ex.getMessage());
|
||||
activeConnections.remove(taskId);
|
||||
});
|
||||
|
||||
// 保存连接
|
||||
activeConnections.put(taskId, emitter);
|
||||
|
||||
// 发送连接成功消息
|
||||
sendLogEvent(taskId, LogEvent.createConnectionEvent(taskId));
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭SSE连接
|
||||
*/
|
||||
public void closeConnection(String taskId) {
|
||||
SseEmitter emitter = activeConnections.remove(taskId);
|
||||
if (emitter != null) {
|
||||
try {
|
||||
emitter.complete();
|
||||
logger.info("🔚 关闭SSE连接: taskId={}", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.error("关闭SSE连接失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送工具开始执行事件
|
||||
*/
|
||||
public void pushToolStart(String taskId, String toolName, String filePath, String message) {
|
||||
ToolLogEvent event = new ToolLogEvent();
|
||||
event.setType("TOOL_START");
|
||||
event.setTaskId(taskId);
|
||||
event.setToolName(toolName);
|
||||
event.setFilePath(filePath);
|
||||
event.setMessage(message);
|
||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||
event.setIcon(getToolIcon(toolName));
|
||||
event.setStatus("RUNNING");
|
||||
|
||||
sendLogEvent(taskId, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送工具执行成功事件
|
||||
*/
|
||||
public void pushToolSuccess(String taskId, String toolName, String filePath, String message, long executionTime) {
|
||||
ToolLogEvent event = new ToolLogEvent();
|
||||
event.setType("TOOL_SUCCESS");
|
||||
event.setTaskId(taskId);
|
||||
event.setToolName(toolName);
|
||||
event.setFilePath(filePath);
|
||||
event.setMessage(message);
|
||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||
event.setIcon(getToolIcon(toolName));
|
||||
event.setStatus("SUCCESS");
|
||||
event.setExecutionTime(executionTime);
|
||||
|
||||
sendLogEvent(taskId, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送工具执行失败事件
|
||||
*/
|
||||
public void pushToolError(String taskId, String toolName, String filePath, String message, long executionTime) {
|
||||
ToolLogEvent event = new ToolLogEvent();
|
||||
event.setType("TOOL_ERROR");
|
||||
event.setTaskId(taskId);
|
||||
event.setToolName(toolName);
|
||||
event.setFilePath(filePath);
|
||||
event.setMessage(message);
|
||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||
event.setIcon("❌");
|
||||
event.setStatus("ERROR");
|
||||
event.setExecutionTime(executionTime);
|
||||
|
||||
sendLogEvent(taskId, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送任务完成事件
|
||||
*/
|
||||
public void pushTaskComplete(String taskId) {
|
||||
LogEvent event = new LogEvent();
|
||||
event.setType("TASK_COMPLETE");
|
||||
event.setTaskId(taskId);
|
||||
event.setMessage("任务执行完成");
|
||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||
|
||||
sendLogEvent(taskId, event);
|
||||
|
||||
// 延迟关闭连接
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(2000); // 等待2秒让前端处理完成事件
|
||||
closeConnection(taskId);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送日志事件到前端
|
||||
*/
|
||||
private void sendLogEvent(String taskId, Object event) {
|
||||
SseEmitter emitter = activeConnections.get(taskId);
|
||||
if (emitter != null) {
|
||||
try {
|
||||
String jsonData = objectMapper.writeValueAsString(event);
|
||||
logger.info("📤 准备推送日志事件: taskId={}, type={}, data={}", taskId,
|
||||
event instanceof LogEvent ? ((LogEvent) event).getType() : "unknown", jsonData);
|
||||
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("log")
|
||||
.data(jsonData));
|
||||
|
||||
logger.info("✅ 日志事件推送成功: taskId={}", taskId);
|
||||
} catch (IOException e) {
|
||||
logger.error("推送日志事件失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
activeConnections.remove(taskId);
|
||||
}
|
||||
} else {
|
||||
logger.warn("⚠️ 未找到SSE连接: taskId={}, 无法推送事件", taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具图标
|
||||
*/
|
||||
private String getToolIcon(String toolName) {
|
||||
switch (toolName) {
|
||||
case "readFile": return "📖";
|
||||
case "writeFile": return "✏️";
|
||||
case "editFile": return "📝";
|
||||
case "listDirectory": return "📁";
|
||||
case "analyzeProject": return "🔍";
|
||||
case "scaffoldProject": return "🏗️";
|
||||
case "smartEdit": return "🧠";
|
||||
default: return "⚙️";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃连接数
|
||||
*/
|
||||
public int getActiveConnectionCount() {
|
||||
return activeConnections.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 智能判断下一步发言者的服务
|
||||
*/
|
||||
@Service
|
||||
public class NextSpeakerService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(NextSpeakerService.class);
|
||||
|
||||
private final ChatModel chatModel;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public NextSpeakerService(ChatModel chatModel) {
|
||||
this.chatModel = chatModel;
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
private static final String CHECK_PROMPT = """
|
||||
Analyze *only* the content and structure of your immediately preceding response (your last turn in the conversation history).
|
||||
Based *strictly* on that response, determine who should logically speak next: the 'user' or the 'model' (you).
|
||||
|
||||
**Decision Rules (apply in order):**
|
||||
1. **Model Continues:** If your last response explicitly states an immediate next action *you* intend to take
|
||||
(e.g., "Next, I will...", "Now I'll process...", "Moving on to analyze...", "Let me create...", "I'll now...",
|
||||
indicates an intended tool call that didn't execute), OR if the response seems clearly incomplete
|
||||
(cut off mid-thought without a natural conclusion), then the **'model'** should speak next.
|
||||
2. **Question to User:** If your last response ends with a direct question specifically addressed *to the user*,
|
||||
then the **'user'** should speak next.
|
||||
3. **Waiting for User:** If your last response completed a thought, statement, or task *and* does not meet
|
||||
the criteria for Rule 1 (Model Continues) or Rule 2 (Question to User), it implies a pause expecting
|
||||
user input or reaction. In this case, the **'user'** should speak next.
|
||||
|
||||
**Output Format:**
|
||||
Respond *only* in JSON format. Do not include any text outside the JSON structure.
|
||||
""";
|
||||
|
||||
// 简化版本的检查提示,用于减少LLM调用开销
|
||||
private static final String SIMPLIFIED_CHECK_PROMPT = """
|
||||
Based on your last response, who should speak next: 'user' or 'model'?
|
||||
Rules: If you stated a next action or response is incomplete -> 'model'.
|
||||
If you asked user a question -> 'user'.
|
||||
If task completed -> 'user'.
|
||||
Respond in JSON: {"next_speaker": "user/model", "reasoning": "brief reason"}
|
||||
""";
|
||||
|
||||
/**
|
||||
* 判断下一步应该由谁发言 - 优化版本,添加快速路径
|
||||
*/
|
||||
public NextSpeakerResponse checkNextSpeaker(List<Message> conversationHistory) {
|
||||
try {
|
||||
// 确保有对话历史
|
||||
if (conversationHistory.isEmpty()) {
|
||||
return new NextSpeakerResponse("user", "No conversation history available");
|
||||
}
|
||||
|
||||
// 获取最后一条消息
|
||||
Message lastMessage = conversationHistory.get(conversationHistory.size() - 1);
|
||||
|
||||
// 如果最后一条不是助手消息,用户应该发言
|
||||
if (!(lastMessage instanceof AssistantMessage)) {
|
||||
return new NextSpeakerResponse("user", "Last message was not from assistant");
|
||||
}
|
||||
|
||||
// 检查是否是空响应
|
||||
String lastContent = lastMessage.getText();
|
||||
if (lastContent == null || lastContent.trim().isEmpty()) {
|
||||
return new NextSpeakerResponse("model", "Last message was empty, model should continue");
|
||||
}
|
||||
|
||||
// 快速路径1: 使用增强的内容分析进行快速判断
|
||||
NextSpeakerResponse fastPathResult = performFastPathCheck(lastContent);
|
||||
if (fastPathResult != null) {
|
||||
logger.debug("Fast path decision: {}", fastPathResult);
|
||||
return fastPathResult;
|
||||
}
|
||||
|
||||
// 快速路径2: 检查对话历史模式
|
||||
NextSpeakerResponse patternResult = checkConversationPattern(conversationHistory);
|
||||
if (patternResult != null) {
|
||||
logger.debug("Pattern-based decision: {}", patternResult);
|
||||
return patternResult;
|
||||
}
|
||||
|
||||
// 只有在快速路径无法确定时才使用LLM判断
|
||||
logger.debug("Fast paths inconclusive, falling back to LLM check");
|
||||
return performLLMCheck(conversationHistory);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to check next speaker, defaulting to user", e);
|
||||
return new NextSpeakerResponse("user", "Error occurred during check: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速路径检查 - 基于内容分析的快速判断
|
||||
*/
|
||||
private NextSpeakerResponse performFastPathCheck(String lastContent) {
|
||||
String lowerContent = lastContent.toLowerCase();
|
||||
|
||||
// 明确的停止信号 - 直接返回user
|
||||
String[] definiteStopSignals = {
|
||||
"task completed successfully",
|
||||
"all files created successfully",
|
||||
"project setup complete",
|
||||
"website is ready",
|
||||
"application is ready",
|
||||
"everything is ready",
|
||||
"setup is complete",
|
||||
"all tasks completed",
|
||||
"work is complete"
|
||||
};
|
||||
|
||||
for (String signal : definiteStopSignals) {
|
||||
if (lowerContent.contains(signal)) {
|
||||
return new NextSpeakerResponse("user", "Fast path: Definite completion signal detected");
|
||||
}
|
||||
}
|
||||
|
||||
// 明确的继续信号 - 直接返回model
|
||||
String[] definiteContinueSignals = {
|
||||
"next, i will",
|
||||
"now i will",
|
||||
"let me create",
|
||||
"let me edit",
|
||||
"let me update",
|
||||
"i'll create",
|
||||
"i'll edit",
|
||||
"i'll update",
|
||||
"moving on to",
|
||||
"proceeding to",
|
||||
"next step is to"
|
||||
};
|
||||
|
||||
for (String signal : definiteContinueSignals) {
|
||||
if (lowerContent.contains(signal)) {
|
||||
return new NextSpeakerResponse("model", "Fast path: Definite continue signal detected");
|
||||
}
|
||||
}
|
||||
|
||||
// 工具调用成功模式 - 通常需要继续
|
||||
if (isToolCallSuccessPattern(lastContent)) {
|
||||
// 但如果同时包含完成信号,则停止
|
||||
if (containsCompletionSignal(lowerContent)) {
|
||||
return new NextSpeakerResponse("user", "Fast path: Tool success with completion signal");
|
||||
}
|
||||
return new NextSpeakerResponse("model", "Fast path: Tool call success, should continue");
|
||||
}
|
||||
|
||||
// 直接问用户问题 - 应该等待用户回答
|
||||
if (lowerContent.trim().endsWith("?") && containsUserQuestion(lowerContent)) {
|
||||
return new NextSpeakerResponse("user", "Fast path: Direct question to user");
|
||||
}
|
||||
|
||||
return null; // 无法快速判断
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对话历史模式
|
||||
*/
|
||||
private NextSpeakerResponse checkConversationPattern(List<Message> conversationHistory) {
|
||||
if (conversationHistory.size() < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查最近几轮的模式
|
||||
int recentTurns = Math.min(4, conversationHistory.size());
|
||||
int modelTurns = 0;
|
||||
int userTurns = 0;
|
||||
|
||||
for (int i = conversationHistory.size() - recentTurns; i < conversationHistory.size(); i++) {
|
||||
Message msg = conversationHistory.get(i);
|
||||
if (msg instanceof AssistantMessage) {
|
||||
modelTurns++;
|
||||
} else if (msg instanceof UserMessage) {
|
||||
userTurns++;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果模型连续说话太多轮,可能需要用户介入
|
||||
if (modelTurns >= 3 && userTurns == 0) {
|
||||
return new NextSpeakerResponse("user", "Pattern: Too many consecutive model turns");
|
||||
}
|
||||
|
||||
return null; // 模式不明确
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含完成信号
|
||||
*/
|
||||
private boolean containsCompletionSignal(String lowerContent) {
|
||||
String[] completionSignals = {
|
||||
"all done", "complete", "finished", "ready", "that's it",
|
||||
"we're done", "task complete", "project complete"
|
||||
};
|
||||
|
||||
for (String signal : completionSignals) {
|
||||
if (lowerContent.contains(signal)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含对用户的直接问题
|
||||
*/
|
||||
private boolean containsUserQuestion(String lowerContent) {
|
||||
String[] userQuestionPatterns = {
|
||||
"what would you like",
|
||||
"what do you want",
|
||||
"would you like me to",
|
||||
"do you want me to",
|
||||
"should i",
|
||||
"would you prefer",
|
||||
"any preferences",
|
||||
"what's next",
|
||||
"what should i do next"
|
||||
};
|
||||
|
||||
for (String pattern : userQuestionPatterns) {
|
||||
if (lowerContent.contains(pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private NextSpeakerResponse performLLMCheck(List<Message> conversationHistory) {
|
||||
try {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 优化:只使用最近的对话历史,减少上下文长度
|
||||
List<Message> recentHistory = getRecentHistory(conversationHistory, 6); // 最多6条消息
|
||||
|
||||
// 创建用于判断的对话历史 - 简化版本
|
||||
List<Message> checkMessages = recentHistory.stream()
|
||||
.map(msg -> {
|
||||
if (msg instanceof UserMessage) {
|
||||
// 截断过长的用户消息
|
||||
String text = msg.getText();
|
||||
if (text.length() > 500) {
|
||||
text = text.substring(0, 500) + "...";
|
||||
}
|
||||
return new UserMessage(text);
|
||||
} else if (msg instanceof AssistantMessage) {
|
||||
// 截断过长的助手消息
|
||||
String text = msg.getText();
|
||||
if (text.length() > 500) {
|
||||
text = text.substring(0, 500) + "...";
|
||||
}
|
||||
return new AssistantMessage(text);
|
||||
}
|
||||
return msg;
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
// 添加简化的检查提示
|
||||
checkMessages.add(new UserMessage(SIMPLIFIED_CHECK_PROMPT));
|
||||
|
||||
// 使用输出转换器
|
||||
BeanOutputConverter<NextSpeakerResponse> outputConverter =
|
||||
new BeanOutputConverter<>(NextSpeakerResponse.class);
|
||||
|
||||
// 调用LLM - 这里可以考虑添加超时,但Spring AI目前不直接支持
|
||||
ChatResponse response = ChatClient.create(chatModel)
|
||||
.prompt()
|
||||
.messages(checkMessages)
|
||||
.call()
|
||||
.chatResponse();
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.debug("LLM check completed in {}ms", duration);
|
||||
|
||||
String responseText = response.getResult().getOutput().getText();
|
||||
logger.debug("Next speaker check response: {}", responseText);
|
||||
|
||||
// 解析响应
|
||||
try {
|
||||
return outputConverter.convert(responseText);
|
||||
} catch (Exception parseError) {
|
||||
logger.warn("Failed to parse next speaker response, trying manual parsing", parseError);
|
||||
return parseManually(responseText);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.warn("LLM check failed, defaulting to user: {}", e.getMessage());
|
||||
return new NextSpeakerResponse("user", "LLM check failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的对话历史
|
||||
*/
|
||||
private List<Message> getRecentHistory(List<Message> fullHistory, int maxMessages) {
|
||||
if (fullHistory.size() <= maxMessages) {
|
||||
return fullHistory;
|
||||
}
|
||||
|
||||
return fullHistory.subList(fullHistory.size() - maxMessages, fullHistory.size());
|
||||
}
|
||||
|
||||
private NextSpeakerResponse parseManually(String responseText) {
|
||||
try {
|
||||
// 简单的手动解析
|
||||
if (responseText.toLowerCase().contains("\"next_speaker\"") &&
|
||||
responseText.toLowerCase().contains("\"model\"")) {
|
||||
return new NextSpeakerResponse("model", "Parsed manually - model should continue");
|
||||
}
|
||||
return new NextSpeakerResponse("user", "Parsed manually - user should speak");
|
||||
} catch (Exception e) {
|
||||
return new NextSpeakerResponse("user", "Manual parsing failed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查响应内容是否表明需要继续 - 优化版本
|
||||
*/
|
||||
public boolean shouldContinueBasedOnContent(String response) {
|
||||
if (response == null || response.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String lowerResponse = response.toLowerCase();
|
||||
|
||||
// 优先检查明确的停止指示词 - 扩展版本
|
||||
String[] stopIndicators = {
|
||||
"completed", "finished", "done", "ready", "all set", "task complete",
|
||||
"project complete", "successfully created all", "that's it", "we're done",
|
||||
"everything is ready", "all files created", "project is ready",
|
||||
"task completed successfully", "all tasks completed", "work is complete",
|
||||
"implementation complete", "setup complete", "configuration complete",
|
||||
"files have been created", "project has been set up", "website is ready",
|
||||
"application is ready", "all necessary files", "setup is complete"
|
||||
};
|
||||
|
||||
// 检查停止指示词
|
||||
for (String indicator : stopIndicators) {
|
||||
if (lowerResponse.contains(indicator)) {
|
||||
logger.debug("Found stop indicator: '{}' in response", indicator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查工具调用成功的模式 - 新增:这是文件编辑场景的关键优化
|
||||
if (isToolCallSuccessPattern(response)) {
|
||||
logger.debug("Detected successful tool call pattern, should continue");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 扩展的继续指示词
|
||||
String[] continueIndicators = {
|
||||
"next, i", "now i", "let me", "i'll", "i will", "moving on",
|
||||
"proceeding", "continuing", "then i", "after that", "following this",
|
||||
"now let's", "let's now", "i need to", "i should", "i'm going to",
|
||||
"next step", "continuing with", "moving to", "proceeding to",
|
||||
"now creating", "now editing", "now updating", "now modifying",
|
||||
"let me create", "let me edit", "let me update", "let me modify",
|
||||
"i'll create", "i'll edit", "i'll update", "i'll modify",
|
||||
"creating the", "editing the", "updating the", "modifying the"
|
||||
};
|
||||
|
||||
// 检查继续指示词
|
||||
for (String indicator : continueIndicators) {
|
||||
if (lowerResponse.contains(indicator)) {
|
||||
logger.debug("Found continue indicator: '{}' in response", indicator);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否包含文件操作相关内容 - 针对文件编辑场景优化
|
||||
if (containsFileOperationIntent(lowerResponse)) {
|
||||
logger.debug("Detected file operation intent, should continue");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果响应很短且没有明确结束,可能需要继续
|
||||
boolean shortResponseContinue = response.length() < 200 && !lowerResponse.contains("?");
|
||||
if (shortResponseContinue) {
|
||||
logger.debug("Short response without question mark, should continue");
|
||||
}
|
||||
|
||||
return shortResponseContinue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是工具调用成功的模式
|
||||
*/
|
||||
private boolean isToolCallSuccessPattern(String response) {
|
||||
String lowerResponse = response.toLowerCase();
|
||||
|
||||
// 工具调用成功的典型模式
|
||||
String[] toolSuccessPatterns = {
|
||||
"successfully created",
|
||||
"successfully updated",
|
||||
"successfully modified",
|
||||
"successfully edited",
|
||||
"file created",
|
||||
"file updated",
|
||||
"file modified",
|
||||
"file edited",
|
||||
"created file",
|
||||
"updated file",
|
||||
"modified file",
|
||||
"edited file",
|
||||
"✅", // 成功标记
|
||||
"file has been created",
|
||||
"file has been updated",
|
||||
"file has been modified",
|
||||
"content has been",
|
||||
"successfully wrote",
|
||||
"successfully saved"
|
||||
};
|
||||
|
||||
for (String pattern : toolSuccessPatterns) {
|
||||
if (lowerResponse.contains(pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含文件操作意图
|
||||
*/
|
||||
private boolean containsFileOperationIntent(String lowerResponse) {
|
||||
String[] fileOperationIntents = {
|
||||
"create a", "create the", "creating a", "creating the",
|
||||
"edit a", "edit the", "editing a", "editing the",
|
||||
"update a", "update the", "updating a", "updating the",
|
||||
"modify a", "modify the", "modifying a", "modifying the",
|
||||
"write a", "write the", "writing a", "writing the",
|
||||
"generate a", "generate the", "generating a", "generating the",
|
||||
"add to", "adding to", "append to", "appending to",
|
||||
"need to create", "need to edit", "need to update", "need to modify",
|
||||
"will create", "will edit", "will update", "will modify",
|
||||
"going to create", "going to edit", "going to update", "going to modify"
|
||||
};
|
||||
|
||||
for (String intent : fileOperationIntents) {
|
||||
if (lowerResponse.contains(intent)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下一步发言者响应
|
||||
*/
|
||||
public static class NextSpeakerResponse {
|
||||
@JsonProperty("next_speaker")
|
||||
private String nextSpeaker;
|
||||
|
||||
@JsonProperty("reasoning")
|
||||
private String reasoning;
|
||||
|
||||
public NextSpeakerResponse() {}
|
||||
|
||||
public NextSpeakerResponse(String nextSpeaker, String reasoning) {
|
||||
this.nextSpeaker = nextSpeaker;
|
||||
this.reasoning = reasoning;
|
||||
}
|
||||
|
||||
public String getNextSpeaker() {
|
||||
return nextSpeaker;
|
||||
}
|
||||
|
||||
public void setNextSpeaker(String nextSpeaker) {
|
||||
this.nextSpeaker = nextSpeaker;
|
||||
}
|
||||
|
||||
public String getReasoning() {
|
||||
return reasoning;
|
||||
}
|
||||
|
||||
public void setReasoning(String reasoning) {
|
||||
this.reasoning = reasoning;
|
||||
}
|
||||
|
||||
public boolean isModelNext() {
|
||||
return "model".equals(nextSpeaker);
|
||||
}
|
||||
|
||||
public boolean isUserNext() {
|
||||
return "user".equals(nextSpeaker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("NextSpeaker{speaker='%s', reasoning='%s'}", nextSpeaker, reasoning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectContext;
|
||||
import com.example.demo.model.ProjectStructure;
|
||||
import com.example.demo.model.ProjectType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 项目上下文分析器
|
||||
* 提供完整的项目分析功能,生成AI可理解的项目上下文
|
||||
*/
|
||||
@Service
|
||||
public class ProjectContextAnalyzer {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProjectContextAnalyzer.class);
|
||||
|
||||
@Autowired
|
||||
public ProjectTypeDetector projectTypeDetector;
|
||||
|
||||
@Autowired
|
||||
public ProjectDiscoveryService projectDiscoveryService;
|
||||
|
||||
/**
|
||||
* 分析项目并生成完整上下文
|
||||
* @param projectRoot 项目根目录
|
||||
* @return 项目上下文信息
|
||||
*/
|
||||
public ProjectContext analyzeProject(Path projectRoot) {
|
||||
logger.info("Starting comprehensive project analysis for: {}", projectRoot);
|
||||
|
||||
ProjectContext context = new ProjectContext(projectRoot);
|
||||
|
||||
try {
|
||||
// 1. 检测项目类型
|
||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||
context.setProjectType(projectType);
|
||||
logger.debug("Detected project type: {}", projectType);
|
||||
|
||||
// 2. 分析项目结构
|
||||
ProjectStructure structure = projectDiscoveryService.analyzeProjectStructure(projectRoot);
|
||||
context.setProjectStructure(structure);
|
||||
logger.debug("Analyzed project structure with {} directories",
|
||||
structure.getDirectories().size());
|
||||
|
||||
// 3. 分析依赖关系
|
||||
List<ProjectContext.DependencyInfo> dependencies =
|
||||
projectDiscoveryService.analyzeDependencies(projectRoot);
|
||||
context.setDependencies(dependencies);
|
||||
logger.debug("Found {} dependencies", dependencies.size());
|
||||
|
||||
// 4. 查找配置文件
|
||||
List<ProjectContext.ConfigFile> configFiles =
|
||||
projectDiscoveryService.findConfigurationFiles(projectRoot);
|
||||
context.setConfigFiles(configFiles);
|
||||
logger.debug("Found {} configuration files", configFiles.size());
|
||||
|
||||
// 5. 分析代码统计
|
||||
ProjectContext.CodeStatistics codeStats = analyzeCodeStatistics(projectRoot, projectType);
|
||||
context.setCodeStatistics(codeStats);
|
||||
logger.debug("Code statistics: {} total lines", codeStats.getTotalLines());
|
||||
|
||||
// 6. 收集项目元数据
|
||||
Map<String, Object> metadata = collectProjectMetadata(projectRoot, projectType);
|
||||
context.setMetadata(metadata);
|
||||
|
||||
// 7. 生成上下文摘要
|
||||
String summary = context.generateContextSummary();
|
||||
logger.debug("Generated context summary with {} characters", summary.length());
|
||||
|
||||
logger.info("Project analysis completed successfully for: {}", projectRoot);
|
||||
return context;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error during project analysis for: " + projectRoot, e);
|
||||
// 返回部分分析结果
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析代码统计信息
|
||||
*/
|
||||
private ProjectContext.CodeStatistics analyzeCodeStatistics(Path projectRoot, ProjectType projectType) {
|
||||
logger.debug("Analyzing code statistics for: {}", projectRoot);
|
||||
|
||||
ProjectContext.CodeStatistics stats = new ProjectContext.CodeStatistics();
|
||||
|
||||
try {
|
||||
analyzeCodeInDirectory(projectRoot, stats, projectType, 0, 3);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error analyzing code statistics", e);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归分析目录中的代码
|
||||
*/
|
||||
private void analyzeCodeInDirectory(Path directory, ProjectContext.CodeStatistics stats,
|
||||
ProjectType projectType, int currentDepth, int maxDepth) {
|
||||
if (currentDepth > maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Stream<Path> paths = Files.list(directory)) {
|
||||
paths.forEach(path -> {
|
||||
try {
|
||||
if (Files.isDirectory(path)) {
|
||||
String dirName = path.getFileName().toString();
|
||||
// 跳过不需要分析的目录
|
||||
if (!shouldSkipDirectory(dirName)) {
|
||||
analyzeCodeInDirectory(path, stats, projectType, currentDepth + 1, maxDepth);
|
||||
}
|
||||
} else if (Files.isRegularFile(path)) {
|
||||
analyzeCodeFile(path, stats, projectType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error processing path during code analysis: " + path, e);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error listing directory: " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析单个代码文件
|
||||
*/
|
||||
private void analyzeCodeFile(Path filePath, ProjectContext.CodeStatistics stats, ProjectType projectType) {
|
||||
String fileName = filePath.getFileName().toString();
|
||||
String extension = getFileExtension(fileName).toLowerCase();
|
||||
|
||||
// 只分析代码文件
|
||||
if (!isCodeFile(extension, projectType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(filePath);
|
||||
int totalLines = lines.size();
|
||||
int codeLines = 0;
|
||||
int commentLines = 0;
|
||||
int blankLines = 0;
|
||||
|
||||
for (String line : lines) {
|
||||
String trimmedLine = line.trim();
|
||||
if (trimmedLine.isEmpty()) {
|
||||
blankLines++;
|
||||
} else if (isCommentLine(trimmedLine, extension)) {
|
||||
commentLines++;
|
||||
} else {
|
||||
codeLines++;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
stats.setTotalLines(stats.getTotalLines() + totalLines);
|
||||
stats.setCodeLines(stats.getCodeLines() + codeLines);
|
||||
stats.setCommentLines(stats.getCommentLines() + commentLines);
|
||||
stats.setBlankLines(stats.getBlankLines() + blankLines);
|
||||
|
||||
// 按语言统计
|
||||
String language = getLanguageByExtension(extension);
|
||||
stats.addLanguageLines(language, totalLines);
|
||||
|
||||
// 分析类和方法(简单实现)
|
||||
if (extension.equals(".java")) {
|
||||
analyzeJavaFile(lines, stats);
|
||||
} else if (extension.equals(".js") || extension.equals(".ts")) {
|
||||
analyzeJavaScriptFile(lines, stats);
|
||||
} else if (extension.equals(".py")) {
|
||||
analyzePythonFile(lines, stats);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading file for code analysis: " + filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Java文件
|
||||
*/
|
||||
private void analyzeJavaFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
||||
for (String line : lines) {
|
||||
String trimmedLine = line.trim();
|
||||
if (trimmedLine.matches(".*\\bclass\\s+\\w+.*")) {
|
||||
stats.setTotalClasses(stats.getTotalClasses() + 1);
|
||||
}
|
||||
if (trimmedLine.matches(".*\\b(public|private|protected)\\s+.*\\s+\\w+\\s*\\(.*\\).*")) {
|
||||
stats.setTotalMethods(stats.getTotalMethods() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析JavaScript文件
|
||||
*/
|
||||
private void analyzeJavaScriptFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
||||
for (String line : lines) {
|
||||
String trimmedLine = line.trim();
|
||||
if (trimmedLine.matches(".*\\bfunction\\s+\\w+.*") ||
|
||||
trimmedLine.matches(".*\\w+\\s*:\\s*function.*") ||
|
||||
trimmedLine.matches(".*\\w+\\s*=\\s*\\(.*\\)\\s*=>.*")) {
|
||||
stats.setTotalFunctions(stats.getTotalFunctions() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Python文件
|
||||
*/
|
||||
private void analyzePythonFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
||||
for (String line : lines) {
|
||||
String trimmedLine = line.trim();
|
||||
if (trimmedLine.matches("^class\\s+\\w+.*:")) {
|
||||
stats.setTotalClasses(stats.getTotalClasses() + 1);
|
||||
}
|
||||
if (trimmedLine.matches("^def\\s+\\w+.*:")) {
|
||||
stats.setTotalFunctions(stats.getTotalFunctions() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集项目元数据
|
||||
*/
|
||||
private Map<String, Object> collectProjectMetadata(Path projectRoot, ProjectType projectType) {
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
|
||||
metadata.put("projectName", projectRoot.getFileName().toString());
|
||||
metadata.put("projectType", projectType.name());
|
||||
metadata.put("primaryLanguage", projectType.getPrimaryLanguage());
|
||||
metadata.put("packageManager", projectType.getPackageManager());
|
||||
metadata.put("analysisTimestamp", System.currentTimeMillis());
|
||||
|
||||
// 检查版本控制
|
||||
if (Files.exists(projectRoot.resolve(".git"))) {
|
||||
metadata.put("versionControl", "Git");
|
||||
}
|
||||
|
||||
// 检查CI/CD配置
|
||||
if (Files.exists(projectRoot.resolve(".github"))) {
|
||||
metadata.put("cicd", "GitHub Actions");
|
||||
} else if (Files.exists(projectRoot.resolve(".gitlab-ci.yml"))) {
|
||||
metadata.put("cicd", "GitLab CI");
|
||||
}
|
||||
|
||||
// 检查Docker支持
|
||||
if (Files.exists(projectRoot.resolve("Dockerfile"))) {
|
||||
metadata.put("containerization", "Docker");
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成编辑上下文
|
||||
*/
|
||||
public String buildEditContext(Path projectRoot, String editDescription) {
|
||||
logger.debug("Building edit context for: {}", projectRoot);
|
||||
|
||||
ProjectContext context = analyzeProject(projectRoot);
|
||||
|
||||
StringBuilder contextBuilder = new StringBuilder();
|
||||
contextBuilder.append("=== EDIT CONTEXT ===\n");
|
||||
contextBuilder.append("Edit Request: ").append(editDescription).append("\n\n");
|
||||
contextBuilder.append(context.generateContextSummary());
|
||||
|
||||
return contextBuilder.toString();
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private boolean shouldSkipDirectory(String dirName) {
|
||||
return dirName.equals(".git") || dirName.equals("node_modules") ||
|
||||
dirName.equals("target") || dirName.equals("build") ||
|
||||
dirName.equals("dist") || dirName.equals("__pycache__") ||
|
||||
dirName.startsWith(".");
|
||||
}
|
||||
|
||||
private String getFileExtension(String fileName) {
|
||||
int lastDot = fileName.lastIndexOf('.');
|
||||
return lastDot > 0 ? fileName.substring(lastDot) : "";
|
||||
}
|
||||
|
||||
private boolean isCodeFile(String extension, ProjectType projectType) {
|
||||
return extension.equals(".java") || extension.equals(".js") || extension.equals(".ts") ||
|
||||
extension.equals(".py") || extension.equals(".html") || extension.equals(".css") ||
|
||||
extension.equals(".jsx") || extension.equals(".tsx") || extension.equals(".vue") ||
|
||||
extension.equals(".go") || extension.equals(".rs") || extension.equals(".php") ||
|
||||
extension.equals(".cs") || extension.equals(".cpp") || extension.equals(".c");
|
||||
}
|
||||
|
||||
private boolean isCommentLine(String line, String extension) {
|
||||
switch (extension) {
|
||||
case ".java":
|
||||
case ".js":
|
||||
case ".ts":
|
||||
case ".jsx":
|
||||
case ".tsx":
|
||||
case ".css":
|
||||
return line.startsWith("//") || line.startsWith("/*") || line.startsWith("*");
|
||||
case ".py":
|
||||
return line.startsWith("#");
|
||||
case ".html":
|
||||
return line.startsWith("<!--");
|
||||
default:
|
||||
return line.startsWith("#") || line.startsWith("//");
|
||||
}
|
||||
}
|
||||
|
||||
private String getLanguageByExtension(String extension) {
|
||||
switch (extension) {
|
||||
case ".java": return "Java";
|
||||
case ".js": case ".jsx": return "JavaScript";
|
||||
case ".ts": case ".tsx": return "TypeScript";
|
||||
case ".py": return "Python";
|
||||
case ".html": return "HTML";
|
||||
case ".css": return "CSS";
|
||||
case ".vue": return "Vue";
|
||||
case ".go": return "Go";
|
||||
case ".rs": return "Rust";
|
||||
case ".php": return "PHP";
|
||||
case ".cs": return "C#";
|
||||
default: return "Other";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectContext;
|
||||
import com.example.demo.model.ProjectStructure;
|
||||
import com.example.demo.model.ProjectType;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 项目发现和分析服务
|
||||
* 负责分析项目结构、依赖关系和配置信息
|
||||
*/
|
||||
@Service
|
||||
public class ProjectDiscoveryService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProjectDiscoveryService.class);
|
||||
|
||||
@Autowired
|
||||
private ProjectTypeDetector projectTypeDetector;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 分析项目结构
|
||||
* @param projectRoot 项目根目录
|
||||
* @return 项目结构信息
|
||||
*/
|
||||
public ProjectStructure analyzeProjectStructure(Path projectRoot) {
|
||||
logger.debug("Analyzing project structure for: {}", projectRoot);
|
||||
|
||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||
ProjectStructure structure = new ProjectStructure(projectRoot, projectType);
|
||||
|
||||
try {
|
||||
analyzeDirectoryStructure(projectRoot, structure, 0, 3); // 最大深度3层
|
||||
structure.markImportantDirectories();
|
||||
|
||||
logger.info("Project structure analysis completed for: {}", projectRoot);
|
||||
return structure;
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Error analyzing project structure for: " + projectRoot, e);
|
||||
return structure; // 返回部分分析结果
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归分析目录结构
|
||||
*/
|
||||
private void analyzeDirectoryStructure(Path currentPath, ProjectStructure structure,
|
||||
int currentDepth, int maxDepth) throws IOException {
|
||||
if (currentDepth > maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Stream<Path> paths = Files.list(currentPath)) {
|
||||
paths.forEach(path -> {
|
||||
try {
|
||||
if (Files.isDirectory(path)) {
|
||||
String dirName = path.getFileName().toString();
|
||||
String relativePath = structure.getProjectRoot().relativize(path).toString();
|
||||
|
||||
// 跳过常见的忽略目录
|
||||
if (shouldIgnoreDirectory(dirName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProjectStructure.DirectoryInfo dirInfo =
|
||||
new ProjectStructure.DirectoryInfo(dirName, relativePath);
|
||||
|
||||
// 分析目录中的文件
|
||||
analyzeDirectoryFiles(path, dirInfo);
|
||||
structure.addDirectory(dirInfo);
|
||||
|
||||
// 递归分析子目录
|
||||
if (currentDepth < maxDepth) {
|
||||
analyzeDirectoryStructure(path, structure, currentDepth + 1, maxDepth);
|
||||
}
|
||||
|
||||
} else if (Files.isRegularFile(path)) {
|
||||
// 处理根目录下的文件
|
||||
String fileName = path.getFileName().toString();
|
||||
String extension = getFileExtension(fileName);
|
||||
|
||||
structure.addFileType(extension, 1);
|
||||
structure.setTotalFiles(structure.getTotalFiles() + 1);
|
||||
|
||||
// 检查是否为关键文件
|
||||
if (isKeyFile(fileName, structure.getProjectType())) {
|
||||
structure.addKeyFile(fileName);
|
||||
}
|
||||
|
||||
// 累计文件大小
|
||||
try {
|
||||
structure.setTotalSize(structure.getTotalSize() + Files.size(path));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not get size for file: {}", path);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error processing path: " + path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析目录中的文件
|
||||
*/
|
||||
private void analyzeDirectoryFiles(Path directory, ProjectStructure.DirectoryInfo dirInfo) {
|
||||
try (Stream<Path> files = Files.list(directory)) {
|
||||
files.filter(Files::isRegularFile)
|
||||
.forEach(file -> {
|
||||
String fileName = file.getFileName().toString();
|
||||
dirInfo.addFile(fileName);
|
||||
});
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error analyzing files in directory: {}", directory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析项目依赖
|
||||
*/
|
||||
public List<ProjectContext.DependencyInfo> analyzeDependencies(Path projectRoot) {
|
||||
logger.debug("Analyzing dependencies for: {}", projectRoot);
|
||||
|
||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||
|
||||
try {
|
||||
switch (projectType) {
|
||||
case JAVA_MAVEN:
|
||||
case SPRING_BOOT:
|
||||
dependencies.addAll(analyzeMavenDependencies(projectRoot));
|
||||
break;
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
dependencies.addAll(analyzeNpmDependencies(projectRoot));
|
||||
break;
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
dependencies.addAll(analyzePythonDependencies(projectRoot));
|
||||
break;
|
||||
default:
|
||||
logger.info("Dependency analysis not supported for project type: {}", projectType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error analyzing dependencies for: " + projectRoot, e);
|
||||
}
|
||||
|
||||
logger.info("Found {} dependencies for project: {}", dependencies.size(), projectRoot);
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Maven依赖
|
||||
*/
|
||||
private List<ProjectContext.DependencyInfo> analyzeMavenDependencies(Path projectRoot) {
|
||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||
Path pomFile = projectRoot.resolve("pom.xml");
|
||||
|
||||
if (!Files.exists(pomFile)) {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
try {
|
||||
String pomContent = Files.readString(pomFile);
|
||||
// 简单的XML解析 - 在实际项目中应该使用专门的XML解析器
|
||||
if (pomContent.contains("spring-boot-starter-web")) {
|
||||
dependencies.add(new ProjectContext.DependencyInfo(
|
||||
"spring-boot-starter-web", "auto", "compile"));
|
||||
}
|
||||
if (pomContent.contains("spring-boot-starter-data-jpa")) {
|
||||
dependencies.add(new ProjectContext.DependencyInfo(
|
||||
"spring-boot-starter-data-jpa", "auto", "compile"));
|
||||
}
|
||||
if (pomContent.contains("spring-boot-starter-test")) {
|
||||
dependencies.add(new ProjectContext.DependencyInfo(
|
||||
"spring-boot-starter-test", "auto", "test"));
|
||||
}
|
||||
// 可以添加更多依赖检测逻辑
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading pom.xml", e);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析NPM依赖
|
||||
*/
|
||||
private List<ProjectContext.DependencyInfo> analyzeNpmDependencies(Path projectRoot) {
|
||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||
Path packageJsonPath = projectRoot.resolve("package.json");
|
||||
|
||||
if (!Files.exists(packageJsonPath)) {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
try {
|
||||
String content = Files.readString(packageJsonPath);
|
||||
JsonNode packageJson = objectMapper.readTree(content);
|
||||
|
||||
// 分析生产依赖
|
||||
JsonNode deps = packageJson.get("dependencies");
|
||||
if (deps != null) {
|
||||
deps.fields().forEachRemaining(entry -> {
|
||||
dependencies.add(new ProjectContext.DependencyInfo(
|
||||
entry.getKey(), entry.getValue().asText(), "production"));
|
||||
});
|
||||
}
|
||||
|
||||
// 分析开发依赖
|
||||
JsonNode devDeps = packageJson.get("devDependencies");
|
||||
if (devDeps != null) {
|
||||
devDeps.fields().forEachRemaining(entry -> {
|
||||
ProjectContext.DependencyInfo depInfo = new ProjectContext.DependencyInfo(
|
||||
entry.getKey(), entry.getValue().asText(), "development");
|
||||
depInfo.setDirectDependency(true);
|
||||
dependencies.add(depInfo);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading package.json", e);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Python依赖
|
||||
*/
|
||||
private List<ProjectContext.DependencyInfo> analyzePythonDependencies(Path projectRoot) {
|
||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||
Path requirementsFile = projectRoot.resolve("requirements.txt");
|
||||
|
||||
if (Files.exists(requirementsFile)) {
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(requirementsFile);
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty() && !line.startsWith("#")) {
|
||||
String[] parts = line.split("==|>=|<=|>|<");
|
||||
String name = parts[0].trim();
|
||||
String version = parts.length > 1 ? parts[1].trim() : "latest";
|
||||
dependencies.add(new ProjectContext.DependencyInfo(name, version, "runtime"));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading requirements.txt", e);
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找配置文件
|
||||
*/
|
||||
public List<ProjectContext.ConfigFile> findConfigurationFiles(Path projectRoot) {
|
||||
logger.debug("Finding configuration files for: {}", projectRoot);
|
||||
|
||||
List<ProjectContext.ConfigFile> configFiles = new ArrayList<>();
|
||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||
|
||||
try {
|
||||
// 通用配置文件
|
||||
addConfigFileIfExists(configFiles, projectRoot, "application.properties", "properties");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "application.yml", "yaml");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "application.yaml", "yaml");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "config.json", "json");
|
||||
|
||||
// 项目类型特定的配置文件
|
||||
switch (projectType) {
|
||||
case JAVA_MAVEN:
|
||||
case SPRING_BOOT:
|
||||
addConfigFileIfExists(configFiles, projectRoot, "pom.xml", "xml");
|
||||
break;
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
addConfigFileIfExists(configFiles, projectRoot, "package.json", "json");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "webpack.config.js", "javascript");
|
||||
break;
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
addConfigFileIfExists(configFiles, projectRoot, "requirements.txt", "text");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "setup.py", "python");
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error finding configuration files for: " + projectRoot, e);
|
||||
}
|
||||
|
||||
logger.info("Found {} configuration files for project: {}", configFiles.size(), projectRoot);
|
||||
return configFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加配置文件(如果存在)
|
||||
*/
|
||||
private void addConfigFileIfExists(List<ProjectContext.ConfigFile> configFiles,
|
||||
Path projectRoot, String fileName, String fileType) {
|
||||
Path configPath = projectRoot.resolve(fileName);
|
||||
if (Files.exists(configPath)) {
|
||||
String relativePath = projectRoot.relativize(configPath).toString();
|
||||
ProjectContext.ConfigFile configFile =
|
||||
new ProjectContext.ConfigFile(fileName, relativePath, fileType);
|
||||
|
||||
// 标记主要配置文件
|
||||
if (fileName.equals("pom.xml") || fileName.equals("package.json") ||
|
||||
fileName.startsWith("application.")) {
|
||||
configFile.setMainConfig(true);
|
||||
}
|
||||
|
||||
configFiles.add(configFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该忽略目录
|
||||
*/
|
||||
private boolean shouldIgnoreDirectory(String dirName) {
|
||||
return dirName.equals(".git") || dirName.equals(".svn") ||
|
||||
dirName.equals("node_modules") || dirName.equals("target") ||
|
||||
dirName.equals("build") || dirName.equals("dist") ||
|
||||
dirName.equals("__pycache__") || dirName.equals(".idea") ||
|
||||
dirName.equals(".vscode") || dirName.startsWith(".");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
private String getFileExtension(String fileName) {
|
||||
int lastDot = fileName.lastIndexOf('.');
|
||||
return lastDot > 0 ? fileName.substring(lastDot) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为关键文件
|
||||
*/
|
||||
private boolean isKeyFile(String fileName, ProjectType projectType) {
|
||||
// 通用关键文件
|
||||
if (fileName.equals("README.md") || fileName.equals("LICENSE") ||
|
||||
fileName.equals("Dockerfile") || fileName.equals(".gitignore")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 项目类型特定的关键文件
|
||||
if (projectType != null) {
|
||||
String keyFile = projectType.getKeyFile();
|
||||
if (keyFile != null && !keyFile.isEmpty()) {
|
||||
return fileName.equals(keyFile) || fileName.matches(keyFile);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectType;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 项目模板扩展服务
|
||||
* 生成README、gitignore等通用文件模板
|
||||
*/
|
||||
@Service
|
||||
public class ProjectTemplateExtensions {
|
||||
|
||||
/**
|
||||
* 生成README.md内容
|
||||
*/
|
||||
public String generateReadmeContent(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
# %s
|
||||
|
||||
%s
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Java 17 or higher (for Java projects)
|
||||
- Node.js 16+ (for JavaScript projects)
|
||||
- Python 3.8+ (for Python projects)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd %s
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
# For Java Maven projects
|
||||
mvn clean install
|
||||
|
||||
# For Node.js projects
|
||||
npm install
|
||||
|
||||
# For Python projects
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Run the application:
|
||||
```bash
|
||||
# For Java Maven projects
|
||||
mvn spring-boot:run
|
||||
|
||||
# For Node.js projects
|
||||
npm start
|
||||
|
||||
# For Python projects
|
||||
python main.py
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
%s/
|
||||
├── src/ # Source code
|
||||
├── test/ # Test files
|
||||
├── docs/ # Documentation
|
||||
├── README.md # This file
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# For Java projects
|
||||
mvn test
|
||||
|
||||
# For Node.js projects
|
||||
npm test
|
||||
|
||||
# For Python projects
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# For Java projects
|
||||
mvn clean package
|
||||
|
||||
# For Node.js projects
|
||||
npm run build
|
||||
|
||||
# For Python projects
|
||||
python setup.py build
|
||||
```
|
||||
|
||||
## 📝 Features
|
||||
|
||||
- Feature 1: Description
|
||||
- Feature 2: Description
|
||||
- Feature 3: Description
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the project
|
||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 👥 Authors
|
||||
|
||||
- **%s** - *Initial work* - [%s](mailto:%s)
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Hat tip to anyone whose code was used
|
||||
- Inspiration
|
||||
- etc
|
||||
|
||||
---
|
||||
|
||||
Created with ❤️ by %s
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("DESCRIPTION"),
|
||||
variables.get("PROJECT_NAME"),
|
||||
variables.get("PROJECT_NAME"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("EMAIL"),
|
||||
variables.get("AUTHOR")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成.gitignore内容
|
||||
*/
|
||||
public String generateGitignoreContent(ProjectType projectType) {
|
||||
StringBuilder gitignore = new StringBuilder();
|
||||
|
||||
// 通用忽略规则
|
||||
gitignore.append("""
|
||||
# General
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
""");
|
||||
|
||||
// 项目类型特定的忽略规则
|
||||
switch (projectType) {
|
||||
case JAVA_MAVEN:
|
||||
case JAVA_GRADLE:
|
||||
case SPRING_BOOT:
|
||||
gitignore.append("""
|
||||
# Java
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.nar
|
||||
hs_err_pid*
|
||||
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# Spring Boot
|
||||
spring-boot-*.log
|
||||
|
||||
""");
|
||||
break;
|
||||
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
gitignore.append("""
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# parcel-bundler cache
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
""");
|
||||
break;
|
||||
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
gitignore.append("""
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
""");
|
||||
break;
|
||||
|
||||
default:
|
||||
// 基本忽略规则已经添加
|
||||
break;
|
||||
}
|
||||
|
||||
return gitignore.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectType;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 项目模板服务
|
||||
* 生成各种项目类型的模板文件内容
|
||||
*/
|
||||
@Service
|
||||
public class ProjectTemplateService {
|
||||
|
||||
/**
|
||||
* 生成Maven pom.xml
|
||||
*/
|
||||
public String generatePomXml(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>%s</artifactId>
|
||||
<version>%s</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>%s</name>
|
||||
<description>%s</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
""",
|
||||
variables.get("PROJECT_NAME"),
|
||||
variables.get("VERSION"),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("DESCRIPTION")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Spring Boot pom.xml
|
||||
*/
|
||||
public String generateSpringBootPomXml(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>%s</artifactId>
|
||||
<version>%s</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>%s</name>
|
||||
<description>%s</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
""",
|
||||
variables.get("PROJECT_NAME"),
|
||||
variables.get("VERSION"),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("DESCRIPTION")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Java主类
|
||||
*/
|
||||
public String generateJavaMainClass(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
package com.example.%s;
|
||||
|
||||
/**
|
||||
* Main application class for %s
|
||||
*
|
||||
* @author %s
|
||||
*/
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello from %s!");
|
||||
System.out.println("Application started successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application name
|
||||
* @return application name
|
||||
*/
|
||||
public String getApplicationName() {
|
||||
return "%s";
|
||||
}
|
||||
}
|
||||
""",
|
||||
variables.get("PROJECT_NAME").toLowerCase(),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("PROJECT_NAME_PASCAL")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Spring Boot主类
|
||||
*/
|
||||
public String generateSpringBootMainClass(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
package com.example.%s;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Spring Boot main application class for %s
|
||||
*
|
||||
* @author %s
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
""",
|
||||
variables.get("PROJECT_NAME").toLowerCase(),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("AUTHOR")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Spring Boot Controller
|
||||
*/
|
||||
public String generateSpringBootController(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
package com.example.%s.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Hello controller for %s
|
||||
*
|
||||
* @author %s
|
||||
*/
|
||||
@RestController
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String hello() {
|
||||
return "Hello from %s!";
|
||||
}
|
||||
|
||||
@GetMapping("/health")
|
||||
public String health() {
|
||||
return "OK";
|
||||
}
|
||||
}
|
||||
""",
|
||||
variables.get("PROJECT_NAME").toLowerCase(),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("PROJECT_NAME_PASCAL")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Java测试类
|
||||
*/
|
||||
public String generateJavaTestClass(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
package com.example.%s;
|
||||
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Test class for %s Application
|
||||
*
|
||||
* @author %s
|
||||
*/
|
||||
public class ApplicationTest {
|
||||
|
||||
@Test
|
||||
public void testApplicationName() {
|
||||
Application app = new Application();
|
||||
assertEquals("%s", app.getApplicationName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplicationCreation() {
|
||||
Application app = new Application();
|
||||
assertNotNull(app);
|
||||
}
|
||||
}
|
||||
""",
|
||||
variables.get("PROJECT_NAME").toLowerCase(),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("PROJECT_NAME_PASCAL")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成application.yml
|
||||
*/
|
||||
public String generateApplicationYml(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
# Application configuration for %s
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: %s
|
||||
profiles:
|
||||
active: dev
|
||||
|
||||
# Logging configuration
|
||||
logging:
|
||||
level:
|
||||
com.example.%s: DEBUG
|
||||
org.springframework: INFO
|
||||
pattern:
|
||||
console: "%%d{yyyy-MM-dd HH:mm:ss} - %%msg%%n"
|
||||
|
||||
# Management endpoints
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when-authorized
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("PROJECT_NAME"),
|
||||
variables.get("PROJECT_NAME").toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成package.json
|
||||
*/
|
||||
public String generatePackageJson(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
{
|
||||
"name": "%s",
|
||||
"version": "%s",
|
||||
"description": "%s",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "echo \\"Error: no test specified\\" && exit 1",
|
||||
"dev": "node index.js"
|
||||
},
|
||||
"keywords": [
|
||||
"nodejs",
|
||||
"%s"
|
||||
],
|
||||
"author": "%s <%s>",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
""",
|
||||
variables.get("PROJECT_NAME"),
|
||||
variables.get("VERSION"),
|
||||
variables.get("DESCRIPTION"),
|
||||
variables.get("PROJECT_NAME"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("EMAIL")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Node.js主文件
|
||||
*/
|
||||
public String generateNodeJsMainFile(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
/**
|
||||
* Main application file for %s
|
||||
*
|
||||
* @author %s
|
||||
*/
|
||||
|
||||
console.log('Hello from %s!');
|
||||
console.log('Node.js application started successfully.');
|
||||
|
||||
// Simple HTTP server example
|
||||
const http = require('http');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hello from %s!\\n');
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("PROJECT_NAME_PASCAL")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成React App.js
|
||||
*/
|
||||
public String generateReactAppJs(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
import React from 'react';
|
||||
import './App.css';
|
||||
|
||||
/**
|
||||
* Main App component for %s
|
||||
*
|
||||
* @author %s
|
||||
*/
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<h1>Welcome to %s</h1>
|
||||
<p>%s</p>
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("DESCRIPTION")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成React index.html
|
||||
*/
|
||||
public String generateReactIndexHtml(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%s</title>
|
||||
<meta name="description" content="%s">
|
||||
<meta name="author" content="%s">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("DESCRIPTION"),
|
||||
variables.get("AUTHOR")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Python主文件
|
||||
*/
|
||||
public String generatePythonMainFile(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
#!/usr/bin/env python3
|
||||
\"\"\"
|
||||
Main application file for %s
|
||||
|
||||
Author: %s
|
||||
\"\"\"
|
||||
|
||||
def main():
|
||||
\"\"\"Main function\"\"\"
|
||||
print("Hello from %s!")
|
||||
print("Python application started successfully.")
|
||||
|
||||
def get_application_name():
|
||||
\"\"\"Get application name\"\"\"
|
||||
return "%s"
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("PROJECT_NAME_PASCAL")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成requirements.txt
|
||||
*/
|
||||
public String generateRequirementsTxt(Map<String, String> variables) {
|
||||
return """
|
||||
# Python dependencies for """ + variables.get("PROJECT_NAME") + """
|
||||
# Add your dependencies here
|
||||
|
||||
# Example dependencies:
|
||||
# requests>=2.28.0
|
||||
# flask>=2.3.0
|
||||
# pytest>=7.0.0
|
||||
""";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成静态HTML index.html
|
||||
*/
|
||||
public String generateStaticIndexHtml(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%s</title>
|
||||
<meta name="description" content="%s">
|
||||
<meta name="author" content="%s">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Welcome to %s</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>About</h2>
|
||||
<p>%s</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Features</h2>
|
||||
<ul>
|
||||
<li>Modern HTML5 structure</li>
|
||||
<li>Responsive design</li>
|
||||
<li>Clean CSS styling</li>
|
||||
<li>JavaScript functionality</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© %s %s. All rights reserved.</p>
|
||||
</footer>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("DESCRIPTION"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("DESCRIPTION"),
|
||||
variables.get("CURRENT_YEAR"),
|
||||
variables.get("AUTHOR")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成基本CSS
|
||||
*/
|
||||
public String generateBasicCss(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
/* CSS styles for %s */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #35424a;
|
||||
color: white;
|
||||
padding: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #35424a;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: #35424a;
|
||||
color: white;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成基本JavaScript
|
||||
*/
|
||||
public String generateBasicJs(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
/**
|
||||
* JavaScript functionality for %s
|
||||
*
|
||||
* @author %s
|
||||
*/
|
||||
|
||||
// Wait for DOM to be fully loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('%s application loaded successfully!');
|
||||
|
||||
// Add click event to header
|
||||
const header = document.querySelector('header h1');
|
||||
if (header) {
|
||||
header.addEventListener('click', function() {
|
||||
alert('Welcome to %s!');
|
||||
});
|
||||
}
|
||||
|
||||
// Add smooth scrolling for anchor links
|
||||
const links = document.querySelectorAll('a[href^="#"]');
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Utility function to get application name
|
||||
*/
|
||||
function getApplicationName() {
|
||||
return '%s';
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to show notification
|
||||
*/
|
||||
function showNotification(message) {
|
||||
console.log('Notification:', message);
|
||||
// You can implement a proper notification system here
|
||||
}
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("PROJECT_NAME_PASCAL")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectType;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 项目类型检测器
|
||||
* 基于文件特征自动识别项目类型
|
||||
*/
|
||||
@Component
|
||||
public class ProjectTypeDetector {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProjectTypeDetector.class);
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 检测项目类型
|
||||
* @param projectRoot 项目根目录
|
||||
* @return 检测到的项目类型
|
||||
*/
|
||||
public ProjectType detectProjectType(Path projectRoot) {
|
||||
if (!Files.exists(projectRoot) || !Files.isDirectory(projectRoot)) {
|
||||
logger.warn("Project root does not exist or is not a directory: {}", projectRoot);
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("Detecting project type for: {}", projectRoot);
|
||||
|
||||
// 按优先级检测项目类型
|
||||
ProjectType detectedType = detectByKeyFiles(projectRoot);
|
||||
if (detectedType != ProjectType.UNKNOWN) {
|
||||
logger.info("Detected project type: {} for {}", detectedType, projectRoot);
|
||||
return detectedType;
|
||||
}
|
||||
|
||||
// 如果关键文件检测失败,尝试基于目录结构检测
|
||||
detectedType = detectByDirectoryStructure(projectRoot);
|
||||
if (detectedType != ProjectType.UNKNOWN) {
|
||||
logger.info("Detected project type by structure: {} for {}", detectedType, projectRoot);
|
||||
return detectedType;
|
||||
}
|
||||
|
||||
logger.info("Could not determine project type for: {}", projectRoot);
|
||||
return ProjectType.UNKNOWN;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error detecting project type for: " + projectRoot, e);
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于关键文件检测项目类型
|
||||
*/
|
||||
private ProjectType detectByKeyFiles(Path projectRoot) throws IOException {
|
||||
// Java Maven项目
|
||||
if (Files.exists(projectRoot.resolve("pom.xml"))) {
|
||||
// 检查是否为Spring Boot项目
|
||||
if (isSpringBootProject(projectRoot)) {
|
||||
return ProjectType.SPRING_BOOT;
|
||||
}
|
||||
return ProjectType.JAVA_MAVEN;
|
||||
}
|
||||
|
||||
// Java Gradle项目
|
||||
if (Files.exists(projectRoot.resolve("build.gradle")) ||
|
||||
Files.exists(projectRoot.resolve("build.gradle.kts"))) {
|
||||
return ProjectType.JAVA_GRADLE;
|
||||
}
|
||||
|
||||
// Node.js项目
|
||||
if (Files.exists(projectRoot.resolve("package.json"))) {
|
||||
return analyzeNodeJsProject(projectRoot);
|
||||
}
|
||||
|
||||
// Python项目
|
||||
if (Files.exists(projectRoot.resolve("requirements.txt")) ||
|
||||
Files.exists(projectRoot.resolve("setup.py")) ||
|
||||
Files.exists(projectRoot.resolve("pyproject.toml"))) {
|
||||
return analyzePythonProject(projectRoot);
|
||||
}
|
||||
|
||||
// .NET项目
|
||||
try (Stream<Path> files = Files.list(projectRoot)) {
|
||||
if (files.anyMatch(path -> path.toString().endsWith(".csproj") ||
|
||||
path.toString().endsWith(".sln"))) {
|
||||
return ProjectType.DOTNET;
|
||||
}
|
||||
}
|
||||
|
||||
// Go项目
|
||||
if (Files.exists(projectRoot.resolve("go.mod"))) {
|
||||
return ProjectType.GO;
|
||||
}
|
||||
|
||||
// Rust项目
|
||||
if (Files.exists(projectRoot.resolve("Cargo.toml"))) {
|
||||
return ProjectType.RUST;
|
||||
}
|
||||
|
||||
// PHP项目
|
||||
if (Files.exists(projectRoot.resolve("composer.json"))) {
|
||||
return ProjectType.PHP;
|
||||
}
|
||||
|
||||
// 静态HTML项目
|
||||
if (Files.exists(projectRoot.resolve("index.html"))) {
|
||||
return ProjectType.HTML_STATIC;
|
||||
}
|
||||
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为Spring Boot项目
|
||||
*/
|
||||
private boolean isSpringBootProject(Path projectRoot) {
|
||||
try {
|
||||
Path pomFile = projectRoot.resolve("pom.xml");
|
||||
if (!Files.exists(pomFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String pomContent = Files.readString(pomFile);
|
||||
return pomContent.contains("spring-boot-starter") ||
|
||||
pomContent.contains("org.springframework.boot");
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading pom.xml for Spring Boot detection", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Node.js项目类型
|
||||
*/
|
||||
private ProjectType analyzeNodeJsProject(Path projectRoot) {
|
||||
try {
|
||||
Path packageJsonPath = projectRoot.resolve("package.json");
|
||||
String content = Files.readString(packageJsonPath);
|
||||
JsonNode packageJson = objectMapper.readTree(content);
|
||||
|
||||
// 检查依赖来确定具体的框架类型
|
||||
JsonNode dependencies = packageJson.get("dependencies");
|
||||
JsonNode devDependencies = packageJson.get("devDependencies");
|
||||
|
||||
if (hasDependency(dependencies, "react") || hasDependency(devDependencies, "react")) {
|
||||
return ProjectType.REACT;
|
||||
}
|
||||
|
||||
if (hasDependency(dependencies, "vue") || hasDependency(devDependencies, "vue")) {
|
||||
return ProjectType.VUE;
|
||||
}
|
||||
|
||||
if (hasDependency(dependencies, "@angular/core") ||
|
||||
hasDependency(devDependencies, "@angular/cli")) {
|
||||
return ProjectType.ANGULAR;
|
||||
}
|
||||
|
||||
if (hasDependency(dependencies, "next") || hasDependency(devDependencies, "next")) {
|
||||
return ProjectType.NEXT_JS;
|
||||
}
|
||||
|
||||
return ProjectType.NODE_JS;
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error analyzing package.json", e);
|
||||
return ProjectType.NODE_JS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Python项目类型
|
||||
*/
|
||||
private ProjectType analyzePythonProject(Path projectRoot) {
|
||||
// 检查Django项目
|
||||
if (Files.exists(projectRoot.resolve("manage.py"))) {
|
||||
return ProjectType.DJANGO;
|
||||
}
|
||||
|
||||
// 检查Flask项目
|
||||
if (Files.exists(projectRoot.resolve("app.py")) ||
|
||||
Files.exists(projectRoot.resolve("application.py"))) {
|
||||
return ProjectType.FLASK;
|
||||
}
|
||||
|
||||
// 检查FastAPI项目
|
||||
if (Files.exists(projectRoot.resolve("main.py"))) {
|
||||
try {
|
||||
String content = Files.readString(projectRoot.resolve("main.py"));
|
||||
if (content.contains("from fastapi import") || content.contains("import fastapi")) {
|
||||
return ProjectType.FASTAPI;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading main.py for FastAPI detection", e);
|
||||
}
|
||||
}
|
||||
|
||||
return ProjectType.PYTHON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于目录结构检测项目类型
|
||||
*/
|
||||
private ProjectType detectByDirectoryStructure(Path projectRoot) {
|
||||
try {
|
||||
List<String> directories = Files.list(projectRoot)
|
||||
.filter(Files::isDirectory)
|
||||
.map(path -> path.getFileName().toString().toLowerCase())
|
||||
.toList();
|
||||
|
||||
// Java项目特征目录
|
||||
if (directories.contains("src") &&
|
||||
(directories.contains("target") || directories.contains("build"))) {
|
||||
return ProjectType.JAVA_MAVEN; // 默认为Maven
|
||||
}
|
||||
|
||||
// Node.js项目特征目录
|
||||
if (directories.contains("node_modules") ||
|
||||
directories.contains("public") ||
|
||||
directories.contains("dist")) {
|
||||
return ProjectType.NODE_JS;
|
||||
}
|
||||
|
||||
// Python项目特征目录
|
||||
if (directories.contains("venv") ||
|
||||
directories.contains("env") ||
|
||||
directories.contains("__pycache__")) {
|
||||
return ProjectType.PYTHON;
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error analyzing directory structure", e);
|
||||
}
|
||||
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在特定依赖
|
||||
*/
|
||||
private boolean hasDependency(JsonNode dependencies, String dependencyName) {
|
||||
return dependencies != null && dependencies.has(dependencyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目类型的详细信息
|
||||
*/
|
||||
public String getProjectTypeDetails(Path projectRoot, ProjectType projectType) {
|
||||
StringBuilder details = new StringBuilder();
|
||||
details.append("Project Type: ").append(projectType.getDisplayName()).append("\n");
|
||||
details.append("Primary Language: ").append(projectType.getPrimaryLanguage()).append("\n");
|
||||
details.append("Package Manager: ").append(projectType.getPackageManager()).append("\n");
|
||||
|
||||
// 添加特定项目类型的详细信息
|
||||
switch (projectType) {
|
||||
case SPRING_BOOT:
|
||||
details.append("Framework: Spring Boot\n");
|
||||
details.append("Build Tool: Maven\n");
|
||||
break;
|
||||
case REACT:
|
||||
details.append("Framework: React\n");
|
||||
details.append("Runtime: Node.js\n");
|
||||
break;
|
||||
case DJANGO:
|
||||
details.append("Framework: Django\n");
|
||||
details.append("Language: Python\n");
|
||||
break;
|
||||
// 可以添加更多项目类型的详细信息
|
||||
}
|
||||
|
||||
return details.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
@Service
|
||||
public class TaskSummaryService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TaskSummaryService.class);
|
||||
|
||||
private static final Pattern[] ACTION_PATTERNS = {
|
||||
Pattern.compile("(?i)creating?\\s+(?:a\\s+)?(?:new\\s+)?(.{1,50}?)(?:\\s+file|\\s+directory|\\s+project)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)writing?\\s+(?:to\\s+)?(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)reading?\\s+(?:from\\s+)?(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)editing?\\s+(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)listing?\\s+(?:the\\s+)?(.{1,50}?)(?:\\s+directory)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)analyzing?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)generating?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)building?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE)
|
||||
};
|
||||
|
||||
private static final String[] ACTION_VERBS = {
|
||||
"创建", "写入", "读取", "编辑", "列出", "分析", "生成", "构建",
|
||||
"creating", "writing", "reading", "editing", "listing", "analyzing", "generating", "building"
|
||||
};
|
||||
|
||||
/**
|
||||
* 从AI响应中提取任务摘要
|
||||
*/
|
||||
public String extractTaskSummary(String aiResponse) {
|
||||
if (aiResponse == null || aiResponse.trim().isEmpty()) {
|
||||
return "处理中...";
|
||||
}
|
||||
|
||||
// 清理响应文本
|
||||
String cleanResponse = aiResponse.replaceAll("```[\\s\\S]*?```", "").trim();
|
||||
|
||||
// 尝试匹配具体操作
|
||||
for (Pattern pattern : ACTION_PATTERNS) {
|
||||
Matcher matcher = pattern.matcher(cleanResponse);
|
||||
if (matcher.find()) {
|
||||
String action = matcher.group(0).trim();
|
||||
if (action.length() > 50) {
|
||||
action = action.substring(0, 47) + "...";
|
||||
}
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
// 查找动作词汇
|
||||
String lowerResponse = cleanResponse.toLowerCase();
|
||||
for (String verb : ACTION_VERBS) {
|
||||
if (lowerResponse.contains(verb.toLowerCase())) {
|
||||
// 提取包含动作词的句子
|
||||
String[] sentences = cleanResponse.split("[.!?\\n]");
|
||||
for (String sentence : sentences) {
|
||||
if (sentence.toLowerCase().contains(verb.toLowerCase())) {
|
||||
String summary = sentence.trim();
|
||||
if (summary.length() > 60) {
|
||||
summary = summary.substring(0, 57) + "...";
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到具体操作,返回通用描述
|
||||
if (cleanResponse.length() > 60) {
|
||||
return cleanResponse.substring(0, 57) + "...";
|
||||
}
|
||||
|
||||
return cleanResponse.isEmpty() ? "处理中..." : cleanResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算任务复杂度和预期轮数
|
||||
*/
|
||||
public int estimateTaskComplexity(String initialMessage) {
|
||||
if (initialMessage == null) return 1;
|
||||
|
||||
String lowerMessage = initialMessage.toLowerCase();
|
||||
int complexity = 1;
|
||||
|
||||
// 基于关键词估算复杂度
|
||||
if (lowerMessage.contains("project") || lowerMessage.contains("项目")) complexity += 3;
|
||||
if (lowerMessage.contains("complete") || lowerMessage.contains("完整")) complexity += 2;
|
||||
if (lowerMessage.contains("multiple") || lowerMessage.contains("多个")) complexity += 2;
|
||||
if (lowerMessage.contains("full-stack") || lowerMessage.contains("全栈")) complexity += 4;
|
||||
if (lowerMessage.contains("website") || lowerMessage.contains("网站")) complexity += 2;
|
||||
if (lowerMessage.contains("api") || lowerMessage.contains("接口")) complexity += 2;
|
||||
|
||||
// 基于文件操作数量估算
|
||||
long fileOperations = lowerMessage.chars()
|
||||
.mapToObj(c -> String.valueOf((char) c))
|
||||
.filter(s -> s.matches(".*(?:create|write|edit|file|directory).*"))
|
||||
.count();
|
||||
|
||||
complexity += (int) Math.min(fileOperations / 2, 5);
|
||||
|
||||
return Math.min(complexity, 15); // 最大15轮
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成当前状态的用户友好描述
|
||||
*/
|
||||
public String generateStatusDescription(String status, String currentAction, int currentTurn, int totalTurns) {
|
||||
StringBuilder desc = new StringBuilder();
|
||||
|
||||
switch (status) {
|
||||
case "RUNNING":
|
||||
if (currentAction != null && !currentAction.trim().isEmpty()) {
|
||||
desc.append("🔄 ").append(currentAction);
|
||||
} else {
|
||||
desc.append("🤔 AI正在思考...");
|
||||
}
|
||||
|
||||
if (totalTurns > 1) {
|
||||
desc.append(String.format(" (第%d/%d轮)", currentTurn, totalTurns));
|
||||
}
|
||||
break;
|
||||
|
||||
case "COMPLETED":
|
||||
desc.append("✅ 任务完成");
|
||||
if (totalTurns > 1) {
|
||||
desc.append(String.format(" (共%d轮)", currentTurn));
|
||||
}
|
||||
break;
|
||||
|
||||
case "ERROR":
|
||||
desc.append("❌ 执行出错");
|
||||
break;
|
||||
|
||||
default:
|
||||
desc.append("⏳ 处理中...");
|
||||
}
|
||||
|
||||
return desc.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 工具执行日志记录服务
|
||||
* 记录所有工具调用的详细信息,使用中文日志
|
||||
*/
|
||||
@Service
|
||||
public class ToolExecutionLogger {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ToolExecutionLogger.class);
|
||||
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
// 工具调用计数器
|
||||
private final AtomicLong callCounter = new AtomicLong(0);
|
||||
|
||||
// 工具执行统计
|
||||
private final Map<String, ToolStats> toolStats = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 记录工具调用开始
|
||||
*/
|
||||
public long logToolStart(String toolName, String description, Object parameters) {
|
||||
long callId = callCounter.incrementAndGet();
|
||||
String timestamp = LocalDateTime.now().format(formatter);
|
||||
|
||||
logger.info("🚀 [工具调用-{}] 开始执行工具: {}", callId, toolName);
|
||||
logger.info("📝 [工具调用-{}] 工具描述: {}", callId, description);
|
||||
logger.info("⚙️ [工具调用-{}] 调用参数: {}", callId, formatParameters(parameters));
|
||||
logger.info("🕐 [工具调用-{}] 开始时间: {}", callId, timestamp);
|
||||
|
||||
// 更新统计信息
|
||||
toolStats.computeIfAbsent(toolName, k -> new ToolStats()).incrementCalls();
|
||||
|
||||
return callId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具调用成功
|
||||
*/
|
||||
public void logToolSuccess(long callId, String toolName, String result, long executionTimeMs) {
|
||||
String timestamp = LocalDateTime.now().format(formatter);
|
||||
|
||||
logger.info("✅ [工具调用-{}] 工具执行成功: {}", callId, toolName);
|
||||
logger.info("📊 [工具调用-{}] 执行结果: {}", callId, truncateResult(result));
|
||||
logger.info("⏱️ [工具调用-{}] 执行耗时: {}ms", callId, executionTimeMs);
|
||||
logger.info("🕐 [工具调用-{}] 完成时间: {}", callId, timestamp);
|
||||
|
||||
// 更新统计信息
|
||||
ToolStats stats = toolStats.get(toolName);
|
||||
if (stats != null) {
|
||||
stats.incrementSuccess();
|
||||
stats.addExecutionTime(executionTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具调用失败
|
||||
*/
|
||||
public void logToolError(long callId, String toolName, String error, long executionTimeMs) {
|
||||
String timestamp = LocalDateTime.now().format(formatter);
|
||||
|
||||
logger.error("❌ [工具调用-{}] 工具执行失败: {}", callId, toolName);
|
||||
logger.error("🚨 [工具调用-{}] 错误信息: {}", callId, error);
|
||||
logger.error("⏱️ [工具调用-{}] 执行耗时: {}ms", callId, executionTimeMs);
|
||||
logger.error("🕐 [工具调用-{}] 失败时间: {}", callId, timestamp);
|
||||
|
||||
// 更新统计信息
|
||||
ToolStats stats = toolStats.get(toolName);
|
||||
if (stats != null) {
|
||||
stats.incrementError();
|
||||
stats.addExecutionTime(executionTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具调用的详细步骤
|
||||
*/
|
||||
public void logToolStep(long callId, String toolName, String step, String details) {
|
||||
logger.debug("🔄 [工具调用-{}] [{}] 执行步骤: {} - {}", callId, toolName, step, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录文件操作
|
||||
*/
|
||||
public void logFileOperation(long callId, String operation, String filePath, String details) {
|
||||
logger.info("📁 [工具调用-{}] 文件操作: {} - 文件: {} - 详情: {}", callId, operation, filePath, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录项目分析
|
||||
*/
|
||||
public void logProjectAnalysis(long callId, String projectPath, String projectType, String details) {
|
||||
logger.info("🔍 [工具调用-{}] 项目分析: 路径={}, 类型={}, 详情={}", callId, projectPath, projectType, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录项目创建
|
||||
*/
|
||||
public void logProjectCreation(long callId, String projectName, String projectType, String projectPath) {
|
||||
logger.info("🏗️ [工具调用-{}] 项目创建: 名称={}, 类型={}, 路径={}", callId, projectName, projectType, projectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具执行统计
|
||||
*/
|
||||
public void logToolStatistics() {
|
||||
logger.info("📈 ========== 工具执行统计 ==========");
|
||||
toolStats.forEach((toolName, stats) -> {
|
||||
logger.info("🔧 工具: {} | 调用次数: {} | 成功: {} | 失败: {} | 平均耗时: {}ms",
|
||||
toolName, stats.getTotalCalls(), stats.getSuccessCount(),
|
||||
stats.getErrorCount(), stats.getAverageExecutionTime());
|
||||
});
|
||||
logger.info("📈 ================================");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化参数显示
|
||||
*/
|
||||
private String formatParameters(Object parameters) {
|
||||
if (parameters == null) {
|
||||
return "无参数";
|
||||
}
|
||||
String paramStr = parameters.toString();
|
||||
return paramStr.length() > 200 ? paramStr.substring(0, 200) + "..." : paramStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断结果显示
|
||||
*/
|
||||
private String truncateResult(String result) {
|
||||
if (result == null) {
|
||||
return "无结果";
|
||||
}
|
||||
return result.length() > 300 ? result.substring(0, 300) + "..." : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具统计信息内部类
|
||||
*/
|
||||
private static class ToolStats {
|
||||
private long totalCalls = 0;
|
||||
private long successCount = 0;
|
||||
private long errorCount = 0;
|
||||
private long totalExecutionTime = 0;
|
||||
|
||||
public void incrementCalls() { totalCalls++; }
|
||||
public void incrementSuccess() { successCount++; }
|
||||
public void incrementError() { errorCount++; }
|
||||
public void addExecutionTime(long time) { totalExecutionTime += time; }
|
||||
|
||||
public long getTotalCalls() { return totalCalls; }
|
||||
public long getSuccessCount() { return successCount; }
|
||||
public long getErrorCount() { return errorCount; }
|
||||
public long getAverageExecutionTime() {
|
||||
return totalCalls > 0 ? totalExecutionTime / totalCalls : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
/**
|
||||
* 工具日志事件类
|
||||
* 继承自LogEvent,添加工具相关的字段
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ToolLogEvent extends LogEvent {
|
||||
|
||||
private String toolName;
|
||||
private String filePath;
|
||||
private String icon;
|
||||
private String status; // RUNNING, SUCCESS, ERROR
|
||||
private Long executionTime; // 执行时间(毫秒)
|
||||
private String summary; // 操作摘要
|
||||
|
||||
// Constructors
|
||||
public ToolLogEvent() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ToolLogEvent(String type, String taskId, String toolName, String filePath,
|
||||
String message, String timestamp, String icon, String status) {
|
||||
super(type, taskId, message, timestamp);
|
||||
this.toolName = toolName;
|
||||
this.filePath = filePath;
|
||||
this.icon = icon;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getToolName() {
|
||||
return toolName;
|
||||
}
|
||||
|
||||
public void setToolName(String toolName) {
|
||||
this.toolName = toolName;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(String icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getExecutionTime() {
|
||||
return executionTime;
|
||||
}
|
||||
|
||||
public void setExecutionTime(Long executionTime) {
|
||||
this.executionTime = executionTime;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ToolLogEvent{" +
|
||||
"toolName='" + toolName + '\'' +
|
||||
", filePath='" + filePath + '\'' +
|
||||
", icon='" + icon + '\'' +
|
||||
", status='" + status + '\'' +
|
||||
", executionTime=" + executionTime +
|
||||
", summary='" + summary + '\'' +
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.model.ProjectContext;
|
||||
import com.example.demo.model.ProjectStructure;
|
||||
import com.example.demo.schema.JsonSchema;
|
||||
import com.example.demo.service.ProjectContextAnalyzer;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 项目分析工具
|
||||
* 分析现有项目的结构、类型、依赖等信息
|
||||
*/
|
||||
@Component
|
||||
public class AnalyzeProjectTool extends BaseTool<AnalyzeProjectTool.AnalyzeProjectParams> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AnalyzeProjectTool.class);
|
||||
|
||||
@Autowired
|
||||
private ProjectContextAnalyzer projectContextAnalyzer;
|
||||
|
||||
private final String rootDirectory;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
public AnalyzeProjectTool(AppProperties appProperties) {
|
||||
super(
|
||||
"analyze_project",
|
||||
"AnalyzeProject",
|
||||
"Analyze an existing project to understand its structure, type, dependencies, and configuration. " +
|
||||
"Provides comprehensive project information that can be used for intelligent editing and refactoring.",
|
||||
createSchema()
|
||||
);
|
||||
this.appProperties = appProperties;
|
||||
this.rootDirectory = appProperties.getWorkspace().getRootDirectory();
|
||||
}
|
||||
|
||||
private static JsonSchema createSchema() {
|
||||
return JsonSchema.object()
|
||||
.addProperty("project_path", JsonSchema.string(
|
||||
"Absolute path to the project root directory to analyze. " +
|
||||
"Must be within the workspace directory."
|
||||
))
|
||||
.addProperty("analysis_depth", JsonSchema.string(
|
||||
"Analysis depth: 'basic', 'detailed', or 'comprehensive'. " +
|
||||
"Default: 'detailed'. " +
|
||||
"- basic: Project type and structure only\n" +
|
||||
"- detailed: Includes dependencies and configuration\n" +
|
||||
"- comprehensive: Full analysis including code statistics"
|
||||
))
|
||||
.addProperty("include_code_stats", JsonSchema.bool(
|
||||
"Whether to include detailed code statistics (lines of code, classes, methods, etc.). " +
|
||||
"Default: true for detailed/comprehensive analysis"
|
||||
))
|
||||
.addProperty("output_format", JsonSchema.string(
|
||||
"Output format: 'summary', 'detailed', or 'json'. Default: 'detailed'"
|
||||
))
|
||||
.required("project_path");
|
||||
}
|
||||
|
||||
public enum AnalysisDepth {
|
||||
BASIC("basic", "Basic project type and structure analysis"),
|
||||
DETAILED("detailed", "Detailed analysis including dependencies and configuration"),
|
||||
COMPREHENSIVE("comprehensive", "Comprehensive analysis with full code statistics");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
|
||||
AnalysisDepth(String value, String description) {
|
||||
this.value = value;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public static AnalysisDepth fromString(String value) {
|
||||
for (AnalysisDepth depth : values()) {
|
||||
if (depth.value.equals(value)) {
|
||||
return depth;
|
||||
}
|
||||
}
|
||||
return DETAILED; // default
|
||||
}
|
||||
|
||||
public String getValue() { return value; }
|
||||
public String getDescription() { return description; }
|
||||
}
|
||||
|
||||
public enum OutputFormat {
|
||||
SUMMARY("summary", "Brief summary of key project information"),
|
||||
DETAILED("detailed", "Detailed human-readable analysis report"),
|
||||
JSON("json", "Structured JSON output for programmatic use");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
|
||||
OutputFormat(String value, String description) {
|
||||
this.value = value;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public static OutputFormat fromString(String value) {
|
||||
for (OutputFormat format : values()) {
|
||||
if (format.value.equals(value)) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
return DETAILED; // default
|
||||
}
|
||||
|
||||
public String getValue() { return value; }
|
||||
public String getDescription() { return description; }
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateToolParams(AnalyzeProjectParams params) {
|
||||
String baseValidation = super.validateToolParams(params);
|
||||
if (baseValidation != null) {
|
||||
return baseValidation;
|
||||
}
|
||||
|
||||
if (params.projectPath == null || params.projectPath.trim().isEmpty()) {
|
||||
return "Project path cannot be empty";
|
||||
}
|
||||
|
||||
Path projectPath = Paths.get(params.projectPath);
|
||||
if (!projectPath.isAbsolute()) {
|
||||
return "Project path must be absolute: " + params.projectPath;
|
||||
}
|
||||
|
||||
if (!Files.exists(projectPath)) {
|
||||
return "Project path does not exist: " + params.projectPath;
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(projectPath)) {
|
||||
return "Project path must be a directory: " + params.projectPath;
|
||||
}
|
||||
|
||||
if (!isWithinWorkspace(projectPath)) {
|
||||
return "Project path must be within the workspace directory: " + params.projectPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze project tool method for Spring AI integration
|
||||
*/
|
||||
@Tool(name = "analyze_project", description = "Analyzes project structure, type, dependencies and other information")
|
||||
public String analyzeProject(String projectPath, String analysisDepth, String outputFormat, Boolean includeCodeStats) {
|
||||
try {
|
||||
AnalyzeProjectParams params = new AnalyzeProjectParams();
|
||||
params.setProjectPath(projectPath);
|
||||
params.setAnalysisDepth(analysisDepth != null ? analysisDepth : "basic");
|
||||
params.setOutputFormat(outputFormat != null ? outputFormat : "detailed");
|
||||
params.setIncludeCodeStats(includeCodeStats != null ? includeCodeStats : false);
|
||||
|
||||
// Validate parameters
|
||||
String validation = validateToolParams(params);
|
||||
if (validation != null) {
|
||||
return "Error: " + validation;
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
ToolResult result = execute(params).join();
|
||||
|
||||
if (result.isSuccess()) {
|
||||
return result.getLlmContent();
|
||||
} else {
|
||||
return "Error: " + result.getErrorMessage();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error in analyze project tool", e);
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolResult> execute(AnalyzeProjectParams params) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
logger.info("Starting project analysis for: {}", params.projectPath);
|
||||
|
||||
Path projectPath = Paths.get(params.projectPath);
|
||||
AnalysisDepth depth = AnalysisDepth.fromString(params.analysisDepth);
|
||||
OutputFormat format = OutputFormat.fromString(params.outputFormat);
|
||||
|
||||
// 执行项目分析
|
||||
ProjectContext context = analyzeProject(projectPath, depth, params);
|
||||
|
||||
// 生成输出
|
||||
String output = generateOutput(context, format, depth);
|
||||
String summary = generateSummary(context);
|
||||
|
||||
logger.info("Project analysis completed for: {}", params.projectPath);
|
||||
return ToolResult.success(summary, output);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error during project analysis", e);
|
||||
return ToolResult.error("Project analysis failed: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行项目分析
|
||||
*/
|
||||
private ProjectContext analyzeProject(Path projectPath, AnalysisDepth depth, AnalyzeProjectParams params) {
|
||||
logger.debug("Analyzing project with depth: {}", depth);
|
||||
|
||||
switch (depth) {
|
||||
case BASIC:
|
||||
return analyzeBasic(projectPath);
|
||||
case DETAILED:
|
||||
return analyzeDetailed(projectPath, params);
|
||||
case COMPREHENSIVE:
|
||||
return analyzeComprehensive(projectPath, params);
|
||||
default:
|
||||
return projectContextAnalyzer.analyzeProject(projectPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础分析
|
||||
*/
|
||||
private ProjectContext analyzeBasic(Path projectPath) {
|
||||
// 只分析项目类型和基本结构
|
||||
ProjectContext context = new ProjectContext(projectPath);
|
||||
context.setProjectType(projectContextAnalyzer.projectTypeDetector.detectProjectType(projectPath));
|
||||
context.setProjectStructure(projectContextAnalyzer.projectDiscoveryService.analyzeProjectStructure(projectPath));
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 详细分析
|
||||
*/
|
||||
private ProjectContext analyzeDetailed(Path projectPath, AnalyzeProjectParams params) {
|
||||
ProjectContext context = analyzeBasic(projectPath);
|
||||
|
||||
// 添加依赖和配置文件分析
|
||||
context.setDependencies(projectContextAnalyzer.projectDiscoveryService.analyzeDependencies(projectPath));
|
||||
context.setConfigFiles(projectContextAnalyzer.projectDiscoveryService.findConfigurationFiles(projectPath));
|
||||
|
||||
// 如果需要代码统计
|
||||
if (params.includeCodeStats == null || params.includeCodeStats) {
|
||||
// 简化的代码统计,避免性能问题
|
||||
ProjectContext.CodeStatistics stats = new ProjectContext.CodeStatistics();
|
||||
// 这里可以添加基本的代码统计逻辑
|
||||
context.setCodeStatistics(stats);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全面分析
|
||||
*/
|
||||
private ProjectContext analyzeComprehensive(Path projectPath, AnalyzeProjectParams params) {
|
||||
// 使用完整的项目分析
|
||||
return projectContextAnalyzer.analyzeProject(projectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成输出
|
||||
*/
|
||||
private String generateOutput(ProjectContext context, OutputFormat format, AnalysisDepth depth) {
|
||||
switch (format) {
|
||||
case SUMMARY:
|
||||
return generateSummaryOutput(context);
|
||||
case DETAILED:
|
||||
return generateDetailedOutput(context, depth);
|
||||
case JSON:
|
||||
return generateJsonOutput(context);
|
||||
default:
|
||||
return generateDetailedOutput(context, depth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成摘要输出
|
||||
*/
|
||||
private String generateSummaryOutput(ProjectContext context) {
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
output.append("📊 PROJECT ANALYSIS SUMMARY\n");
|
||||
output.append("=" .repeat(50)).append("\n\n");
|
||||
|
||||
// 基本信息
|
||||
output.append("🏗️ Project: ").append(context.getProjectRoot().getFileName()).append("\n");
|
||||
output.append("🔧 Type: ").append(context.getProjectType().getDisplayName()).append("\n");
|
||||
output.append("💻 Language: ").append(context.getProjectType().getPrimaryLanguage()).append("\n");
|
||||
output.append("📦 Package Manager: ").append(context.getProjectType().getPackageManager()).append("\n\n");
|
||||
|
||||
// 结构信息
|
||||
if (context.getProjectStructure() != null) {
|
||||
ProjectStructure structure = context.getProjectStructure();
|
||||
output.append("📁 Structure:\n");
|
||||
output.append(" - Directories: ").append(structure.getTotalDirectories()).append("\n");
|
||||
output.append(" - Files: ").append(structure.getTotalFiles()).append("\n");
|
||||
output.append(" - Size: ").append(formatFileSize(structure.getTotalSize())).append("\n\n");
|
||||
}
|
||||
|
||||
// 依赖信息
|
||||
if (context.getDependencies() != null && !context.getDependencies().isEmpty()) {
|
||||
output.append("📚 Dependencies: ").append(context.getDependencies().size()).append(" found\n");
|
||||
output.append(" - Key dependencies: ").append(context.getDependencySummary()).append("\n\n");
|
||||
}
|
||||
|
||||
// 配置文件
|
||||
if (context.getConfigFiles() != null && !context.getConfigFiles().isEmpty()) {
|
||||
output.append("⚙️ Configuration Files: ").append(context.getConfigFiles().size()).append(" found\n");
|
||||
context.getConfigFiles().stream()
|
||||
.filter(ProjectContext.ConfigFile::isMainConfig)
|
||||
.forEach(config -> output.append(" - ").append(config.getFileName()).append("\n"));
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成详细输出
|
||||
*/
|
||||
private String generateDetailedOutput(ProjectContext context, AnalysisDepth depth) {
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
output.append("📊 COMPREHENSIVE PROJECT ANALYSIS\n");
|
||||
output.append("=" .repeat(60)).append("\n\n");
|
||||
|
||||
// 使用项目上下文的摘要生成功能
|
||||
output.append(context.generateContextSummary());
|
||||
|
||||
// 添加分析深度特定的信息
|
||||
if (depth == AnalysisDepth.COMPREHENSIVE) {
|
||||
output.append("\n=== DETAILED INSIGHTS ===\n");
|
||||
output.append(generateProjectInsights(context));
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成JSON输出
|
||||
*/
|
||||
private String generateJsonOutput(ProjectContext context) {
|
||||
// 简化的JSON输出实现
|
||||
// 在实际项目中应该使用Jackson等JSON库
|
||||
StringBuilder json = new StringBuilder();
|
||||
json.append("{\n");
|
||||
json.append(" \"projectName\": \"").append(context.getProjectRoot().getFileName()).append("\",\n");
|
||||
json.append(" \"projectType\": \"").append(context.getProjectType().name()).append("\",\n");
|
||||
json.append(" \"primaryLanguage\": \"").append(context.getProjectType().getPrimaryLanguage()).append("\",\n");
|
||||
|
||||
if (context.getProjectStructure() != null) {
|
||||
ProjectStructure structure = context.getProjectStructure();
|
||||
json.append(" \"structure\": {\n");
|
||||
json.append(" \"directories\": ").append(structure.getTotalDirectories()).append(",\n");
|
||||
json.append(" \"files\": ").append(structure.getTotalFiles()).append(",\n");
|
||||
json.append(" \"totalSize\": ").append(structure.getTotalSize()).append("\n");
|
||||
json.append(" },\n");
|
||||
}
|
||||
|
||||
json.append(" \"dependencyCount\": ").append(
|
||||
context.getDependencies() != null ? context.getDependencies().size() : 0).append(",\n");
|
||||
json.append(" \"configFileCount\": ").append(
|
||||
context.getConfigFiles() != null ? context.getConfigFiles().size() : 0).append("\n");
|
||||
|
||||
json.append("}");
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成项目洞察
|
||||
*/
|
||||
private String generateProjectInsights(ProjectContext context) {
|
||||
StringBuilder insights = new StringBuilder();
|
||||
|
||||
// 项目健康度评估
|
||||
insights.append("Project Health Assessment:\n");
|
||||
|
||||
// 检查是否有版本控制
|
||||
if (context.getMetadata().containsKey("versionControl")) {
|
||||
insights.append("✅ Version control detected: ").append(context.getMetadata().get("versionControl")).append("\n");
|
||||
} else {
|
||||
insights.append("⚠️ No version control detected\n");
|
||||
}
|
||||
|
||||
// 检查是否有CI/CD
|
||||
if (context.getMetadata().containsKey("cicd")) {
|
||||
insights.append("✅ CI/CD configured: ").append(context.getMetadata().get("cicd")).append("\n");
|
||||
} else {
|
||||
insights.append("💡 Consider setting up CI/CD\n");
|
||||
}
|
||||
|
||||
// 检查是否有容器化
|
||||
if (context.getMetadata().containsKey("containerization")) {
|
||||
insights.append("✅ Containerization: ").append(context.getMetadata().get("containerization")).append("\n");
|
||||
}
|
||||
|
||||
// 代码质量建议
|
||||
insights.append("\nRecommendations:\n");
|
||||
if (context.getProjectType().isJavaProject()) {
|
||||
insights.append("- Consider using static analysis tools like SpotBugs or PMD\n");
|
||||
insights.append("- Ensure proper test coverage with JUnit\n");
|
||||
} else if (context.getProjectType().isJavaScriptProject()) {
|
||||
insights.append("- Consider using ESLint for code quality\n");
|
||||
insights.append("- Add TypeScript for better type safety\n");
|
||||
} else if (context.getProjectType().isPythonProject()) {
|
||||
insights.append("- Consider using pylint or flake8 for code quality\n");
|
||||
insights.append("- Add type hints for better code documentation\n");
|
||||
}
|
||||
|
||||
return insights.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成摘要
|
||||
*/
|
||||
private String generateSummary(ProjectContext context) {
|
||||
return String.format("Analyzed %s project: %s (%s) with %d dependencies and %d config files",
|
||||
context.getProjectType().getDisplayName(),
|
||||
context.getProjectRoot().getFileName(),
|
||||
context.getProjectType().getPrimaryLanguage(),
|
||||
context.getDependencies() != null ? context.getDependencies().size() : 0,
|
||||
context.getConfigFiles() != null ? context.getConfigFiles().size() : 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
private String formatFileSize(long bytes) {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
|
||||
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024));
|
||||
return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否在工作空间内
|
||||
*/
|
||||
private boolean isWithinWorkspace(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
Path normalizedPath = filePath.toRealPath();
|
||||
return normalizedPath.startsWith(workspaceRoot);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Could not resolve workspace path", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析项目参数
|
||||
*/
|
||||
public static class AnalyzeProjectParams {
|
||||
@JsonProperty("project_path")
|
||||
private String projectPath;
|
||||
|
||||
@JsonProperty("analysis_depth")
|
||||
private String analysisDepth = "detailed";
|
||||
|
||||
@JsonProperty("include_code_stats")
|
||||
private Boolean includeCodeStats;
|
||||
|
||||
@JsonProperty("output_format")
|
||||
private String outputFormat = "detailed";
|
||||
|
||||
// Getters and Setters
|
||||
public String getProjectPath() { return projectPath; }
|
||||
public void setProjectPath(String projectPath) { this.projectPath = projectPath; }
|
||||
|
||||
public String getAnalysisDepth() { return analysisDepth; }
|
||||
public void setAnalysisDepth(String analysisDepth) { this.analysisDepth = analysisDepth; }
|
||||
|
||||
public Boolean getIncludeCodeStats() { return includeCodeStats; }
|
||||
public void setIncludeCodeStats(Boolean includeCodeStats) { this.includeCodeStats = includeCodeStats; }
|
||||
|
||||
public String getOutputFormat() { return outputFormat; }
|
||||
public void setOutputFormat(String outputFormat) { this.outputFormat = outputFormat; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("AnalyzeProjectParams{path='%s', depth='%s', format='%s'}",
|
||||
projectPath, analysisDepth, outputFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.example.demo.schema.JsonSchema;
|
||||
import com.example.demo.schema.SchemaValidator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Base abstract class for tools
|
||||
* All tools should inherit from this class
|
||||
*/
|
||||
public abstract class BaseTool<P> {
|
||||
|
||||
protected final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
protected final String name;
|
||||
protected final String displayName;
|
||||
protected final String description;
|
||||
protected final JsonSchema parameterSchema;
|
||||
protected final boolean isOutputMarkdown;
|
||||
protected final boolean canUpdateOutput;
|
||||
|
||||
protected SchemaValidator schemaValidator;
|
||||
|
||||
public BaseTool(String name, String displayName, String description, JsonSchema parameterSchema) {
|
||||
this(name, displayName, description, parameterSchema, true, false);
|
||||
}
|
||||
|
||||
public BaseTool(String name, String displayName, String description, JsonSchema parameterSchema,
|
||||
boolean isOutputMarkdown, boolean canUpdateOutput) {
|
||||
this.name = name;
|
||||
this.displayName = displayName;
|
||||
this.description = description;
|
||||
this.parameterSchema = parameterSchema;
|
||||
this.isOutputMarkdown = isOutputMarkdown;
|
||||
this.canUpdateOutput = canUpdateOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Schema validator (through dependency injection)
|
||||
*/
|
||||
public void setSchemaValidator(SchemaValidator schemaValidator) {
|
||||
this.schemaValidator = schemaValidator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate tool parameters
|
||||
*
|
||||
* @param params Parameter object
|
||||
* @return Validation error message, null means validation passed
|
||||
*/
|
||||
public String validateToolParams(P params) {
|
||||
if (schemaValidator == null || parameterSchema == null) {
|
||||
logger.warn("Schema validator or parameter schema is null, skipping validation");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return schemaValidator.validate(parameterSchema, params);
|
||||
} catch (Exception e) {
|
||||
logger.error("Parameter validation failed", e);
|
||||
return "Parameter validation error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm whether user approval is needed for execution
|
||||
*
|
||||
* @param params Parameter object
|
||||
* @return Confirmation details, null means no confirmation needed
|
||||
*/
|
||||
public CompletableFuture<ToolConfirmationDetails> shouldConfirmExecute(P params) {
|
||||
return CompletableFuture.completedFuture(null); // Default no confirmation needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute tool
|
||||
*
|
||||
* @param params Parameter object
|
||||
* @return Execution result
|
||||
*/
|
||||
public abstract CompletableFuture<ToolResult> execute(P params);
|
||||
|
||||
/**
|
||||
* Get tool description (for AI understanding)
|
||||
*
|
||||
* @param params Parameter object
|
||||
* @return Description information
|
||||
*/
|
||||
public String getDescription(P params) {
|
||||
return description;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getName() { return name; }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public String getDescription() { return description; }
|
||||
public JsonSchema getParameterSchema() { return parameterSchema; }
|
||||
public boolean isOutputMarkdown() { return isOutputMarkdown; }
|
||||
public boolean canUpdateOutput() { return canUpdateOutput; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Tool{name='%s', displayName='%s'}", name, displayName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.schema.JsonSchema;
|
||||
import com.example.demo.service.ToolExecutionLogger;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.github.difflib.DiffUtils;
|
||||
import com.github.difflib.UnifiedDiffUtils;
|
||||
import com.github.difflib.patch.Patch;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* File editing tool
|
||||
* Supports file editing based on string replacement, automatically shows differences
|
||||
*/
|
||||
@Component
|
||||
public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
|
||||
|
||||
private final String rootDirectory;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
@Autowired
|
||||
private ToolExecutionLogger executionLogger;
|
||||
|
||||
public EditFileTool(AppProperties appProperties) {
|
||||
super(
|
||||
"edit_file",
|
||||
"EditFile",
|
||||
"Edits a file by replacing specified text with new text. " +
|
||||
"Shows a diff of the changes before applying them. " +
|
||||
"Supports both exact string matching and line-based editing. " +
|
||||
"Use absolute paths within the workspace directory.",
|
||||
createSchema()
|
||||
);
|
||||
this.appProperties = appProperties;
|
||||
this.rootDirectory = appProperties.getWorkspace().getRootDirectory();
|
||||
}
|
||||
|
||||
private static String getWorkspaceBasePath() {
|
||||
return Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
||||
}
|
||||
|
||||
private static String getPathExample(String subPath) {
|
||||
return "Example: \"" + Paths.get(getWorkspaceBasePath(), subPath).toString() + "\"";
|
||||
}
|
||||
|
||||
private static JsonSchema createSchema() {
|
||||
return JsonSchema.object()
|
||||
.addProperty("file_path", JsonSchema.string(
|
||||
"MUST be an absolute path to the file to edit. Path must be within the workspace directory (" +
|
||||
getWorkspaceBasePath() + "). " +
|
||||
getPathExample("project/src/main.java") + ". " +
|
||||
"Relative paths are NOT allowed."
|
||||
))
|
||||
.addProperty("old_str", JsonSchema.string(
|
||||
"The exact string to find and replace. Must match exactly including whitespace and newlines."
|
||||
))
|
||||
.addProperty("new_str", JsonSchema.string(
|
||||
"The new string to replace the old string with. Can be empty to delete the old string."
|
||||
))
|
||||
.addProperty("start_line", JsonSchema.integer(
|
||||
"Optional: 1-based line number where the old_str starts. Helps with disambiguation."
|
||||
).minimum(1))
|
||||
.addProperty("end_line", JsonSchema.integer(
|
||||
"Optional: 1-based line number where the old_str ends. Must be >= start_line."
|
||||
).minimum(1))
|
||||
.required("file_path", "old_str", "new_str");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateToolParams(EditFileParams params) {
|
||||
String baseValidation = super.validateToolParams(params);
|
||||
if (baseValidation != null) {
|
||||
return baseValidation;
|
||||
}
|
||||
|
||||
// 验证路径
|
||||
if (params.filePath == null || params.filePath.trim().isEmpty()) {
|
||||
return "File path cannot be empty";
|
||||
}
|
||||
|
||||
if (params.oldStr == null) {
|
||||
return "Old string cannot be null";
|
||||
}
|
||||
|
||||
if (params.newStr == null) {
|
||||
return "New string cannot be null";
|
||||
}
|
||||
|
||||
Path filePath = Paths.get(params.filePath);
|
||||
|
||||
// Validate if it's an absolute path
|
||||
if (!filePath.isAbsolute()) {
|
||||
return "File path must be absolute: " + params.filePath;
|
||||
}
|
||||
|
||||
// 验证是否在工作目录内
|
||||
if (!isWithinWorkspace(filePath)) {
|
||||
return "File path must be within the workspace directory (" + rootDirectory + "): " + params.filePath;
|
||||
}
|
||||
|
||||
// 验证行号
|
||||
if (params.startLine != null && params.endLine != null) {
|
||||
if (params.endLine < params.startLine) {
|
||||
return "End line must be >= start line";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolConfirmationDetails> shouldConfirmExecute(EditFileParams params) {
|
||||
// Decide whether confirmation is needed based on configuration
|
||||
if (appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.AUTO_EDIT ||
|
||||
appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.YOLO) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Path filePath = Paths.get(params.filePath);
|
||||
|
||||
if (!Files.exists(filePath)) {
|
||||
return null; // 文件不存在,无法预览差异
|
||||
}
|
||||
|
||||
String currentContent = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||
String newContent = performEdit(currentContent, params);
|
||||
|
||||
if (newContent == null) {
|
||||
return null; // Edit failed, cannot preview differences
|
||||
}
|
||||
|
||||
// 生成差异显示
|
||||
String diff = generateDiff(filePath.getFileName().toString(), currentContent, newContent);
|
||||
String title = "Confirm Edit: " + getRelativePath(filePath);
|
||||
|
||||
return ToolConfirmationDetails.edit(title, filePath.getFileName().toString(), diff);
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not read file for edit preview: " + params.filePath, e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit file tool method for Spring AI integration
|
||||
*/
|
||||
@Tool(name = "edit_file", description = "Edits a file by replacing specified text with new text")
|
||||
public String editFile(String filePath, String oldStr, String newStr, Integer startLine, Integer endLine) {
|
||||
long callId = executionLogger.logToolStart("edit_file", "编辑文件内容",
|
||||
String.format("文件=%s, 替换文本长度=%d->%d, 行号范围=%s-%s",
|
||||
filePath, oldStr != null ? oldStr.length() : 0,
|
||||
newStr != null ? newStr.length() : 0, startLine, endLine));
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
EditFileParams params = new EditFileParams();
|
||||
params.setFilePath(filePath);
|
||||
params.setOldStr(oldStr);
|
||||
params.setNewStr(newStr);
|
||||
params.setStartLine(startLine);
|
||||
params.setEndLine(endLine);
|
||||
|
||||
executionLogger.logToolStep(callId, "edit_file", "参数验证", "验证文件路径和替换内容");
|
||||
|
||||
// Validate parameters
|
||||
String validation = validateToolParams(params);
|
||||
if (validation != null) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
executionLogger.logToolError(callId, "edit_file", "参数验证失败: " + validation, executionTime);
|
||||
return "Error: " + validation;
|
||||
}
|
||||
|
||||
String editDetails = startLine != null && endLine != null ?
|
||||
String.format("行号范围编辑: %d-%d行", startLine, endLine) : "字符串替换编辑";
|
||||
executionLogger.logFileOperation(callId, "编辑文件", filePath, editDetails);
|
||||
|
||||
// Execute the tool
|
||||
ToolResult result = execute(params).join();
|
||||
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
if (result.isSuccess()) {
|
||||
executionLogger.logToolSuccess(callId, "edit_file", "文件编辑成功", executionTime);
|
||||
return result.getLlmContent();
|
||||
} else {
|
||||
executionLogger.logToolError(callId, "edit_file", result.getErrorMessage(), executionTime);
|
||||
return "Error: " + result.getErrorMessage();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
executionLogger.logToolError(callId, "edit_file", "工具执行异常: " + e.getMessage(), executionTime);
|
||||
logger.error("Error in edit file tool", e);
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolResult> execute(EditFileParams params) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Path filePath = Paths.get(params.filePath);
|
||||
|
||||
// Check if file exists
|
||||
if (!Files.exists(filePath)) {
|
||||
return ToolResult.error("File not found: " + params.filePath);
|
||||
}
|
||||
|
||||
// Check if it's a file
|
||||
if (!Files.isRegularFile(filePath)) {
|
||||
return ToolResult.error("Path is not a regular file: " + params.filePath);
|
||||
}
|
||||
|
||||
// 读取原始内容
|
||||
String originalContent = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||
|
||||
// 执行编辑
|
||||
String newContent = performEdit(originalContent, params);
|
||||
if (newContent == null) {
|
||||
return ToolResult.error("Could not find the specified text to replace in file: " + params.filePath);
|
||||
}
|
||||
|
||||
// 创建备份
|
||||
if (shouldCreateBackup()) {
|
||||
createBackup(filePath, originalContent);
|
||||
}
|
||||
|
||||
// Write new content
|
||||
Files.writeString(filePath, newContent, StandardCharsets.UTF_8);
|
||||
|
||||
// Generate differences and results
|
||||
String diff = generateDiff(filePath.getFileName().toString(), originalContent, newContent);
|
||||
String relativePath = getRelativePath(filePath);
|
||||
String successMessage = String.format("Successfully edited file: %s", params.filePath);
|
||||
|
||||
return ToolResult.success(successMessage, new FileDiff(diff, filePath.getFileName().toString()));
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Error editing file: " + params.filePath, e);
|
||||
return ToolResult.error("Error editing file: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error editing file: " + params.filePath, e);
|
||||
return ToolResult.error("Unexpected error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String performEdit(String content, EditFileParams params) {
|
||||
// If line numbers are specified, use line numbers to assist in finding
|
||||
if (params.startLine != null && params.endLine != null) {
|
||||
return performEditWithLineNumbers(content, params);
|
||||
} else {
|
||||
return performSimpleEdit(content, params);
|
||||
}
|
||||
}
|
||||
|
||||
private String performSimpleEdit(String content, EditFileParams params) {
|
||||
// Simple string replacement
|
||||
if (!content.contains(params.oldStr)) {
|
||||
return null; // Cannot find string to replace
|
||||
}
|
||||
|
||||
// Only replace the first match to avoid unexpected multiple replacements
|
||||
int index = content.indexOf(params.oldStr);
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return content.substring(0, index) + params.newStr + content.substring(index + params.oldStr.length());
|
||||
}
|
||||
|
||||
private String performEditWithLineNumbers(String content, EditFileParams params) {
|
||||
String[] lines = content.split("\n", -1); // -1 preserve trailing empty lines
|
||||
|
||||
// Validate line number range
|
||||
if (params.startLine > lines.length || params.endLine > lines.length) {
|
||||
return null; // Line number out of range
|
||||
}
|
||||
|
||||
// Extract content from specified line range
|
||||
StringBuilder targetContent = new StringBuilder();
|
||||
for (int i = params.startLine - 1; i < params.endLine; i++) {
|
||||
if (i > params.startLine - 1) {
|
||||
targetContent.append("\n");
|
||||
}
|
||||
targetContent.append(lines[i]);
|
||||
}
|
||||
|
||||
// 检查是否匹配
|
||||
if (!targetContent.toString().equals(params.oldStr)) {
|
||||
return null; // 指定行范围的内容与old_str不匹配
|
||||
}
|
||||
|
||||
// 执行替换
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
// 添加前面的行
|
||||
for (int i = 0; i < params.startLine - 1; i++) {
|
||||
if (i > 0) result.append("\n");
|
||||
result.append(lines[i]);
|
||||
}
|
||||
|
||||
// 添加新内容
|
||||
if (params.startLine > 1) result.append("\n");
|
||||
result.append(params.newStr);
|
||||
|
||||
// 添加后面的行
|
||||
for (int i = params.endLine; i < lines.length; i++) {
|
||||
result.append("\n");
|
||||
result.append(lines[i]);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String generateDiff(String fileName, String oldContent, String newContent) {
|
||||
try {
|
||||
List<String> oldLines = Arrays.asList(oldContent.split("\n"));
|
||||
List<String> newLines = Arrays.asList(newContent.split("\n"));
|
||||
|
||||
Patch<String> patch = DiffUtils.diff(oldLines, newLines);
|
||||
List<String> unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(
|
||||
fileName + " (Original)",
|
||||
fileName + " (Edited)",
|
||||
oldLines,
|
||||
patch,
|
||||
3 // context lines
|
||||
);
|
||||
|
||||
return String.join("\n", unifiedDiff);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Could not generate diff", e);
|
||||
return "Diff generation failed: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private void createBackup(Path filePath, String content) throws IOException {
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
||||
String backupFileName = filePath.getFileName().toString() + ".backup." + timestamp;
|
||||
Path backupPath = filePath.getParent().resolve(backupFileName);
|
||||
|
||||
Files.writeString(backupPath, content, StandardCharsets.UTF_8);
|
||||
logger.info("Created backup: {}", backupPath);
|
||||
}
|
||||
|
||||
private boolean shouldCreateBackup() {
|
||||
return true; // 总是创建备份
|
||||
}
|
||||
|
||||
private boolean isWithinWorkspace(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
Path normalizedPath = filePath.normalize();
|
||||
return normalizedPath.startsWith(workspaceRoot.normalize());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not resolve workspace path", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String getRelativePath(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory);
|
||||
return workspaceRoot.relativize(filePath).toString();
|
||||
} catch (Exception e) {
|
||||
return filePath.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文件参数
|
||||
*/
|
||||
public static class EditFileParams {
|
||||
@JsonProperty("file_path")
|
||||
private String filePath;
|
||||
|
||||
@JsonProperty("old_str")
|
||||
private String oldStr;
|
||||
|
||||
@JsonProperty("new_str")
|
||||
private String newStr;
|
||||
|
||||
@JsonProperty("start_line")
|
||||
private Integer startLine;
|
||||
|
||||
@JsonProperty("end_line")
|
||||
private Integer endLine;
|
||||
|
||||
// 构造器
|
||||
public EditFileParams() {}
|
||||
|
||||
public EditFileParams(String filePath, String oldStr, String newStr) {
|
||||
this.filePath = filePath;
|
||||
this.oldStr = oldStr;
|
||||
this.newStr = newStr;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getFilePath() { return filePath; }
|
||||
public void setFilePath(String filePath) { this.filePath = filePath; }
|
||||
|
||||
public String getOldStr() { return oldStr; }
|
||||
public void setOldStr(String oldStr) { this.oldStr = oldStr; }
|
||||
|
||||
public String getNewStr() { return newStr; }
|
||||
public void setNewStr(String newStr) { this.newStr = newStr; }
|
||||
|
||||
public Integer getStartLine() { return startLine; }
|
||||
public void setStartLine(Integer startLine) { this.startLine = startLine; }
|
||||
|
||||
public Integer getEndLine() { return endLine; }
|
||||
public void setEndLine(Integer endLine) { this.endLine = endLine; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("EditFileParams{path='%s', oldStrLength=%d, newStrLength=%d, lines=%s-%s}",
|
||||
filePath,
|
||||
oldStr != null ? oldStr.length() : 0,
|
||||
newStr != null ? newStr.length() : 0,
|
||||
startLine, endLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.utils.PathUtils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 文件操作工具类 - 使用Spring AI 1.0.0 @Tool注解
|
||||
*/
|
||||
@Component
|
||||
public class FileOperationTools {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FileOperationTools.class);
|
||||
|
||||
private final String rootDirectory;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
// 在构造函数中
|
||||
public FileOperationTools(AppProperties appProperties) {
|
||||
this.appProperties = appProperties;
|
||||
// 使用规范化的路径
|
||||
this.rootDirectory = PathUtils.normalizePath(appProperties.getWorkspace().getRootDirectory());
|
||||
}
|
||||
|
||||
@Tool(description = "Read the content of a file from the local filesystem. Supports pagination for large files.")
|
||||
public String readFile(
|
||||
@ToolParam(description = "The absolute path to the file to read. Must be within the workspace directory.")
|
||||
String absolutePath,
|
||||
@ToolParam(description = "Optional: For text files, the 0-based line number to start reading from.", required = false)
|
||||
Integer offset,
|
||||
@ToolParam(description = "Optional: For text files, the number of lines to read from the offset.", required = false)
|
||||
Integer limit) {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
logger.debug("Starting readFile operation for: {}", absolutePath);
|
||||
// 验证路径
|
||||
String validationError = validatePath(absolutePath);
|
||||
if (validationError != null) {
|
||||
return "Error: " + validationError;
|
||||
}
|
||||
|
||||
Path filePath = Paths.get(absolutePath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!Files.exists(filePath)) {
|
||||
return "Error: File not found: " + absolutePath;
|
||||
}
|
||||
|
||||
// 检查是否为文件
|
||||
if (!Files.isRegularFile(filePath)) {
|
||||
return "Error: Path is not a regular file: " + absolutePath;
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
long fileSize = Files.size(filePath);
|
||||
if (fileSize > appProperties.getWorkspace().getMaxFileSize()) {
|
||||
return "Error: File too large: " + fileSize + " bytes. Maximum allowed: " +
|
||||
appProperties.getWorkspace().getMaxFileSize() + " bytes";
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
String fileName = filePath.getFileName().toString();
|
||||
if (!isAllowedFileType(fileName)) {
|
||||
return "Error: File type not allowed: " + fileName +
|
||||
". Allowed extensions: " + appProperties.getWorkspace().getAllowedExtensions();
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
if (offset != null && limit != null) {
|
||||
return readFileWithPagination(filePath, offset, limit);
|
||||
} else {
|
||||
return readFullFile(filePath);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.error("Error reading file: {} (duration: {}ms)", absolutePath, duration, e);
|
||||
return String.format("❌ Error reading file: %s\n⏱️ Duration: %dms\n🔍 Details: %s",
|
||||
absolutePath, duration, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.error("Unexpected error reading file: {} (duration: {}ms)", absolutePath, duration, e);
|
||||
return String.format("❌ Unexpected error reading file: %s\n⏱️ Duration: %dms\n🔍 Details: %s",
|
||||
absolutePath, duration, e.getMessage());
|
||||
} finally {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.debug("Completed readFile operation for: {} (duration: {}ms)", absolutePath, duration);
|
||||
}
|
||||
}
|
||||
|
||||
@Tool(description = "Write content to a file. Creates new file or overwrites existing file.")
|
||||
public String writeFile(
|
||||
@ToolParam(description = "The absolute path to the file to write. Must be within the workspace directory.")
|
||||
String filePath,
|
||||
@ToolParam(description = "The content to write to the file")
|
||||
String content) {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
logger.debug("Starting writeFile operation for: {}", filePath);
|
||||
// 验证路径
|
||||
String validationError = validatePath(filePath);
|
||||
if (validationError != null) {
|
||||
return "Error: " + validationError;
|
||||
}
|
||||
|
||||
// 验证内容大小
|
||||
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
|
||||
if (contentBytes.length > appProperties.getWorkspace().getMaxFileSize()) {
|
||||
return "Error: Content too large: " + contentBytes.length + " bytes. Maximum allowed: " +
|
||||
appProperties.getWorkspace().getMaxFileSize() + " bytes";
|
||||
}
|
||||
|
||||
Path path = Paths.get(filePath);
|
||||
boolean isNewFile = !Files.exists(path);
|
||||
|
||||
// 确保父目录存在
|
||||
Files.createDirectories(path.getParent());
|
||||
|
||||
// 写入文件
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
|
||||
long lineCount = content.lines().count();
|
||||
String absolutePath = path.toAbsolutePath().toString();
|
||||
String relativePath = getRelativePath(path);
|
||||
|
||||
if (isNewFile) {
|
||||
return String.format("Successfully created file:\n📁 Full path: %s\n📂 Relative path: %s\n📊 Stats: %d lines, %d bytes",
|
||||
absolutePath, relativePath, lineCount, contentBytes.length);
|
||||
} else {
|
||||
return String.format("Successfully wrote to file:\n📁 Full path: %s\n📂 Relative path: %s\n📊 Stats: %d lines, %d bytes",
|
||||
absolutePath, relativePath, lineCount, contentBytes.length);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.error("Error writing file: {} (duration: {}ms)", filePath, duration, e);
|
||||
return String.format("❌ Error writing file: %s\n⏱️ Duration: %dms\n🔍 Details: %s",
|
||||
filePath, duration, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.error("Unexpected error writing file: {} (duration: {}ms)", filePath, duration, e);
|
||||
return String.format("❌ Unexpected error writing file: %s\n⏱️ Duration: %dms\n🔍 Details: %s",
|
||||
filePath, duration, e.getMessage());
|
||||
} finally {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
logger.debug("Completed writeFile operation for: {} (duration: {}ms)", filePath, duration);
|
||||
}
|
||||
}
|
||||
|
||||
@Tool(description = "Edit a file by replacing specific text content.")
|
||||
public String editFile(
|
||||
@ToolParam(description = "The absolute path to the file to edit. Must be within the workspace directory.")
|
||||
String filePath,
|
||||
@ToolParam(description = "The text to find and replace in the file")
|
||||
String oldText,
|
||||
@ToolParam(description = "The new text to replace the old text with")
|
||||
String newText) {
|
||||
|
||||
try {
|
||||
// 验证路径
|
||||
String validationError = validatePath(filePath);
|
||||
if (validationError != null) {
|
||||
return "Error: " + validationError;
|
||||
}
|
||||
|
||||
Path path = Paths.get(filePath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!Files.exists(path)) {
|
||||
return "Error: File not found: " + filePath;
|
||||
}
|
||||
|
||||
// 检查是否为文件
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return "Error: Path is not a regular file: " + filePath;
|
||||
}
|
||||
|
||||
// 读取原始内容
|
||||
String originalContent = Files.readString(path, StandardCharsets.UTF_8);
|
||||
|
||||
// 执行替换
|
||||
if (!originalContent.contains(oldText)) {
|
||||
return "Error: Could not find the specified text to replace in file: " + filePath;
|
||||
}
|
||||
|
||||
String newContent = originalContent.replace(oldText, newText);
|
||||
|
||||
// 写入新内容
|
||||
Files.writeString(path, newContent, StandardCharsets.UTF_8);
|
||||
|
||||
String absolutePath = path.toAbsolutePath().toString();
|
||||
String relativePath = getRelativePath(path);
|
||||
return String.format("Successfully edited file:\n📁 Full path: %s\n📂 Relative path: %s\n✏️ Replaced text successfully",
|
||||
absolutePath, relativePath);
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Error editing file: " + filePath, e);
|
||||
return "Error editing file: " + e.getMessage();
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error editing file: " + filePath, e);
|
||||
return "Unexpected error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Tool(description = "List the contents of a directory.")
|
||||
public String listDirectory(
|
||||
@ToolParam(description = "The absolute path to the directory to list. Must be within the workspace directory.")
|
||||
String directoryPath,
|
||||
@ToolParam(description = "Whether to list contents recursively", required = false)
|
||||
Boolean recursive) {
|
||||
|
||||
try {
|
||||
// 验证路径
|
||||
String validationError = validatePath(directoryPath);
|
||||
if (validationError != null) {
|
||||
return "Error: " + validationError;
|
||||
}
|
||||
|
||||
Path path = Paths.get(directoryPath);
|
||||
|
||||
// 检查目录是否存在
|
||||
if (!Files.exists(path)) {
|
||||
return "Error: Directory not found: " + directoryPath;
|
||||
}
|
||||
|
||||
// 检查是否为目录
|
||||
if (!Files.isDirectory(path)) {
|
||||
return "Error: Path is not a directory: " + directoryPath;
|
||||
}
|
||||
|
||||
boolean isRecursive = recursive != null && recursive;
|
||||
String absolutePath = path.toAbsolutePath().toString();
|
||||
String relativePath = getRelativePath(path);
|
||||
|
||||
if (isRecursive) {
|
||||
return listDirectoryRecursive(path, absolutePath, relativePath);
|
||||
} else {
|
||||
return listDirectorySimple(path, absolutePath, relativePath);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Error listing directory: " + directoryPath, e);
|
||||
return "Error listing directory: " + e.getMessage();
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error listing directory: " + directoryPath, e);
|
||||
return "Unexpected error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private String validatePath(String path) {
|
||||
if (path == null || path.trim().isEmpty()) {
|
||||
return "Path cannot be empty";
|
||||
}
|
||||
|
||||
Path filePath = Paths.get(path);
|
||||
|
||||
// 验证是否为绝对路径
|
||||
if (!filePath.isAbsolute()) {
|
||||
return "Path must be absolute: " + path;
|
||||
}
|
||||
|
||||
// 验证是否在工作目录内
|
||||
if (!isWithinWorkspace(filePath)) {
|
||||
return "Path must be within the workspace directory (" + rootDirectory + "): " + path;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isWithinWorkspace(Path path) {
|
||||
try {
|
||||
Path workspacePath = Paths.get(rootDirectory).toRealPath();
|
||||
Path targetPath = path.toRealPath();
|
||||
return targetPath.startsWith(workspacePath);
|
||||
} catch (IOException e) {
|
||||
// 如果路径不存在,检查其父目录
|
||||
try {
|
||||
Path workspacePath = Paths.get(rootDirectory).toRealPath();
|
||||
Path normalizedPath = path.normalize();
|
||||
return normalizedPath.startsWith(workspacePath.normalize());
|
||||
} catch (IOException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAllowedFileType(String fileName) {
|
||||
List<String> allowedExtensions = appProperties.getWorkspace().getAllowedExtensions();
|
||||
return allowedExtensions.stream()
|
||||
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
|
||||
}
|
||||
|
||||
private String getRelativePath(Path path) {
|
||||
try {
|
||||
Path workspacePath = Paths.get(rootDirectory);
|
||||
return workspacePath.relativize(path).toString();
|
||||
} catch (Exception e) {
|
||||
return path.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private String readFullFile(Path filePath) throws IOException {
|
||||
String content = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||
String absolutePath = filePath.toAbsolutePath().toString();
|
||||
String relativePath = getRelativePath(filePath);
|
||||
|
||||
long lineCount = content.lines().count();
|
||||
return String.format("📁 Full path: %s\n📂 Relative path: %s\n📊 Stats: %d lines, %d bytes\n\n📄 Content:\n%s",
|
||||
absolutePath, relativePath, lineCount, content.getBytes(StandardCharsets.UTF_8).length, content);
|
||||
}
|
||||
|
||||
private String readFileWithPagination(Path filePath, int offset, int limit) throws IOException {
|
||||
List<String> allLines = Files.readAllLines(filePath, StandardCharsets.UTF_8);
|
||||
|
||||
if (offset >= allLines.size()) {
|
||||
return "Error: Offset " + offset + " is beyond file length (" + allLines.size() + " lines)";
|
||||
}
|
||||
|
||||
int endIndex = Math.min(offset + limit, allLines.size());
|
||||
List<String> selectedLines = allLines.subList(offset, endIndex);
|
||||
String content = String.join("\n", selectedLines);
|
||||
|
||||
String absolutePath = filePath.toAbsolutePath().toString();
|
||||
String relativePath = getRelativePath(filePath);
|
||||
return String.format("📁 Full path: %s\n📂 Relative path: %s\n📊 Showing lines %d-%d of %d total\n\n📄 Content:\n%s",
|
||||
absolutePath, relativePath, offset + 1, endIndex, allLines.size(), content);
|
||||
}
|
||||
|
||||
private String listDirectorySimple(Path path, String absolutePath, String relativePath) throws IOException {
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("📁 Full path: ").append(absolutePath).append("\n");
|
||||
result.append("📂 Relative path: ").append(relativePath).append("\n\n");
|
||||
result.append("📋 Directory contents:\n");
|
||||
|
||||
try (Stream<Path> entries = Files.list(path)) {
|
||||
List<Path> sortedEntries = entries.sorted().collect(Collectors.toList());
|
||||
|
||||
for (Path entry : sortedEntries) {
|
||||
String name = entry.getFileName().toString();
|
||||
String entryAbsolutePath = entry.toAbsolutePath().toString();
|
||||
if (Files.isDirectory(entry)) {
|
||||
result.append("📁 [DIR] ").append(name).append("/\n");
|
||||
result.append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||
} else {
|
||||
long size = Files.size(entry);
|
||||
result.append("📄 [FILE] ").append(name).append(" (").append(size).append(" bytes)\n");
|
||||
result.append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String listDirectoryRecursive(Path path, String absolutePath, String relativePath) throws IOException {
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("📁 Full path: ").append(absolutePath).append("\n");
|
||||
result.append("📂 Relative path: ").append(relativePath).append("\n\n");
|
||||
result.append("🌳 Directory tree (recursive):\n");
|
||||
|
||||
try (Stream<Path> entries = Files.walk(path)) {
|
||||
entries.sorted()
|
||||
.forEach(entry -> {
|
||||
if (!entry.equals(path)) {
|
||||
String entryAbsolutePath = entry.toAbsolutePath().toString();
|
||||
String entryRelativePath = getRelativePath(entry);
|
||||
|
||||
// 计算缩进级别
|
||||
int depth = entry.getNameCount() - path.getNameCount();
|
||||
String indent = " ".repeat(depth);
|
||||
|
||||
if (Files.isDirectory(entry)) {
|
||||
result.append(indent).append("📁 ").append(entryRelativePath).append("/\n");
|
||||
result.append(indent).append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||
} else {
|
||||
try {
|
||||
long size = Files.size(entry);
|
||||
result.append(indent).append("📄 ").append(entryRelativePath).append(" (").append(size).append(" bytes)\n");
|
||||
result.append(indent).append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||
} catch (IOException e) {
|
||||
result.append(indent).append("📄 ").append(entryRelativePath).append(" (size unknown)\n");
|
||||
result.append(indent).append(" └─ ").append(entryAbsolutePath).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.schema.JsonSchema;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 目录列表工具
|
||||
* 列出指定目录的文件和子目录,支持递归列表
|
||||
*/
|
||||
@Component
|
||||
public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryParams> {
|
||||
|
||||
private final String rootDirectory;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
public ListDirectoryTool(AppProperties appProperties) {
|
||||
super(
|
||||
"list_directory",
|
||||
"ListDirectory",
|
||||
"Lists files and directories in the specified path. " +
|
||||
"Supports recursive listing and filtering. " +
|
||||
"Shows file sizes, modification times, and types. " +
|
||||
"Use absolute paths within the workspace directory.",
|
||||
createSchema()
|
||||
);
|
||||
this.appProperties = appProperties;
|
||||
this.rootDirectory = appProperties.getWorkspace().getRootDirectory();
|
||||
}
|
||||
|
||||
private static String getWorkspaceBasePath() {
|
||||
return Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
||||
}
|
||||
|
||||
private static String getPathExample(String subPath) {
|
||||
return "Example: \"" + Paths.get(getWorkspaceBasePath(), subPath).toString() + "\"";
|
||||
}
|
||||
|
||||
private static JsonSchema createSchema() {
|
||||
return JsonSchema.object()
|
||||
.addProperty("path", JsonSchema.string(
|
||||
"MUST be an absolute path to the directory to list. Path must be within the workspace directory (" +
|
||||
getWorkspaceBasePath() + "). " +
|
||||
getPathExample("project/src") + ". " +
|
||||
"Relative paths are NOT allowed."
|
||||
))
|
||||
.addProperty("recursive", JsonSchema.bool(
|
||||
"Optional: Whether to list files recursively in subdirectories. Default: false"
|
||||
))
|
||||
.addProperty("max_depth", JsonSchema.integer(
|
||||
"Optional: Maximum depth for recursive listing. Default: 3, Maximum: 10"
|
||||
).minimum(1).maximum(10))
|
||||
.addProperty("show_hidden", JsonSchema.bool(
|
||||
"Optional: Whether to show hidden files (starting with '.'). Default: false"
|
||||
))
|
||||
.required("path");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateToolParams(ListDirectoryParams params) {
|
||||
String baseValidation = super.validateToolParams(params);
|
||||
if (baseValidation != null) {
|
||||
return baseValidation;
|
||||
}
|
||||
|
||||
// 验证路径
|
||||
if (params.path == null || params.path.trim().isEmpty()) {
|
||||
return "Directory path cannot be empty";
|
||||
}
|
||||
|
||||
Path dirPath = Paths.get(params.path);
|
||||
|
||||
// 验证是否为绝对路径
|
||||
if (!dirPath.isAbsolute()) {
|
||||
return "Directory path must be absolute: " + params.path;
|
||||
}
|
||||
|
||||
// 验证是否在工作目录内
|
||||
if (!isWithinWorkspace(dirPath)) {
|
||||
return "Directory path must be within the workspace directory (" + rootDirectory + "): " + params.path;
|
||||
}
|
||||
|
||||
// 验证最大深度
|
||||
if (params.maxDepth != null && (params.maxDepth < 1 || params.maxDepth > 10)) {
|
||||
return "Max depth must be between 1 and 10";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolResult> execute(ListDirectoryParams params) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Path dirPath = Paths.get(params.path);
|
||||
|
||||
// 检查目录是否存在
|
||||
if (!Files.exists(dirPath)) {
|
||||
return ToolResult.error("Directory not found: " + params.path);
|
||||
}
|
||||
|
||||
// 检查是否为目录
|
||||
if (!Files.isDirectory(dirPath)) {
|
||||
return ToolResult.error("Path is not a directory: " + params.path);
|
||||
}
|
||||
|
||||
// 列出文件和目录
|
||||
List<FileInfo> fileInfos = listFiles(dirPath, params);
|
||||
|
||||
// 生成输出
|
||||
String content = formatFileList(fileInfos, params);
|
||||
String relativePath = getRelativePath(dirPath);
|
||||
String displayMessage = String.format("Listed directory: %s (%d items)",
|
||||
relativePath, fileInfos.size());
|
||||
|
||||
return ToolResult.success(content, displayMessage);
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Error listing directory: " + params.path, e);
|
||||
return ToolResult.error("Error listing directory: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error listing directory: " + params.path, e);
|
||||
return ToolResult.error("Unexpected error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private List<FileInfo> listFiles(Path dirPath, ListDirectoryParams params) throws IOException {
|
||||
List<FileInfo> fileInfos = new ArrayList<>();
|
||||
|
||||
if (params.recursive != null && params.recursive) {
|
||||
int maxDepth = params.maxDepth != null ? params.maxDepth : 3;
|
||||
listFilesRecursive(dirPath, fileInfos, 0, maxDepth, params);
|
||||
} else {
|
||||
listFilesInDirectory(dirPath, fileInfos, params);
|
||||
}
|
||||
|
||||
// 排序:目录在前,然后按名称排序
|
||||
fileInfos.sort(Comparator
|
||||
.comparing((FileInfo f) -> !f.isDirectory())
|
||||
.thenComparing(FileInfo::getName));
|
||||
|
||||
return fileInfos;
|
||||
}
|
||||
|
||||
private void listFilesInDirectory(Path dirPath, List<FileInfo> fileInfos, ListDirectoryParams params) throws IOException {
|
||||
try (Stream<Path> stream = Files.list(dirPath)) {
|
||||
stream.forEach(path -> {
|
||||
try {
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
// 跳过隐藏文件(除非明确要求显示)
|
||||
if (!params.showHidden && fileName.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileInfo fileInfo = createFileInfo(path, dirPath);
|
||||
fileInfos.add(fileInfo);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not get info for file: " + path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void listFilesRecursive(Path dirPath, List<FileInfo> fileInfos, int currentDepth, int maxDepth, ListDirectoryParams params) throws IOException {
|
||||
if (currentDepth >= maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Stream<Path> stream = Files.list(dirPath)) {
|
||||
List<Path> paths = stream.collect(Collectors.toList());
|
||||
|
||||
for (Path path : paths) {
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
// 跳过隐藏文件(除非明确要求显示)
|
||||
if (!params.showHidden && fileName.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
FileInfo fileInfo = createFileInfo(path, Paths.get(params.path));
|
||||
fileInfos.add(fileInfo);
|
||||
|
||||
// 如果是目录,递归列出
|
||||
if (Files.isDirectory(path)) {
|
||||
listFilesRecursive(path, fileInfos, currentDepth + 1, maxDepth, params);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not get info for file: " + path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private FileInfo createFileInfo(Path path, Path basePath) throws IOException {
|
||||
String name = path.getFileName().toString();
|
||||
boolean isDirectory = Files.isDirectory(path);
|
||||
long size = isDirectory ? 0 : Files.size(path);
|
||||
|
||||
LocalDateTime lastModified = LocalDateTime.ofInstant(
|
||||
Files.getLastModifiedTime(path).toInstant(),
|
||||
ZoneId.systemDefault()
|
||||
);
|
||||
|
||||
String relativePath = basePath.relativize(path).toString();
|
||||
|
||||
return new FileInfo(name, relativePath, isDirectory, size, lastModified);
|
||||
}
|
||||
|
||||
private String formatFileList(List<FileInfo> fileInfos, ListDirectoryParams params) {
|
||||
if (fileInfos.isEmpty()) {
|
||||
return "Directory is empty.";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(String.format("Directory listing for: %s\n", getRelativePath(Paths.get(params.path))));
|
||||
sb.append(String.format("Total items: %d\n\n", fileInfos.size()));
|
||||
|
||||
// 表头
|
||||
sb.append(String.format("%-4s %-40s %-12s %-20s %s\n",
|
||||
"Type", "Name", "Size", "Modified", "Path"));
|
||||
sb.append("-".repeat(80)).append("\n");
|
||||
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
for (FileInfo fileInfo : fileInfos) {
|
||||
String type = fileInfo.isDirectory() ? "DIR" : "FILE";
|
||||
String sizeStr = fileInfo.isDirectory() ? "-" : formatFileSize(fileInfo.getSize());
|
||||
String modifiedStr = fileInfo.getLastModified().format(formatter);
|
||||
|
||||
sb.append(String.format("%-4s %-40s %-12s %-20s %s\n",
|
||||
type,
|
||||
truncate(fileInfo.getName(), 40),
|
||||
sizeStr,
|
||||
modifiedStr,
|
||||
fileInfo.getRelativePath()
|
||||
));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatFileSize(long bytes) {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
|
||||
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024));
|
||||
return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
|
||||
}
|
||||
|
||||
private String truncate(String str, int maxLength) {
|
||||
if (str.length() <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
return str.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
|
||||
private boolean isWithinWorkspace(Path dirPath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
Path normalizedPath = dirPath.normalize();
|
||||
return normalizedPath.startsWith(workspaceRoot.normalize());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not resolve workspace path", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String getRelativePath(Path dirPath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory);
|
||||
return workspaceRoot.relativize(dirPath).toString();
|
||||
} catch (Exception e) {
|
||||
return dirPath.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件信息
|
||||
*/
|
||||
public static class FileInfo {
|
||||
private final String name;
|
||||
private final String relativePath;
|
||||
private final boolean isDirectory;
|
||||
private final long size;
|
||||
private final LocalDateTime lastModified;
|
||||
|
||||
public FileInfo(String name, String relativePath, boolean isDirectory, long size, LocalDateTime lastModified) {
|
||||
this.name = name;
|
||||
this.relativePath = relativePath;
|
||||
this.isDirectory = isDirectory;
|
||||
this.size = size;
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getName() { return name; }
|
||||
public String getRelativePath() { return relativePath; }
|
||||
public boolean isDirectory() { return isDirectory; }
|
||||
public long getSize() { return size; }
|
||||
public LocalDateTime getLastModified() { return lastModified; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表目录参数
|
||||
*/
|
||||
public static class ListDirectoryParams {
|
||||
private String path;
|
||||
private Boolean recursive;
|
||||
|
||||
@JsonProperty("max_depth")
|
||||
private Integer maxDepth;
|
||||
|
||||
@JsonProperty("show_hidden")
|
||||
private Boolean showHidden;
|
||||
|
||||
// 构造器
|
||||
public ListDirectoryParams() {}
|
||||
|
||||
public ListDirectoryParams(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
|
||||
public Boolean getRecursive() { return recursive; }
|
||||
public void setRecursive(Boolean recursive) { this.recursive = recursive; }
|
||||
|
||||
public Integer getMaxDepth() { return maxDepth; }
|
||||
public void setMaxDepth(Integer maxDepth) { this.maxDepth = maxDepth; }
|
||||
|
||||
public Boolean getShowHidden() { return showHidden; }
|
||||
public void setShowHidden(Boolean showHidden) { this.showHidden = showHidden; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ListDirectoryParams{path='%s', recursive=%s, maxDepth=%d}",
|
||||
path, recursive, maxDepth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.model.ProjectType;
|
||||
import com.example.demo.schema.JsonSchema;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Project scaffolding tool
|
||||
* Quickly create standard project structure and template files
|
||||
*/
|
||||
@Component
|
||||
public class ProjectScaffoldTool extends BaseTool<ProjectScaffoldTool.ScaffoldParams> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProjectScaffoldTool.class);
|
||||
|
||||
private final String rootDirectory;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
public ProjectScaffoldTool(AppProperties appProperties) {
|
||||
super(
|
||||
"scaffold_project",
|
||||
"ScaffoldProject",
|
||||
"Create a new project with standard structure and template files. " +
|
||||
"Supports various project types including Java, Node.js, Python, and more.",
|
||||
createSchema()
|
||||
);
|
||||
this.appProperties = appProperties;
|
||||
this.rootDirectory = appProperties.getWorkspace().getRootDirectory();
|
||||
}
|
||||
|
||||
private static JsonSchema createSchema() {
|
||||
return JsonSchema.object()
|
||||
.addProperty("project_name", JsonSchema.string(
|
||||
"Name of the project to create. Will be used as directory name and in templates."
|
||||
))
|
||||
.addProperty("project_type", JsonSchema.string(
|
||||
"Type of project to create. Options: " +
|
||||
"java_maven, java_gradle, spring_boot, " +
|
||||
"node_js, react, vue, angular, " +
|
||||
"python, django, flask, " +
|
||||
"html_static"
|
||||
))
|
||||
.addProperty("project_path", JsonSchema.string(
|
||||
"Optional: Custom path where to create the project. " +
|
||||
"If not provided, will create in workspace root."
|
||||
))
|
||||
.addProperty("template_variables", JsonSchema.object(
|
||||
))
|
||||
.addProperty("include_git", JsonSchema.bool(
|
||||
"Whether to initialize Git repository. Default: true"
|
||||
))
|
||||
.addProperty("include_readme", JsonSchema.bool(
|
||||
"Whether to create README.md file. Default: true"
|
||||
))
|
||||
.addProperty("include_gitignore", JsonSchema.bool(
|
||||
"Whether to create .gitignore file. Default: true"
|
||||
))
|
||||
.required("project_name", "project_type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateToolParams(ScaffoldParams params) {
|
||||
String baseValidation = super.validateToolParams(params);
|
||||
if (baseValidation != null) {
|
||||
return baseValidation;
|
||||
}
|
||||
|
||||
if (params.projectName == null || params.projectName.trim().isEmpty()) {
|
||||
return "Project name cannot be empty";
|
||||
}
|
||||
|
||||
if (params.projectType == null || params.projectType.trim().isEmpty()) {
|
||||
return "Project type cannot be empty";
|
||||
}
|
||||
|
||||
// 验证项目名称格式
|
||||
if (!params.projectName.matches("[a-zA-Z0-9_-]+")) {
|
||||
return "Project name can only contain letters, numbers, underscores, and hyphens";
|
||||
}
|
||||
|
||||
// 验证项目类型
|
||||
try {
|
||||
ProjectType.valueOf(params.projectType.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return "Invalid project type: " + params.projectType;
|
||||
}
|
||||
|
||||
// 验证项目路径
|
||||
if (params.projectPath != null) {
|
||||
Path projectPath = Paths.get(params.projectPath);
|
||||
if (!isWithinWorkspace(projectPath)) {
|
||||
return "Project path must be within workspace: " + params.projectPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project scaffold tool method for Spring AI integration
|
||||
*/
|
||||
@Tool(name = "scaffold_project", description = "Create a new project with standard structure and template files")
|
||||
public String scaffoldProject(String projectName, String projectType, String projectPath, Boolean includeGit, Boolean includeReadme, Boolean includeGitignore) {
|
||||
try {
|
||||
ScaffoldParams params = new ScaffoldParams();
|
||||
params.setProjectName(projectName);
|
||||
params.setProjectType(projectType);
|
||||
params.setProjectPath(projectPath);
|
||||
params.setIncludeGit(includeGit != null ? includeGit : true);
|
||||
params.setIncludeReadme(includeReadme != null ? includeReadme : true);
|
||||
params.setIncludeGitignore(includeGitignore != null ? includeGitignore : true);
|
||||
|
||||
// Validate parameters
|
||||
String validation = validateToolParams(params);
|
||||
if (validation != null) {
|
||||
return "Error: " + validation;
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
ToolResult result = execute(params).join();
|
||||
|
||||
if (result.isSuccess()) {
|
||||
return result.getLlmContent();
|
||||
} else {
|
||||
return "Error: " + result.getErrorMessage();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error in scaffold project tool", e);
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolResult> execute(ScaffoldParams params) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
logger.info("Creating project scaffold: {} ({})", params.projectName, params.projectType);
|
||||
|
||||
// 1. 确定项目路径
|
||||
Path projectPath = determineProjectPath(params);
|
||||
|
||||
// 2. 检查项目是否已存在
|
||||
if (Files.exists(projectPath)) {
|
||||
return ToolResult.error("Project directory already exists: " + projectPath);
|
||||
}
|
||||
|
||||
// 3. 创建项目目录
|
||||
Files.createDirectories(projectPath);
|
||||
|
||||
// 4. 获取项目类型
|
||||
ProjectType projectType = ProjectType.valueOf(params.projectType.toUpperCase());
|
||||
|
||||
// 5. 创建项目结构
|
||||
ScaffoldResult result = createProjectStructure(projectPath, projectType, params);
|
||||
|
||||
logger.info("Project scaffold created successfully: {}", projectPath);
|
||||
return ToolResult.success(result.getSummary(), result.getDetails());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error creating project scaffold", e);
|
||||
return ToolResult.error("Failed to create project: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定项目路径
|
||||
*/
|
||||
private Path determineProjectPath(ScaffoldParams params) {
|
||||
if (params.projectPath != null && !params.projectPath.trim().isEmpty()) {
|
||||
return Paths.get(params.projectPath, params.projectName);
|
||||
} else {
|
||||
return Paths.get(rootDirectory, params.projectName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建项目结构
|
||||
*/
|
||||
private ScaffoldResult createProjectStructure(Path projectPath, ProjectType projectType, ScaffoldParams params) throws IOException {
|
||||
ScaffoldResult result = new ScaffoldResult();
|
||||
|
||||
// 准备模板变量
|
||||
Map<String, String> variables = prepareTemplateVariables(params);
|
||||
|
||||
// 根据项目类型创建结构
|
||||
switch (projectType) {
|
||||
case JAVA_MAVEN:
|
||||
createJavaMavenProject(projectPath, variables, result);
|
||||
break;
|
||||
case SPRING_BOOT:
|
||||
createSpringBootProject(projectPath, variables, result);
|
||||
break;
|
||||
case NODE_JS:
|
||||
createNodeJsProject(projectPath, variables, result);
|
||||
break;
|
||||
case REACT:
|
||||
createReactProject(projectPath, variables, result);
|
||||
break;
|
||||
case PYTHON:
|
||||
createPythonProject(projectPath, variables, result);
|
||||
break;
|
||||
case HTML_STATIC:
|
||||
createHtmlStaticProject(projectPath, variables, result);
|
||||
break;
|
||||
default:
|
||||
createBasicProject(projectPath, variables, result);
|
||||
}
|
||||
|
||||
// 创建通用文件
|
||||
if (params.includeReadme == null || params.includeReadme) {
|
||||
createReadmeFile(projectPath, variables, result);
|
||||
}
|
||||
|
||||
if (params.includeGitignore == null || params.includeGitignore) {
|
||||
createGitignoreFile(projectPath, projectType, result);
|
||||
}
|
||||
|
||||
if (params.includeGit == null || params.includeGit) {
|
||||
initializeGitRepository(projectPath, result);
|
||||
}
|
||||
|
||||
result.generateSummary(projectPath, projectType);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备模板变量
|
||||
*/
|
||||
private Map<String, String> prepareTemplateVariables(ScaffoldParams params) {
|
||||
Map<String, String> variables = new HashMap<>();
|
||||
|
||||
// Default variables
|
||||
variables.put("PROJECT_NAME", params.projectName);
|
||||
variables.put("PROJECT_NAME_CAMEL", toCamelCase(params.projectName));
|
||||
variables.put("PROJECT_NAME_PASCAL", toPascalCase(params.projectName));
|
||||
variables.put("CURRENT_YEAR", String.valueOf(java.time.Year.now().getValue()));
|
||||
variables.put("AUTHOR", "Developer");
|
||||
variables.put("EMAIL", "developer@example.com");
|
||||
variables.put("VERSION", "1.0.0");
|
||||
variables.put("DESCRIPTION", "A new " + params.projectType + " project");
|
||||
|
||||
// User-provided variables
|
||||
if (params.templateVariables != null) {
|
||||
params.templateVariables.forEach((key, value) ->
|
||||
variables.put(key.toUpperCase(), String.valueOf(value)));
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Java Maven project
|
||||
*/
|
||||
private void createJavaMavenProject(Path projectPath, Map<String, String> variables, ScaffoldResult result) throws IOException {
|
||||
// Create directory structure
|
||||
createDirectory(projectPath.resolve("src/main/java"), result);
|
||||
createDirectory(projectPath.resolve("src/main/resources"), result);
|
||||
createDirectory(projectPath.resolve("src/test/java"), result);
|
||||
createDirectory(projectPath.resolve("src/test/resources"), result);
|
||||
|
||||
// Create pom.xml
|
||||
String pomContent = generatePomXml(variables);
|
||||
createFile(projectPath.resolve("pom.xml"), pomContent, result);
|
||||
|
||||
// Create main class
|
||||
String packagePath = "src/main/java/com/example/" + variables.get("PROJECT_NAME").toLowerCase();
|
||||
createDirectory(projectPath.resolve(packagePath), result);
|
||||
|
||||
String mainClassContent = generateJavaMainClass(variables);
|
||||
createFile(projectPath.resolve(packagePath + "/Application.java"), mainClassContent, result);
|
||||
|
||||
// Create test class
|
||||
String testPackagePath = "src/test/java/com/example/" + variables.get("PROJECT_NAME").toLowerCase();
|
||||
createDirectory(projectPath.resolve(testPackagePath), result);
|
||||
|
||||
String testClassContent = generateJavaTestClass(variables);
|
||||
createFile(projectPath.resolve(testPackagePath + "/ApplicationTest.java"), testClassContent, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Spring Boot项目
|
||||
*/
|
||||
private void createSpringBootProject(Path projectPath, Map<String, String> variables, ScaffoldResult result) throws IOException {
|
||||
// 先创建基本的Maven结构
|
||||
createJavaMavenProject(projectPath, variables, result);
|
||||
|
||||
// 覆盖pom.xml为Spring Boot版本
|
||||
String springBootPomContent = generateSpringBootPomXml(variables);
|
||||
createFile(projectPath.resolve("pom.xml"), springBootPomContent, result);
|
||||
|
||||
// 创建Spring Boot主类
|
||||
String packagePath = "src/main/java/com/example/" + variables.get("PROJECT_NAME").toLowerCase();
|
||||
String springBootMainClass = generateSpringBootMainClass(variables);
|
||||
createFile(projectPath.resolve(packagePath + "/Application.java"), springBootMainClass, result);
|
||||
|
||||
// 创建application.yml
|
||||
String applicationYml = generateApplicationYml(variables);
|
||||
createFile(projectPath.resolve("src/main/resources/application.yml"), applicationYml, result);
|
||||
|
||||
// 创建简单的Controller
|
||||
String controllerContent = generateSpringBootController(variables);
|
||||
createFile(projectPath.resolve(packagePath + "/controller/HelloController.java"), controllerContent, result);
|
||||
}
|
||||
|
||||
// 其他项目类型的创建方法将在后续实现
|
||||
private void createNodeJsProject(Path projectPath, Map<String, String> variables, ScaffoldResult result) throws IOException {
|
||||
// Node.js项目结构实现
|
||||
createFile(projectPath.resolve("package.json"), generatePackageJson(variables), result);
|
||||
createFile(projectPath.resolve("index.js"), generateNodeJsMainFile(variables), result);
|
||||
createDirectory(projectPath.resolve("src"), result);
|
||||
createDirectory(projectPath.resolve("test"), result);
|
||||
}
|
||||
|
||||
private void createReactProject(Path projectPath, Map<String, String> variables, ScaffoldResult result) throws IOException {
|
||||
// React项目结构实现
|
||||
createNodeJsProject(projectPath, variables, result);
|
||||
createDirectory(projectPath.resolve("public"), result);
|
||||
createDirectory(projectPath.resolve("src/components"), result);
|
||||
createFile(projectPath.resolve("public/index.html"), generateReactIndexHtml(variables), result);
|
||||
createFile(projectPath.resolve("src/App.js"), generateReactAppJs(variables), result);
|
||||
}
|
||||
|
||||
private void createPythonProject(Path projectPath, Map<String, String> variables, ScaffoldResult result) throws IOException {
|
||||
// Python项目结构实现
|
||||
createFile(projectPath.resolve("requirements.txt"), generateRequirementsTxt(variables), result);
|
||||
createFile(projectPath.resolve("main.py"), generatePythonMainFile(variables), result);
|
||||
createDirectory(projectPath.resolve("src"), result);
|
||||
createDirectory(projectPath.resolve("tests"), result);
|
||||
}
|
||||
|
||||
private void createHtmlStaticProject(Path projectPath, Map<String, String> variables, ScaffoldResult result) throws IOException {
|
||||
// 静态HTML项目结构实现
|
||||
createFile(projectPath.resolve("index.html"), generateStaticIndexHtml(variables), result);
|
||||
createDirectory(projectPath.resolve("css"), result);
|
||||
createDirectory(projectPath.resolve("js"), result);
|
||||
createDirectory(projectPath.resolve("images"), result);
|
||||
createFile(projectPath.resolve("css/style.css"), generateBasicCss(variables), result);
|
||||
createFile(projectPath.resolve("js/script.js"), generateBasicJs(variables), result);
|
||||
}
|
||||
|
||||
private void createBasicProject(Path projectPath, Map<String, String> variables, ScaffoldResult result) throws IOException {
|
||||
// 基本项目结构
|
||||
createDirectory(projectPath.resolve("src"), result);
|
||||
createDirectory(projectPath.resolve("docs"), result);
|
||||
createFile(projectPath.resolve("src/main.txt"), "Main file for " + variables.get("PROJECT_NAME"), result);
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private void createDirectory(Path path, ScaffoldResult result) throws IOException {
|
||||
Files.createDirectories(path);
|
||||
result.addCreatedItem("Directory: " + path.getFileName());
|
||||
}
|
||||
|
||||
private void createFile(Path path, String content, ScaffoldResult result) throws IOException {
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
result.addCreatedItem("File: " + path.getFileName());
|
||||
}
|
||||
|
||||
private void createReadmeFile(Path projectPath, Map<String, String> variables, ScaffoldResult result) throws IOException {
|
||||
String readmeContent = generateReadmeContent(variables);
|
||||
createFile(projectPath.resolve("README.md"), readmeContent, result);
|
||||
}
|
||||
|
||||
private void createGitignoreFile(Path projectPath, ProjectType projectType, ScaffoldResult result) throws IOException {
|
||||
String gitignoreContent = generateGitignoreContent(projectType);
|
||||
createFile(projectPath.resolve(".gitignore"), gitignoreContent, result);
|
||||
}
|
||||
|
||||
private void initializeGitRepository(Path projectPath, ScaffoldResult result) {
|
||||
try {
|
||||
// 简单的Git初始化 - 在实际项目中应该使用JGit库
|
||||
result.addCreatedItem("Git repository initialized");
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to initialize Git repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWithinWorkspace(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
Path normalizedPath = filePath.toRealPath();
|
||||
return normalizedPath.startsWith(workspaceRoot);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// String utility methods
|
||||
private String toCamelCase(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return str;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean capitalizeNext = false;
|
||||
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
char c = str.charAt(i);
|
||||
if (c == '-' || c == '_') {
|
||||
capitalizeNext = true;
|
||||
} else if (capitalizeNext) {
|
||||
result.append(Character.toUpperCase(c));
|
||||
capitalizeNext = false;
|
||||
} else if (i == 0) {
|
||||
result.append(Character.toLowerCase(c));
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String toPascalCase(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return str;
|
||||
}
|
||||
String camelCase = toCamelCase(str);
|
||||
return Character.toUpperCase(camelCase.charAt(0)) + camelCase.substring(1);
|
||||
}
|
||||
|
||||
// 模板生成方法 - 委托给模板服务
|
||||
private String generatePomXml(Map<String, String> variables) {
|
||||
// 这些方法需要注入ProjectTemplateService
|
||||
return "<!-- Maven POM.xml content -->";
|
||||
}
|
||||
|
||||
private String generateSpringBootPomXml(Map<String, String> variables) {
|
||||
return "<!-- Spring Boot POM.xml content -->";
|
||||
}
|
||||
|
||||
private String generateJavaMainClass(Map<String, String> variables) {
|
||||
return "// Java main class content";
|
||||
}
|
||||
|
||||
private String generateSpringBootMainClass(Map<String, String> variables) {
|
||||
return "// Spring Boot main class content";
|
||||
}
|
||||
|
||||
private String generateSpringBootController(Map<String, String> variables) {
|
||||
return "// Spring Boot controller content";
|
||||
}
|
||||
|
||||
private String generateJavaTestClass(Map<String, String> variables) {
|
||||
return "// Java test class content";
|
||||
}
|
||||
|
||||
private String generateApplicationYml(Map<String, String> variables) {
|
||||
return "# Application YAML content";
|
||||
}
|
||||
|
||||
private String generatePackageJson(Map<String, String> variables) {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
private String generateNodeJsMainFile(Map<String, String> variables) {
|
||||
return "// Node.js main file content";
|
||||
}
|
||||
|
||||
private String generateReactIndexHtml(Map<String, String> variables) {
|
||||
return "<!-- React index.html content -->";
|
||||
}
|
||||
|
||||
private String generateReactAppJs(Map<String, String> variables) {
|
||||
return "// React App.js content";
|
||||
}
|
||||
|
||||
private String generateRequirementsTxt(Map<String, String> variables) {
|
||||
return "# Python requirements";
|
||||
}
|
||||
|
||||
private String generatePythonMainFile(Map<String, String> variables) {
|
||||
return "# Python main file content";
|
||||
}
|
||||
|
||||
private String generateStaticIndexHtml(Map<String, String> variables) {
|
||||
return "<!-- Static HTML content -->";
|
||||
}
|
||||
|
||||
private String generateBasicCss(Map<String, String> variables) {
|
||||
return "/* CSS content */";
|
||||
}
|
||||
|
||||
private String generateBasicJs(Map<String, String> variables) {
|
||||
return "// JavaScript content";
|
||||
}
|
||||
|
||||
private String generateReadmeContent(Map<String, String> variables) {
|
||||
return "# README content";
|
||||
}
|
||||
|
||||
private String generateGitignoreContent(ProjectType projectType) {
|
||||
return "# Gitignore content";
|
||||
}
|
||||
|
||||
/**
|
||||
* 脚手架参数类
|
||||
*/
|
||||
public static class ScaffoldParams {
|
||||
@JsonProperty("project_name")
|
||||
private String projectName;
|
||||
|
||||
@JsonProperty("project_type")
|
||||
private String projectType;
|
||||
|
||||
@JsonProperty("project_path")
|
||||
private String projectPath;
|
||||
|
||||
@JsonProperty("template_variables")
|
||||
private Map<String, Object> templateVariables;
|
||||
|
||||
@JsonProperty("include_git")
|
||||
private Boolean includeGit = true;
|
||||
|
||||
@JsonProperty("include_readme")
|
||||
private Boolean includeReadme = true;
|
||||
|
||||
@JsonProperty("include_gitignore")
|
||||
private Boolean includeGitignore = true;
|
||||
|
||||
// Getters and Setters
|
||||
public String getProjectName() { return projectName; }
|
||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||
|
||||
public String getProjectType() { return projectType; }
|
||||
public void setProjectType(String projectType) { this.projectType = projectType; }
|
||||
|
||||
public String getProjectPath() { return projectPath; }
|
||||
public void setProjectPath(String projectPath) { this.projectPath = projectPath; }
|
||||
|
||||
public Map<String, Object> getTemplateVariables() { return templateVariables; }
|
||||
public void setTemplateVariables(Map<String, Object> templateVariables) { this.templateVariables = templateVariables; }
|
||||
|
||||
public Boolean getIncludeGit() { return includeGit; }
|
||||
public void setIncludeGit(Boolean includeGit) { this.includeGit = includeGit; }
|
||||
|
||||
public Boolean getIncludeReadme() { return includeReadme; }
|
||||
public void setIncludeReadme(Boolean includeReadme) { this.includeReadme = includeReadme; }
|
||||
|
||||
public Boolean getIncludeGitignore() { return includeGitignore; }
|
||||
public void setIncludeGitignore(Boolean includeGitignore) { this.includeGitignore = includeGitignore; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 脚手架结果类
|
||||
*/
|
||||
private static class ScaffoldResult {
|
||||
private java.util.List<String> createdItems = new java.util.ArrayList<>();
|
||||
private String summary;
|
||||
private String details;
|
||||
|
||||
public void addCreatedItem(String item) {
|
||||
createdItems.add(item);
|
||||
}
|
||||
|
||||
public void generateSummary(Path projectPath, ProjectType projectType) {
|
||||
this.summary = String.format("Created %s project '%s' with %d files/directories",
|
||||
projectType.getDisplayName(),
|
||||
projectPath.getFileName(),
|
||||
createdItems.size());
|
||||
|
||||
StringBuilder detailsBuilder = new StringBuilder();
|
||||
detailsBuilder.append("Created project structure:\n");
|
||||
for (String item : createdItems) {
|
||||
detailsBuilder.append("✓ ").append(item).append("\n");
|
||||
}
|
||||
this.details = detailsBuilder.toString();
|
||||
}
|
||||
|
||||
public String getSummary() { return summary; }
|
||||
public String getDetails() { return details; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.schema.JsonSchema;
|
||||
import com.example.demo.service.ToolExecutionLogger;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 文件读取工具
|
||||
* 支持读取文本文件,可以分页读取大文件
|
||||
*/
|
||||
@Component
|
||||
public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
|
||||
|
||||
private final String rootDirectory;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
@Autowired
|
||||
private ToolExecutionLogger executionLogger;
|
||||
|
||||
public ReadFileTool(AppProperties appProperties) {
|
||||
super(
|
||||
"read_file",
|
||||
"ReadFile",
|
||||
"Reads and returns the content of a specified file from the local filesystem. " +
|
||||
"Handles text files and supports pagination for large files. " +
|
||||
"Always use absolute paths within the workspace directory.",
|
||||
createSchema()
|
||||
);
|
||||
this.appProperties = appProperties;
|
||||
this.rootDirectory = appProperties.getWorkspace().getRootDirectory();
|
||||
}
|
||||
|
||||
private static String getWorkspaceBasePath() {
|
||||
return Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
||||
}
|
||||
|
||||
private static String getPathExample(String subPath) {
|
||||
return "Example: \"" + Paths.get(getWorkspaceBasePath(), subPath).toString() + "\"";
|
||||
}
|
||||
|
||||
private static JsonSchema createSchema() {
|
||||
return JsonSchema.object()
|
||||
.addProperty("absolute_path", JsonSchema.string(
|
||||
"MUST be an absolute path to the file to read. Path must be within the workspace directory (" +
|
||||
getWorkspaceBasePath() + "). " +
|
||||
getPathExample("project/src/main.java") + ". " +
|
||||
"Relative paths are NOT allowed."
|
||||
))
|
||||
.addProperty("offset", JsonSchema.integer(
|
||||
"Optional: For text files, the 0-based line number to start reading from. " +
|
||||
"Requires 'limit' to be set. Use for paginating through large files."
|
||||
).minimum(0))
|
||||
.addProperty("limit", JsonSchema.integer(
|
||||
"Optional: For text files, the number of lines to read from the offset. " +
|
||||
"Use for paginating through large files."
|
||||
).minimum(1))
|
||||
.required("absolute_path");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateToolParams(ReadFileParams params) {
|
||||
String baseValidation = super.validateToolParams(params);
|
||||
if (baseValidation != null) {
|
||||
return baseValidation;
|
||||
}
|
||||
|
||||
// 验证路径
|
||||
if (params.absolutePath == null || params.absolutePath.trim().isEmpty()) {
|
||||
return "File path cannot be empty";
|
||||
}
|
||||
|
||||
Path filePath = Paths.get(params.absolutePath);
|
||||
|
||||
// 验证是否为绝对路径
|
||||
if (!filePath.isAbsolute()) {
|
||||
return "File path must be absolute: " + params.absolutePath;
|
||||
}
|
||||
|
||||
// 验证是否在工作目录内
|
||||
if (!isWithinWorkspace(filePath)) {
|
||||
return "File path must be within the workspace directory (" + rootDirectory + "): " + params.absolutePath;
|
||||
}
|
||||
|
||||
// 验证分页参数
|
||||
if (params.offset != null && params.limit == null) {
|
||||
return "When 'offset' is specified, 'limit' must also be specified";
|
||||
}
|
||||
|
||||
if (params.offset != null && params.offset < 0) {
|
||||
return "Offset must be non-negative";
|
||||
}
|
||||
|
||||
if (params.limit != null && params.limit <= 0) {
|
||||
return "Limit must be positive";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file tool method for Spring AI integration
|
||||
*/
|
||||
@Tool(name = "read_file", description = "Reads and returns the content of a specified file from the local filesystem")
|
||||
public String readFile(String absolutePath, Integer offset, Integer limit) {
|
||||
long callId = executionLogger.logToolStart("read_file", "读取文件内容",
|
||||
String.format("文件路径=%s, 偏移量=%s, 限制行数=%s", absolutePath, offset, limit));
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
ReadFileParams params = new ReadFileParams();
|
||||
params.setAbsolutePath(absolutePath);
|
||||
params.setOffset(offset);
|
||||
params.setLimit(limit);
|
||||
|
||||
executionLogger.logToolStep(callId, "read_file", "参数验证", "验证文件路径和分页参数");
|
||||
|
||||
// Validate parameters
|
||||
String validation = validateToolParams(params);
|
||||
if (validation != null) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
executionLogger.logToolError(callId, "read_file", "参数验证失败: " + validation, executionTime);
|
||||
return "Error: " + validation;
|
||||
}
|
||||
|
||||
executionLogger.logFileOperation(callId, "读取文件", absolutePath,
|
||||
offset != null ? String.format("分页读取: 偏移=%d, 限制=%d", offset, limit) : "完整读取");
|
||||
|
||||
// Execute the tool
|
||||
ToolResult result = execute(params).join();
|
||||
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
if (result.isSuccess()) {
|
||||
executionLogger.logToolSuccess(callId, "read_file", "文件读取成功", executionTime);
|
||||
return result.getLlmContent();
|
||||
} else {
|
||||
executionLogger.logToolError(callId, "read_file", result.getErrorMessage(), executionTime);
|
||||
return "Error: " + result.getErrorMessage();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
executionLogger.logToolError(callId, "read_file", "工具执行异常: " + e.getMessage(), executionTime);
|
||||
logger.error("Error in read file tool", e);
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolResult> execute(ReadFileParams params) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Path filePath = Paths.get(params.absolutePath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!Files.exists(filePath)) {
|
||||
return ToolResult.error("File not found: " + params.absolutePath);
|
||||
}
|
||||
|
||||
// 检查是否为文件
|
||||
if (!Files.isRegularFile(filePath)) {
|
||||
return ToolResult.error("Path is not a regular file: " + params.absolutePath);
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
long fileSize = Files.size(filePath);
|
||||
if (fileSize > appProperties.getWorkspace().getMaxFileSize()) {
|
||||
return ToolResult.error("File too large: " + fileSize + " bytes. Maximum allowed: " +
|
||||
appProperties.getWorkspace().getMaxFileSize() + " bytes");
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
String fileName = filePath.getFileName().toString();
|
||||
if (!isAllowedFileType(fileName)) {
|
||||
return ToolResult.error("File type not allowed: " + fileName +
|
||||
". Allowed extensions: " + appProperties.getWorkspace().getAllowedExtensions());
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
if (params.offset != null && params.limit != null) {
|
||||
return readFileWithPagination(filePath, params.offset, params.limit);
|
||||
} else {
|
||||
return readFullFile(filePath);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Error reading file: " + params.absolutePath, e);
|
||||
return ToolResult.error("Error reading file: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error reading file: " + params.absolutePath, e);
|
||||
return ToolResult.error("Unexpected error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ToolResult readFullFile(Path filePath) throws IOException {
|
||||
String content = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||
String relativePath = getRelativePath(filePath);
|
||||
|
||||
long lineCount = content.lines().count();
|
||||
String displayMessage = String.format("Read file: %s (%d lines, %d bytes)",
|
||||
relativePath, lineCount, content.getBytes(StandardCharsets.UTF_8).length);
|
||||
|
||||
return ToolResult.success(content, displayMessage);
|
||||
}
|
||||
|
||||
private ToolResult readFileWithPagination(Path filePath, int offset, int limit) throws IOException {
|
||||
List<String> allLines = Files.readAllLines(filePath, StandardCharsets.UTF_8);
|
||||
|
||||
if (offset >= allLines.size()) {
|
||||
return ToolResult.error("Offset " + offset + " is beyond file length (" + allLines.size() + " lines)");
|
||||
}
|
||||
|
||||
int endIndex = Math.min(offset + limit, allLines.size());
|
||||
List<String> selectedLines = allLines.subList(offset, endIndex);
|
||||
String content = String.join("\n", selectedLines);
|
||||
|
||||
String relativePath = getRelativePath(filePath);
|
||||
String displayMessage = String.format("Read file: %s (lines %d-%d of %d total)",
|
||||
relativePath, offset + 1, endIndex, allLines.size());
|
||||
|
||||
return ToolResult.success(content, displayMessage);
|
||||
}
|
||||
|
||||
private boolean isWithinWorkspace(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
Path resolvedPath = filePath.toRealPath();
|
||||
return resolvedPath.startsWith(workspaceRoot);
|
||||
} catch (IOException e) {
|
||||
// 如果路径不存在,检查其父目录
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
Path normalizedPath = filePath.normalize();
|
||||
return normalizedPath.startsWith(workspaceRoot.normalize());
|
||||
} catch (IOException ex) {
|
||||
logger.warn("Could not resolve workspace path", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAllowedFileType(String fileName) {
|
||||
List<String> allowedExtensions = appProperties.getWorkspace().getAllowedExtensions();
|
||||
return allowedExtensions.stream()
|
||||
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
|
||||
}
|
||||
|
||||
private String getRelativePath(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory);
|
||||
return workspaceRoot.relativize(filePath).toString();
|
||||
} catch (Exception e) {
|
||||
return filePath.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件参数
|
||||
*/
|
||||
public static class ReadFileParams {
|
||||
@JsonProperty("absolute_path")
|
||||
private String absolutePath;
|
||||
|
||||
private Integer offset;
|
||||
private Integer limit;
|
||||
|
||||
// 构造器
|
||||
public ReadFileParams() {}
|
||||
|
||||
public ReadFileParams(String absolutePath) {
|
||||
this.absolutePath = absolutePath;
|
||||
}
|
||||
|
||||
public ReadFileParams(String absolutePath, Integer offset, Integer limit) {
|
||||
this.absolutePath = absolutePath;
|
||||
this.offset = offset;
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getAbsolutePath() { return absolutePath; }
|
||||
public void setAbsolutePath(String absolutePath) { this.absolutePath = absolutePath; }
|
||||
|
||||
public Integer getOffset() { return offset; }
|
||||
public void setOffset(Integer offset) { this.offset = offset; }
|
||||
|
||||
public Integer getLimit() { return limit; }
|
||||
public void setLimit(Integer limit) { this.limit = limit; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ReadFileParams{path='%s', offset=%d, limit=%d}",
|
||||
absolutePath, offset, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.model.ProjectContext;
|
||||
import com.example.demo.schema.JsonSchema;
|
||||
import com.example.demo.service.ProjectContextAnalyzer;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Smart editing tool
|
||||
* Provides intelligent multi-file editing capabilities based on project context understanding
|
||||
*/
|
||||
@Component
|
||||
public class SmartEditTool extends BaseTool<SmartEditTool.SmartEditParams> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SmartEditTool.class);
|
||||
|
||||
@Autowired
|
||||
private ProjectContextAnalyzer projectContextAnalyzer;
|
||||
|
||||
@Autowired
|
||||
private EditFileTool editFileTool;
|
||||
|
||||
@Autowired
|
||||
private ReadFileTool readFileTool;
|
||||
|
||||
@Autowired
|
||||
private WriteFileTool writeFileTool;
|
||||
|
||||
@Autowired
|
||||
private ChatModel chatModel;
|
||||
|
||||
private final String rootDirectory;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
public SmartEditTool(AppProperties appProperties) {
|
||||
super(
|
||||
"smart_edit",
|
||||
"SmartEdit",
|
||||
"Intelligently edit projects based on natural language descriptions. " +
|
||||
"Analyzes project context and performs multi-file edits when necessary. " +
|
||||
"Can handle complex refactoring, feature additions, and project-wide changes.",
|
||||
createSchema()
|
||||
);
|
||||
this.appProperties = appProperties;
|
||||
this.rootDirectory = appProperties.getWorkspace().getRootDirectory();
|
||||
}
|
||||
|
||||
private static JsonSchema createSchema() {
|
||||
return JsonSchema.object()
|
||||
.addProperty("project_path", JsonSchema.string(
|
||||
"Absolute path to the project root directory to analyze and edit"
|
||||
))
|
||||
.addProperty("edit_description", JsonSchema.string(
|
||||
"Natural language description of the desired changes. " +
|
||||
"Examples: 'Add a new REST endpoint for user management', " +
|
||||
"'Refactor the authentication logic', 'Update dependencies to latest versions'"
|
||||
))
|
||||
.addProperty("target_files", JsonSchema.array(JsonSchema.string(
|
||||
"Optional: Specific files to focus on. If not provided, the tool will determine which files to edit based on the description"
|
||||
)))
|
||||
.addProperty("scope", JsonSchema.string(
|
||||
"Edit scope: 'single_file', 'related_files', or 'project_wide'. Default: 'related_files'"
|
||||
))
|
||||
.addProperty("dry_run", JsonSchema.bool(
|
||||
"If true, only analyze and show what would be changed without making actual changes. Default: false"
|
||||
))
|
||||
.required("project_path", "edit_description");
|
||||
}
|
||||
|
||||
public enum EditScope {
|
||||
SINGLE_FILE("single_file", "Edit only one file"),
|
||||
RELATED_FILES("related_files", "Edit related files that are affected by the change"),
|
||||
PROJECT_WIDE("project_wide", "Make project-wide changes including configuration files");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
|
||||
EditScope(String value, String description) {
|
||||
this.value = value;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public static EditScope fromString(String value) {
|
||||
for (EditScope scope : values()) {
|
||||
if (scope.value.equals(value)) {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
return RELATED_FILES; // default
|
||||
}
|
||||
|
||||
public String getValue() { return value; }
|
||||
public String getDescription() { return description; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart edit tool method for Spring AI integration
|
||||
*/
|
||||
@Tool(name = "smart_edit", description = "Intelligently edit projects based on natural language descriptions")
|
||||
public String smartEdit(
|
||||
String projectPath,
|
||||
String editDescription,
|
||||
String scope,
|
||||
Boolean dryRun) {
|
||||
|
||||
try {
|
||||
SmartEditParams params = new SmartEditParams();
|
||||
params.setProjectPath(projectPath);
|
||||
params.setEditDescription(editDescription);
|
||||
params.setScope(scope != null ? scope : "related_files");
|
||||
params.setDryRun(dryRun != null ? dryRun : false);
|
||||
|
||||
// Validate parameters
|
||||
String validation = validateToolParams(params);
|
||||
if (validation != null) {
|
||||
return "Error: " + validation;
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
ToolResult result = execute(params).join();
|
||||
|
||||
if (result.isSuccess()) {
|
||||
return result.getLlmContent();
|
||||
} else {
|
||||
return "Error: " + result.getErrorMessage();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error in smart edit tool", e);
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateToolParams(SmartEditParams params) {
|
||||
String baseValidation = super.validateToolParams(params);
|
||||
if (baseValidation != null) {
|
||||
return baseValidation;
|
||||
}
|
||||
|
||||
if (params.projectPath == null || params.projectPath.trim().isEmpty()) {
|
||||
return "Project path cannot be empty";
|
||||
}
|
||||
|
||||
if (params.editDescription == null || params.editDescription.trim().isEmpty()) {
|
||||
return "Edit description cannot be empty";
|
||||
}
|
||||
|
||||
Path projectPath = Paths.get(params.projectPath);
|
||||
if (!projectPath.isAbsolute()) {
|
||||
return "Project path must be absolute: " + params.projectPath;
|
||||
}
|
||||
|
||||
if (!Files.exists(projectPath)) {
|
||||
return "Project path does not exist: " + params.projectPath;
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(projectPath)) {
|
||||
return "Project path must be a directory: " + params.projectPath;
|
||||
}
|
||||
|
||||
if (!isWithinWorkspace(projectPath)) {
|
||||
return "Project path must be within the workspace directory: " + params.projectPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolConfirmationDetails> shouldConfirmExecute(SmartEditParams params) {
|
||||
if (params.dryRun != null && params.dryRun) {
|
||||
return CompletableFuture.completedFuture(null); // No confirmation needed for dry run
|
||||
}
|
||||
|
||||
if (appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.AUTO_EDIT ||
|
||||
appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.YOLO) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
EditPlan plan = analyzeAndPlanEdit(params);
|
||||
String confirmationMessage = buildConfirmationMessage(plan);
|
||||
|
||||
return new ToolConfirmationDetails(
|
||||
"smart_edit",
|
||||
"Confirm Smart Edit: " + params.editDescription,
|
||||
"Smart edit operation confirmation",
|
||||
confirmationMessage
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.warn("Could not generate edit plan for confirmation", e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolResult> execute(SmartEditParams params) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
logger.info("Starting smart edit for project: {}", params.projectPath);
|
||||
logger.info("Edit description: {}", params.editDescription);
|
||||
|
||||
// 1. Analyze project context
|
||||
Path projectPath = Paths.get(params.projectPath);
|
||||
ProjectContext context = projectContextAnalyzer.analyzeProject(projectPath);
|
||||
|
||||
// 2. Generate edit plan
|
||||
EditPlan plan = generateEditPlan(params, context);
|
||||
|
||||
if (params.dryRun != null && params.dryRun) {
|
||||
return ToolResult.success(
|
||||
"Dry run completed. Edit plan generated successfully.",
|
||||
plan.toString()
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Execute edit plan
|
||||
EditResult result = executeEditPlan(plan);
|
||||
|
||||
logger.info("Smart edit completed for project: {}", params.projectPath);
|
||||
return ToolResult.success(
|
||||
result.getSummary(),
|
||||
result.getDetails()
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error during smart edit execution", e);
|
||||
return ToolResult.error("Smart edit failed: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze and generate edit plan
|
||||
*/
|
||||
private EditPlan analyzeAndPlanEdit(SmartEditParams params) {
|
||||
Path projectPath = Paths.get(params.projectPath);
|
||||
ProjectContext context = projectContextAnalyzer.analyzeProject(projectPath);
|
||||
return generateEditPlan(params, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate edit plan
|
||||
*/
|
||||
private EditPlan generateEditPlan(SmartEditParams params, ProjectContext context) {
|
||||
logger.debug("Generating edit plan for: {}", params.editDescription);
|
||||
|
||||
EditPlan plan = new EditPlan();
|
||||
plan.setDescription(params.editDescription);
|
||||
plan.setScope(EditScope.fromString(params.scope));
|
||||
plan.setProjectContext(context);
|
||||
|
||||
// Use AI to analyze edit intent and generate specific edit steps
|
||||
String editContext = buildEditContext(context, params.editDescription);
|
||||
|
||||
List<EditStep> steps = generateEditSteps(editContext, params);
|
||||
plan.setSteps(steps);
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build edit context from project context and description
|
||||
*/
|
||||
private String buildEditContext(ProjectContext context, String editDescription) {
|
||||
StringBuilder contextBuilder = new StringBuilder();
|
||||
|
||||
contextBuilder.append("PROJECT CONTEXT:\n");
|
||||
contextBuilder.append("Type: ").append(context.getProjectType().getDisplayName()).append("\n");
|
||||
contextBuilder.append("Language: ").append(context.getProjectType().getPrimaryLanguage()).append("\n");
|
||||
|
||||
if (context.getProjectStructure() != null) {
|
||||
contextBuilder.append("Structure: ").append(context.getProjectStructure().getStructureSummary()).append("\n");
|
||||
}
|
||||
|
||||
if (context.getDependencies() != null && !context.getDependencies().isEmpty()) {
|
||||
contextBuilder.append("Dependencies: ").append(context.getDependencySummary()).append("\n");
|
||||
}
|
||||
|
||||
contextBuilder.append("\nEDIT REQUEST: ").append(editDescription);
|
||||
|
||||
return contextBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use AI to generate edit steps
|
||||
*/
|
||||
private List<EditStep> generateEditSteps(String editContext, SmartEditParams params) {
|
||||
List<EditStep> steps = new ArrayList<>();
|
||||
|
||||
try {
|
||||
String prompt = buildEditPlanPrompt(editContext, params);
|
||||
|
||||
List<Message> messages = List.of(new UserMessage(prompt));
|
||||
|
||||
ChatResponse response = ChatClient.create(chatModel)
|
||||
.prompt()
|
||||
.messages(messages)
|
||||
.call()
|
||||
.chatResponse();
|
||||
|
||||
String aiResponse = response.getResult().getOutput().getText();
|
||||
steps = parseEditStepsFromAI(aiResponse, params);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to generate AI-based edit steps, using fallback", e);
|
||||
steps = generateFallbackEditSteps(params);
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build edit plan prompt
|
||||
*/
|
||||
private String buildEditPlanPrompt(String editContext, SmartEditParams params) {
|
||||
return String.format("""
|
||||
You are an expert software developer. Based on the project context below,
|
||||
create a detailed plan to implement the requested changes.
|
||||
|
||||
%s
|
||||
|
||||
TASK: %s
|
||||
|
||||
Please provide a step-by-step plan in the following format:
|
||||
STEP 1: [Action] - [File] - [Description]
|
||||
STEP 2: [Action] - [File] - [Description]
|
||||
...
|
||||
|
||||
Actions can be: CREATE, EDIT, DELETE, RENAME
|
||||
Be specific about which files need to be modified and what changes are needed.
|
||||
Consider dependencies between files and the overall project structure.
|
||||
""", editContext, params.editDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse edit steps from AI response
|
||||
*/
|
||||
private List<EditStep> parseEditStepsFromAI(String aiResponse, SmartEditParams params) {
|
||||
List<EditStep> steps = new ArrayList<>();
|
||||
|
||||
String[] lines = aiResponse.split("\n");
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (line.startsWith("STEP") && line.contains(":")) {
|
||||
try {
|
||||
EditStep step = parseEditStepLine(line, params);
|
||||
if (step != null) {
|
||||
steps.add(step);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to parse edit step: {}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse single edit step line
|
||||
*/
|
||||
private EditStep parseEditStepLine(String line, SmartEditParams params) {
|
||||
// Simple parsing implementation
|
||||
// In actual projects, more complex parsing logic should be used
|
||||
String[] parts = line.split(" - ");
|
||||
if (parts.length >= 3) {
|
||||
String actionPart = parts[0].substring(parts[0].indexOf(":") + 1).trim();
|
||||
String filePart = parts[1].trim();
|
||||
String descriptionPart = parts[2].trim();
|
||||
|
||||
EditStep step = new EditStep();
|
||||
step.setAction(actionPart);
|
||||
step.setTargetFile(filePart);
|
||||
step.setDescription(descriptionPart);
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate fallback edit steps
|
||||
*/
|
||||
private List<EditStep> generateFallbackEditSteps(SmartEditParams params) {
|
||||
List<EditStep> steps = new ArrayList<>();
|
||||
|
||||
// Simple fallback logic
|
||||
if (params.targetFiles != null && !params.targetFiles.isEmpty()) {
|
||||
for (String file : params.targetFiles) {
|
||||
EditStep step = new EditStep();
|
||||
step.setAction("EDIT");
|
||||
step.setTargetFile(file);
|
||||
step.setDescription("Edit " + file + " according to: " + params.editDescription);
|
||||
steps.add(step);
|
||||
}
|
||||
} else {
|
||||
// Default edit step
|
||||
EditStep step = new EditStep();
|
||||
step.setAction("ANALYZE");
|
||||
step.setTargetFile("*");
|
||||
step.setDescription("Analyze project and apply changes: " + params.editDescription);
|
||||
steps.add(step);
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute edit plan
|
||||
*/
|
||||
private EditResult executeEditPlan(EditPlan plan) {
|
||||
EditResult result = new EditResult();
|
||||
List<String> executedSteps = new ArrayList<>();
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
for (EditStep step : plan.getSteps()) {
|
||||
try {
|
||||
String stepResult = executeEditStep(step, plan.getProjectContext());
|
||||
executedSteps.add(stepResult);
|
||||
logger.debug("Executed step: {}", step.getDescription());
|
||||
} catch (Exception e) {
|
||||
String error = "Failed to execute step: " + step.getDescription() + " - " + e.getMessage();
|
||||
errors.add(error);
|
||||
logger.warn(error, e);
|
||||
}
|
||||
}
|
||||
|
||||
result.setExecutedSteps(executedSteps);
|
||||
result.setErrors(errors);
|
||||
result.generateSummary();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute single edit step
|
||||
*/
|
||||
private String executeEditStep(EditStep step, ProjectContext context) throws Exception {
|
||||
switch (step.getAction().toUpperCase()) {
|
||||
case "CREATE":
|
||||
return executeCreateStep(step, context);
|
||||
case "EDIT":
|
||||
return executeFileEditStep(step, context);
|
||||
case "DELETE":
|
||||
return executeDeleteStep(step, context);
|
||||
default:
|
||||
return "Skipped unsupported action: " + step.getAction();
|
||||
}
|
||||
}
|
||||
|
||||
private String executeCreateStep(EditStep step, ProjectContext context) throws Exception {
|
||||
// Implement file creation logic
|
||||
return "Created file: " + step.getTargetFile();
|
||||
}
|
||||
|
||||
private String executeFileEditStep(EditStep step, ProjectContext context) throws Exception {
|
||||
// Implement file editing logic
|
||||
return "Edited file: " + step.getTargetFile();
|
||||
}
|
||||
|
||||
private String executeDeleteStep(EditStep step, ProjectContext context) throws Exception {
|
||||
// Implement file deletion logic
|
||||
return "Deleted file: " + step.getTargetFile();
|
||||
}
|
||||
|
||||
private String buildConfirmationMessage(EditPlan plan) {
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.append("Smart Edit Plan:\n");
|
||||
message.append("Description: ").append(plan.getDescription()).append("\n");
|
||||
message.append("Scope: ").append(plan.getScope().getDescription()).append("\n");
|
||||
message.append("Steps to execute:\n");
|
||||
|
||||
for (int i = 0; i < plan.getSteps().size(); i++) {
|
||||
EditStep step = plan.getSteps().get(i);
|
||||
message.append(String.format("%d. %s - %s\n",
|
||||
i + 1, step.getAction(), step.getDescription()));
|
||||
}
|
||||
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
private boolean isWithinWorkspace(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
Path normalizedPath = filePath.toRealPath();
|
||||
return normalizedPath.startsWith(workspaceRoot);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not resolve workspace path", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Inner class definitions
|
||||
public static class SmartEditParams {
|
||||
@JsonProperty("project_path")
|
||||
private String projectPath;
|
||||
|
||||
@JsonProperty("edit_description")
|
||||
private String editDescription;
|
||||
|
||||
@JsonProperty("target_files")
|
||||
private List<String> targetFiles;
|
||||
|
||||
@JsonProperty("scope")
|
||||
private String scope = "related_files";
|
||||
|
||||
@JsonProperty("dry_run")
|
||||
private Boolean dryRun = false;
|
||||
|
||||
// Getters and Setters
|
||||
public String getProjectPath() { return projectPath; }
|
||||
public void setProjectPath(String projectPath) { this.projectPath = projectPath; }
|
||||
|
||||
public String getEditDescription() { return editDescription; }
|
||||
public void setEditDescription(String editDescription) { this.editDescription = editDescription; }
|
||||
|
||||
public List<String> getTargetFiles() { return targetFiles; }
|
||||
public void setTargetFiles(List<String> targetFiles) { this.targetFiles = targetFiles; }
|
||||
|
||||
public String getScope() { return scope; }
|
||||
public void setScope(String scope) { this.scope = scope; }
|
||||
|
||||
public Boolean getDryRun() { return dryRun; }
|
||||
public void setDryRun(Boolean dryRun) { this.dryRun = dryRun; }
|
||||
}
|
||||
|
||||
private static class EditPlan {
|
||||
private String description;
|
||||
private EditScope scope;
|
||||
private ProjectContext projectContext;
|
||||
private List<EditStep> steps;
|
||||
|
||||
// Getters and Setters
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public EditScope getScope() { return scope; }
|
||||
public void setScope(EditScope scope) { this.scope = scope; }
|
||||
|
||||
public ProjectContext getProjectContext() { return projectContext; }
|
||||
public void setProjectContext(ProjectContext projectContext) { this.projectContext = projectContext; }
|
||||
|
||||
public List<EditStep> getSteps() { return steps; }
|
||||
public void setSteps(List<EditStep> steps) { this.steps = steps; }
|
||||
}
|
||||
|
||||
private static class EditStep {
|
||||
private String action;
|
||||
private String targetFile;
|
||||
private String description;
|
||||
|
||||
// Getters and Setters
|
||||
public String getAction() { return action; }
|
||||
public void setAction(String action) { this.action = action; }
|
||||
|
||||
public String getTargetFile() { return targetFile; }
|
||||
public void setTargetFile(String targetFile) { this.targetFile = targetFile; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
}
|
||||
|
||||
private static class EditResult {
|
||||
private List<String> executedSteps;
|
||||
private List<String> errors;
|
||||
private String summary;
|
||||
private String details;
|
||||
|
||||
public void generateSummary() {
|
||||
int successCount = executedSteps.size();
|
||||
int errorCount = errors.size();
|
||||
|
||||
this.summary = String.format("Smart edit completed: %d steps executed, %d errors",
|
||||
successCount, errorCount);
|
||||
|
||||
StringBuilder detailsBuilder = new StringBuilder();
|
||||
detailsBuilder.append("Executed Steps:\n");
|
||||
for (String step : executedSteps) {
|
||||
detailsBuilder.append("✓ ").append(step).append("\n");
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
detailsBuilder.append("\nErrors:\n");
|
||||
for (String error : errors) {
|
||||
detailsBuilder.append("✗ ").append(error).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
this.details = detailsBuilder.toString();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public List<String> getExecutedSteps() { return executedSteps; }
|
||||
public void setExecutedSteps(List<String> executedSteps) { this.executedSteps = executedSteps; }
|
||||
|
||||
public List<String> getErrors() { return errors; }
|
||||
public void setErrors(List<String> errors) { this.errors = errors; }
|
||||
|
||||
public String getSummary() { return summary; }
|
||||
public String getDetails() { return details; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
/**
|
||||
* 工具执行结果
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ToolResult {
|
||||
|
||||
private final boolean success;
|
||||
private final String llmContent;
|
||||
private final Object returnDisplay;
|
||||
private final String errorMessage;
|
||||
|
||||
private ToolResult(boolean success, String llmContent, Object returnDisplay, String errorMessage) {
|
||||
this.success = success;
|
||||
this.llmContent = llmContent;
|
||||
this.returnDisplay = returnDisplay;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
// 静态工厂方法
|
||||
public static ToolResult success(String llmContent) {
|
||||
return new ToolResult(true, llmContent, llmContent, null);
|
||||
}
|
||||
|
||||
public static ToolResult success(String llmContent, Object returnDisplay) {
|
||||
return new ToolResult(true, llmContent, returnDisplay, null);
|
||||
}
|
||||
|
||||
public static ToolResult error(String errorMessage) {
|
||||
return new ToolResult(false, "Error: " + errorMessage, "Error: " + errorMessage, errorMessage);
|
||||
}
|
||||
|
||||
// Getters
|
||||
public boolean isSuccess() { return success; }
|
||||
public String getLlmContent() { return llmContent; }
|
||||
public Object getReturnDisplay() { return returnDisplay; }
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (success) {
|
||||
return "ToolResult{success=true, content='" + llmContent + "'}";
|
||||
} else {
|
||||
return "ToolResult{success=false, error='" + errorMessage + "'}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件差异结果
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class FileDiff {
|
||||
private final String fileDiff;
|
||||
private final String fileName;
|
||||
|
||||
public FileDiff(String fileDiff, String fileName) {
|
||||
this.fileDiff = fileDiff;
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getFileDiff() { return fileDiff; }
|
||||
public String getFileName() { return fileName; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FileDiff{fileName='" + fileName + "'}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具确认详情
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class ToolConfirmationDetails {
|
||||
private final String type;
|
||||
private final String title;
|
||||
private final String description;
|
||||
private final Object details;
|
||||
|
||||
public ToolConfirmationDetails(String type, String title, String description, Object details) {
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public static ToolConfirmationDetails edit(String title, String fileName, String fileDiff) {
|
||||
return new ToolConfirmationDetails("edit", title, "File edit confirmation",
|
||||
new FileDiff(fileDiff, fileName));
|
||||
}
|
||||
|
||||
public static ToolConfirmationDetails exec(String title, String command) {
|
||||
return new ToolConfirmationDetails("exec", title, "Command execution confirmation", command);
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getType() { return type; }
|
||||
public String getTitle() { return title; }
|
||||
public String getDescription() { return description; }
|
||||
public Object getDetails() { return details; }
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package com.example.demo.tools;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.schema.JsonSchema;
|
||||
import com.example.demo.service.ToolExecutionLogger;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.github.difflib.DiffUtils;
|
||||
import com.github.difflib.UnifiedDiffUtils;
|
||||
import com.github.difflib.patch.Patch;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 文件写入工具
|
||||
* 支持创建新文件或覆盖现有文件,自动显示差异
|
||||
*/
|
||||
@Component
|
||||
public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
|
||||
|
||||
private final String rootDirectory;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
@Autowired
|
||||
private ToolExecutionLogger executionLogger;
|
||||
|
||||
public WriteFileTool(AppProperties appProperties) {
|
||||
super(
|
||||
"write_file",
|
||||
"WriteFile",
|
||||
"Writes content to a file. Creates new files or overwrites existing ones. " +
|
||||
"Always shows a diff before writing. Automatically creates parent directories if needed. " +
|
||||
"Use absolute paths within the workspace directory.",
|
||||
createSchema()
|
||||
);
|
||||
this.appProperties = appProperties;
|
||||
this.rootDirectory = appProperties.getWorkspace().getRootDirectory();
|
||||
}
|
||||
|
||||
private static String getWorkspaceBasePath() {
|
||||
return Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
||||
}
|
||||
|
||||
private static String getPathExample(String subPath) {
|
||||
return "Example: \"" + Paths.get(getWorkspaceBasePath(), subPath).toString() + "\"";
|
||||
}
|
||||
|
||||
private static JsonSchema createSchema() {
|
||||
return JsonSchema.object()
|
||||
.addProperty("file_path", JsonSchema.string(
|
||||
"MUST be an absolute path to the file to write to. Path must be within the workspace directory (" +
|
||||
getWorkspaceBasePath() + "). " +
|
||||
getPathExample("project/src/main.java") + ". " +
|
||||
"Relative paths are NOT allowed."
|
||||
))
|
||||
.addProperty("content", JsonSchema.string(
|
||||
"The content to write to the file. Will completely replace existing content if file exists."
|
||||
))
|
||||
.required("file_path", "content");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validateToolParams(WriteFileParams params) {
|
||||
String baseValidation = super.validateToolParams(params);
|
||||
if (baseValidation != null) {
|
||||
return baseValidation;
|
||||
}
|
||||
|
||||
// 验证路径
|
||||
if (params.filePath == null || params.filePath.trim().isEmpty()) {
|
||||
return "File path cannot be empty";
|
||||
}
|
||||
|
||||
if (params.content == null) {
|
||||
return "Content cannot be null";
|
||||
}
|
||||
|
||||
Path filePath = Paths.get(params.filePath);
|
||||
|
||||
// 验证是否为绝对路径
|
||||
if (!filePath.isAbsolute()) {
|
||||
return "File path must be absolute: " + params.filePath;
|
||||
}
|
||||
|
||||
// 验证是否在工作目录内
|
||||
if (!isWithinWorkspace(filePath)) {
|
||||
return "File path must be within the workspace directory (" + rootDirectory + "): " + params.filePath;
|
||||
}
|
||||
|
||||
// 验证文件扩展名
|
||||
String fileName = filePath.getFileName().toString();
|
||||
if (!isAllowedFileType(fileName)) {
|
||||
return "File type not allowed: " + fileName +
|
||||
". Allowed extensions: " + appProperties.getWorkspace().getAllowedExtensions();
|
||||
}
|
||||
|
||||
// 验证内容大小
|
||||
byte[] contentBytes = params.content.getBytes(StandardCharsets.UTF_8);
|
||||
if (contentBytes.length > appProperties.getWorkspace().getMaxFileSize()) {
|
||||
return "Content too large: " + contentBytes.length + " bytes. Maximum allowed: " +
|
||||
appProperties.getWorkspace().getMaxFileSize() + " bytes";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolConfirmationDetails> shouldConfirmExecute(WriteFileParams params) {
|
||||
// 根据配置决定是否需要确认
|
||||
if (appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.AUTO_EDIT ||
|
||||
appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.YOLO) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Path filePath = Paths.get(params.filePath);
|
||||
String currentContent = "";
|
||||
boolean isNewFile = !Files.exists(filePath);
|
||||
|
||||
if (!isNewFile) {
|
||||
currentContent = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
// 生成差异显示
|
||||
String diff = generateDiff(
|
||||
filePath.getFileName().toString(),
|
||||
currentContent,
|
||||
params.content
|
||||
);
|
||||
|
||||
String title = isNewFile ?
|
||||
"Confirm Create: " + getRelativePath(filePath) :
|
||||
"Confirm Write: " + getRelativePath(filePath);
|
||||
|
||||
return ToolConfirmationDetails.edit(title, filePath.getFileName().toString(), diff);
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not read existing file for diff: " + params.filePath, e);
|
||||
return null; // 如果无法读取文件,直接执行
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file tool method for Spring AI integration
|
||||
*/
|
||||
@Tool(name = "write_file", description = "Creates a new file or overwrites an existing file with the specified content")
|
||||
public String writeFile(String filePath, String content) {
|
||||
long callId = executionLogger.logToolStart("write_file", "写入文件内容",
|
||||
String.format("文件路径=%s, 内容长度=%d字符", filePath, content != null ? content.length() : 0));
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
WriteFileParams params = new WriteFileParams();
|
||||
params.setFilePath(filePath);
|
||||
params.setContent(content);
|
||||
|
||||
executionLogger.logToolStep(callId, "write_file", "参数验证", "验证文件路径和内容");
|
||||
|
||||
// Validate parameters
|
||||
String validation = validateToolParams(params);
|
||||
if (validation != null) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
executionLogger.logToolError(callId, "write_file", "参数验证失败: " + validation, executionTime);
|
||||
return "Error: " + validation;
|
||||
}
|
||||
|
||||
executionLogger.logFileOperation(callId, "写入文件", filePath,
|
||||
String.format("内容长度: %d字符", content != null ? content.length() : 0));
|
||||
|
||||
// Execute the tool
|
||||
ToolResult result = execute(params).join();
|
||||
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
if (result.isSuccess()) {
|
||||
executionLogger.logToolSuccess(callId, "write_file", "文件写入成功", executionTime);
|
||||
return result.getLlmContent();
|
||||
} else {
|
||||
executionLogger.logToolError(callId, "write_file", result.getErrorMessage(), executionTime);
|
||||
return "Error: " + result.getErrorMessage();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
executionLogger.logToolError(callId, "write_file", "工具执行异常: " + e.getMessage(), executionTime);
|
||||
logger.error("Error in write file tool", e);
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ToolResult> execute(WriteFileParams params) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Path filePath = Paths.get(params.filePath);
|
||||
boolean isNewFile = !Files.exists(filePath);
|
||||
String originalContent = "";
|
||||
|
||||
// 读取原始内容(用于备份和差异显示)
|
||||
if (!isNewFile) {
|
||||
originalContent = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
// 创建备份(如果启用)
|
||||
if (!isNewFile && shouldCreateBackup()) {
|
||||
createBackup(filePath, originalContent);
|
||||
}
|
||||
|
||||
// 确保父目录存在
|
||||
Files.createDirectories(filePath.getParent());
|
||||
|
||||
// 写入文件
|
||||
Files.writeString(filePath, params.content, StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
|
||||
// 生成结果
|
||||
String relativePath = getRelativePath(filePath);
|
||||
long lineCount = params.content.lines().count();
|
||||
long byteCount = params.content.getBytes(StandardCharsets.UTF_8).length;
|
||||
|
||||
if (isNewFile) {
|
||||
String successMessage = String.format("Successfully created file: %s (%d lines, %d bytes)",
|
||||
params.filePath, lineCount, byteCount);
|
||||
String displayMessage = String.format("Created %s (%d lines)", relativePath, lineCount);
|
||||
return ToolResult.success(successMessage, displayMessage);
|
||||
} else {
|
||||
String diff = generateDiff(filePath.getFileName().toString(), originalContent, params.content);
|
||||
String successMessage = String.format("Successfully wrote to file: %s (%d lines, %d bytes)",
|
||||
params.filePath, lineCount, byteCount);
|
||||
return ToolResult.success(successMessage, new FileDiff(diff, filePath.getFileName().toString()));
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Error writing file: " + params.filePath, e);
|
||||
return ToolResult.error("Error writing file: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error writing file: " + params.filePath, e);
|
||||
return ToolResult.error("Unexpected error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String generateDiff(String fileName, String oldContent, String newContent) {
|
||||
try {
|
||||
List<String> oldLines = Arrays.asList(oldContent.split("\n"));
|
||||
List<String> newLines = Arrays.asList(newContent.split("\n"));
|
||||
|
||||
Patch<String> patch = DiffUtils.diff(oldLines, newLines);
|
||||
List<String> unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(
|
||||
fileName + " (Original)",
|
||||
fileName + " (New)",
|
||||
oldLines,
|
||||
patch,
|
||||
3 // context lines
|
||||
);
|
||||
|
||||
return String.join("\n", unifiedDiff);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Could not generate diff", e);
|
||||
return "Diff generation failed: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private void createBackup(Path filePath, String content) throws IOException {
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
||||
String backupFileName = filePath.getFileName().toString() + ".backup." + timestamp;
|
||||
Path backupPath = filePath.getParent().resolve(backupFileName);
|
||||
|
||||
Files.writeString(backupPath, content, StandardCharsets.UTF_8);
|
||||
logger.info("Created backup: {}", backupPath);
|
||||
}
|
||||
|
||||
private boolean shouldCreateBackup() {
|
||||
// 可以从配置中读取,这里简化为总是创建备份
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isWithinWorkspace(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory).toRealPath();
|
||||
Path normalizedPath = filePath.normalize();
|
||||
return normalizedPath.startsWith(workspaceRoot.normalize());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not resolve workspace path", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAllowedFileType(String fileName) {
|
||||
List<String> allowedExtensions = appProperties.getWorkspace().getAllowedExtensions();
|
||||
return allowedExtensions.stream()
|
||||
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
|
||||
}
|
||||
|
||||
private String getRelativePath(Path filePath) {
|
||||
try {
|
||||
Path workspaceRoot = Paths.get(rootDirectory);
|
||||
return workspaceRoot.relativize(filePath).toString();
|
||||
} catch (Exception e) {
|
||||
return filePath.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入文件参数
|
||||
*/
|
||||
public static class WriteFileParams {
|
||||
@JsonProperty("file_path")
|
||||
private String filePath;
|
||||
|
||||
private String content;
|
||||
|
||||
// 构造器
|
||||
public WriteFileParams() {}
|
||||
|
||||
public WriteFileParams(String filePath, String content) {
|
||||
this.filePath = filePath;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getFilePath() { return filePath; }
|
||||
public void setFilePath(String filePath) { this.filePath = filePath; }
|
||||
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("WriteFileParams{path='%s', contentLength=%d}",
|
||||
filePath, content != null ? content.length() : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.example.demo.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
/**
|
||||
* 浏览器工具类
|
||||
* 用于跨平台打开默认浏览器
|
||||
*/
|
||||
public class BrowserUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BrowserUtil.class);
|
||||
|
||||
/**
|
||||
* 打开默认浏览器访问指定URL
|
||||
*
|
||||
* @param url 要访问的URL
|
||||
* @return 是否成功打开
|
||||
*/
|
||||
public static boolean openBrowser(String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
logger.warn("URL is null or empty, cannot open browser");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 方法1: 使用Desktop API (推荐)
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
Desktop desktop = Desktop.getDesktop();
|
||||
if (desktop.isSupported(Desktop.Action.BROWSE)) {
|
||||
desktop.browse(new URI(url));
|
||||
logger.info("Successfully opened browser with URL: {}", url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 方法2: 使用系统命令 (备用方案)
|
||||
return openBrowserWithCommand(url);
|
||||
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
logger.error("Failed to open browser with Desktop API for URL: {}", url, e);
|
||||
// 尝试备用方案
|
||||
return openBrowserWithCommand(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用系统命令打开浏览器 (备用方案)
|
||||
*
|
||||
* @param url 要访问的URL
|
||||
* @return 是否成功打开
|
||||
*/
|
||||
private static boolean openBrowserWithCommand(String url) {
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
ProcessBuilder processBuilder;
|
||||
|
||||
if (os.contains("win")) {
|
||||
// Windows
|
||||
processBuilder = new ProcessBuilder("rundll32", "url.dll,FileProtocolHandler", url);
|
||||
} else if (os.contains("mac")) {
|
||||
// macOS
|
||||
processBuilder = new ProcessBuilder("open", url);
|
||||
} else {
|
||||
// Linux/Unix
|
||||
processBuilder = new ProcessBuilder("xdg-open", url);
|
||||
}
|
||||
|
||||
Process process = processBuilder.start();
|
||||
|
||||
// 等待一小段时间确保命令执行
|
||||
Thread.sleep(500);
|
||||
|
||||
logger.info("Successfully opened browser using system command for URL: {}", url);
|
||||
return true;
|
||||
|
||||
} catch (IOException | InterruptedException e) {
|
||||
logger.error("Failed to open browser using system command for URL: {}", url, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查URL是否有效
|
||||
*
|
||||
* @param url 要检查的URL
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValidUrl(String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
new URI(url);
|
||||
return url.startsWith("http://") || url.startsWith("https://");
|
||||
} catch (URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.example.demo.utils;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 跨平台路径处理工具类
|
||||
*/
|
||||
public class PathUtils {
|
||||
|
||||
/**
|
||||
* 构建跨平台兼容的绝对路径
|
||||
* @param basePath 基础路径
|
||||
* @param relativePath 相对路径部分
|
||||
* @return 跨平台兼容的绝对路径
|
||||
*/
|
||||
public static String buildPath(String basePath, String... relativePath) {
|
||||
return Paths.get(basePath, relativePath).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径,确保跨平台兼容
|
||||
* @param path 原始路径
|
||||
* @return 规范化后的路径
|
||||
*/
|
||||
public static String normalizePath(String path) {
|
||||
return Paths.get(path).normalize().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否为绝对路径
|
||||
* @param path 要检查的路径
|
||||
* @return 是否为绝对路径
|
||||
*/
|
||||
public static boolean isAbsolute(String path) {
|
||||
return Paths.get(path).isAbsolute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前工作目录
|
||||
* @return 当前工作目录的绝对路径
|
||||
*/
|
||||
public static String getCurrentWorkingDirectory() {
|
||||
return System.getProperty("user.dir");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工作空间路径
|
||||
* @return 工作空间的绝对路径
|
||||
*/
|
||||
public static String buildWorkspacePath() {
|
||||
return buildPath(getCurrentWorkingDirectory(), "workspace");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
spring:
|
||||
application:
|
||||
name: springAI-alibaba-copilot
|
||||
ai:
|
||||
openai:
|
||||
base-url: https://dashscope.aliyuncs.com/compatible-mode
|
||||
api-key: xx
|
||||
chat:
|
||||
options:
|
||||
model: qwen-plus
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
app:
|
||||
# 工作目录配置
|
||||
workspace:
|
||||
# 使用 ${file.separator} 或让 Java 代码处理路径拼接
|
||||
root-directory: ${user.dir}/workspace # 改为使用正斜杠,Java会自动转换
|
||||
max-file-size: 10485760 # 10MB
|
||||
allowed-extensions:
|
||||
- .txt
|
||||
- .md
|
||||
- .java
|
||||
- .js
|
||||
- .ts
|
||||
- .json
|
||||
- .xml
|
||||
- .yml
|
||||
- .yaml
|
||||
- .properties
|
||||
- .html
|
||||
- .css
|
||||
- .sql
|
||||
|
||||
# 浏览器自动打开配置
|
||||
browser:
|
||||
# 是否启用项目启动后自动打开浏览器
|
||||
auto-open: true
|
||||
# 要打开的URL,默认为项目首页
|
||||
url: http://localhost:${server.port:8080}
|
||||
# 启动后延迟打开时间(秒)
|
||||
delay-seconds: 2
|
||||
|
||||
# 安全配置
|
||||
security:
|
||||
approval-mode: DEFAULT # DEFAULT, AUTO_EDIT, YOLO
|
||||
dangerous-commands:
|
||||
- rm
|
||||
- del
|
||||
- format
|
||||
- fdisk
|
||||
- mkfs
|
||||
|
||||
# 工具配置
|
||||
tools:
|
||||
read-file:
|
||||
enabled: true
|
||||
max-lines-per-read: 1000
|
||||
write-file:
|
||||
enabled: true
|
||||
backup-enabled: true
|
||||
edit-file:
|
||||
enabled: true
|
||||
diff-context-lines: 3
|
||||
list-directory:
|
||||
enabled: true
|
||||
max-depth: 5
|
||||
shell:
|
||||
enabled: true
|
||||
timeout-seconds: 30
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.example.demo: DEBUG
|
||||
com.example.demo.tools: INFO
|
||||
com.example.demo.controller: INFO
|
||||
com.example.demo.service: INFO
|
||||
com.example.demo.config: DEBUG
|
||||
# 禁用 Spring AI 默认工具调用日志,使用我们的自定义日志
|
||||
org.springframework.ai.model.tool.DefaultToolCallingManager: WARN
|
||||
org.springframework.ai.tool.method.MethodToolCallback: WARN
|
||||
org.springframework.ai: INFO
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file:
|
||||
name: logs/copilot-file-ops.log
|
||||
@@ -0,0 +1,591 @@
|
||||
/* SpringAI Alibaba Copilot - 主样式文件 */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message.user > div {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
}
|
||||
|
||||
.message.assistant > div {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 18px 18px 18px 4px;
|
||||
}
|
||||
|
||||
.message > div {
|
||||
max-width: 70%;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.message-role {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-content pre {
|
||||
background: #f4f4f4;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
overflow-x: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.message-content code {
|
||||
background: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-area input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 25px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input-area input:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.input-area button {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-area button:first-of-type {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.input-area button:first-of-type:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: #e9ecef;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-area button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.quick-action {
|
||||
padding: 12px 16px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.quick-action:hover {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.status {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
z-index: 1000;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 工具日志容器样式 */
|
||||
.tool-log-container {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
margin: 15px 0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tool-log-header {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tool-log-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background: #e9ecef;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.connection-status.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.connection-status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.connection-status.completed {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.tool-log-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tool-card.running {
|
||||
border-left: 4px solid #ffc107;
|
||||
background: #fff8e1;
|
||||
}
|
||||
|
||||
.tool-card.success {
|
||||
border-left: 4px solid #28a745;
|
||||
background: #f8fff9;
|
||||
}
|
||||
|
||||
.tool-card.error {
|
||||
border-left: 4px solid #dc3545;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.tool-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tool-status {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
background: #e9ecef;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tool-status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.tool-status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.tool-file {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tool-message {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tool-time {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* 等待状态卡片样式 */
|
||||
.tool-log-container.waiting {
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #e8f2ff 100%);
|
||||
border: 2px dashed #4285f4;
|
||||
animation: waitingPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.waiting-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid #e3e3e3;
|
||||
border-top: 3px solid #4285f4;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 12px;
|
||||
}
|
||||
|
||||
.waiting-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #4285f4;
|
||||
}
|
||||
|
||||
.waiting-hint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.connection-status.connecting {
|
||||
color: #4285f4;
|
||||
animation: blink 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 改进加载状态样式 */
|
||||
.loading.show {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
@keyframes waitingPulse {
|
||||
0%, 100% {
|
||||
border-color: #4285f4;
|
||||
box-shadow: 0 0 0 0 rgba(66, 133, 244, 0.3);
|
||||
}
|
||||
50% {
|
||||
border-color: #34a853;
|
||||
box-shadow: 0 0 0 8px rgba(66, 133, 244, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
width: 95%;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.message > div {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.input-area input {
|
||||
font-size: 16px; /* 防止iOS缩放 */
|
||||
}
|
||||
|
||||
.quick-action {
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.waiting-message {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.waiting-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.waiting-hint {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tool-log-container {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.tool-log-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 流式消息样式 */
|
||||
.message.streaming {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stream-content {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.stream-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #007bff;
|
||||
animation: typing 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.stream-indicator .error {
|
||||
color: #dc3545;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* SpringAI Alibaba Copilot - 主JavaScript文件
|
||||
* 处理聊天界面交互、SSE连接和工具日志显示
|
||||
*/
|
||||
|
||||
// 全局变量
|
||||
const messagesContainer = document.getElementById('messages');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const clearBtn = document.getElementById('clearBtn');
|
||||
const loading = document.getElementById('loading');
|
||||
const status = document.getElementById('status');
|
||||
|
||||
// 全局错误处理
|
||||
window.addEventListener('error', function(event) {
|
||||
console.error('Global JavaScript error:', event.error);
|
||||
if (event.error && event.error.message && event.error.message.includes('userMessage')) {
|
||||
console.error('Detected userMessage error, this might be a variable scope issue');
|
||||
}
|
||||
});
|
||||
|
||||
// 函数声明会被提升,但为了安全起见,我们在页面加载后再设置全局引用
|
||||
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
const message = messageInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// 添加用户消息
|
||||
addMessage('user', message);
|
||||
messageInput.value = '';
|
||||
|
||||
// 显示加载状态
|
||||
showLoading(true);
|
||||
setButtonsEnabled(false);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/chat/message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ message: message })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// 如果是异步任务(工具调用),建立SSE连接
|
||||
if (data.taskId && data.asyncTask) {
|
||||
// 先显示等待状态的工具卡片
|
||||
showWaitingToolCard();
|
||||
logStreamManager.startLogStream(data.taskId);
|
||||
showStatus('任务已启动,正在建立实时连接...', 'success');
|
||||
} else if (data.streamResponse) {
|
||||
// 流式对话响应
|
||||
handleStreamResponse(message);
|
||||
showStatus('开始流式对话...', 'success');
|
||||
} else {
|
||||
// 同步任务,直接显示结果
|
||||
addMessage('assistant', data.message);
|
||||
|
||||
// 显示连续对话统计信息
|
||||
let statusMessage = 'Message sent successfully';
|
||||
if (data.totalTurns && data.totalTurns > 1) {
|
||||
statusMessage += ` (${data.totalTurns} turns`;
|
||||
if (data.totalDurationMs) {
|
||||
statusMessage += `, ${(data.totalDurationMs / 1000).toFixed(1)}s`;
|
||||
}
|
||||
statusMessage += ')';
|
||||
|
||||
if (data.reachedMaxTurns) {
|
||||
statusMessage += ' - Reached max turns limit';
|
||||
}
|
||||
if (data.stopReason) {
|
||||
statusMessage += ` - ${data.stopReason}`;
|
||||
}
|
||||
}
|
||||
showStatus(statusMessage, 'success');
|
||||
}
|
||||
} else {
|
||||
addMessage('assistant', data.message);
|
||||
showStatus('Error: ' + data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
// 更安全的错误处理
|
||||
const errorMessage = error && error.message ? error.message : 'Unknown error occurred';
|
||||
addMessage('assistant', 'Sorry, there was an error processing your request: ' + errorMessage);
|
||||
showStatus('Network error: ' + errorMessage, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
setButtonsEnabled(true);
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 快速操作
|
||||
function quickAction(message) {
|
||||
messageInput.value = message;
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
// 清除历史
|
||||
async function clearHistory() {
|
||||
try {
|
||||
await fetch('/api/chat/clear', { method: 'POST' });
|
||||
messagesContainer.innerHTML = '';
|
||||
showStatus('History cleared', 'success');
|
||||
} catch (error) {
|
||||
showStatus('Error clearing history', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 添加消息到界面
|
||||
function addMessage(role, content) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${role}`;
|
||||
|
||||
// 处理代码块和格式化
|
||||
const formattedContent = formatMessage(content);
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div>
|
||||
<div class="message-role">${role === 'user' ? 'You' : 'Assistant'}</div>
|
||||
<div class="message-content">${formattedContent}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 格式化消息内容
|
||||
function formatMessage(content) {
|
||||
// 简单的代码块处理
|
||||
content = content.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
|
||||
|
||||
// 处理行内代码
|
||||
content = content.replace(/`([^`]+)`/g, '<code style="background: #f0f0f0; padding: 2px 4px; border-radius: 3px;">$1</code>');
|
||||
|
||||
// 处理换行
|
||||
content = content.replace(/\n/g, '<br>');
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// 显示/隐藏加载状态
|
||||
function showLoading(show) {
|
||||
loading.classList.toggle('show', show);
|
||||
}
|
||||
|
||||
// 启用/禁用按钮
|
||||
function setButtonsEnabled(enabled) {
|
||||
sendBtn.disabled = !enabled;
|
||||
clearBtn.disabled = !enabled;
|
||||
}
|
||||
|
||||
// 显示状态消息
|
||||
function showStatus(message, type) {
|
||||
status.textContent = message;
|
||||
status.className = `status ${type}`;
|
||||
status.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
status.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 显示等待状态的工具卡片
|
||||
function showWaitingToolCard() {
|
||||
const waitingCard = document.createElement('div');
|
||||
waitingCard.className = 'tool-log-container waiting';
|
||||
waitingCard.id = 'waiting-tool-card';
|
||||
waitingCard.innerHTML = `
|
||||
<div class="tool-log-header">
|
||||
<span class="tool-log-title">🔧 工具执行准备中</span>
|
||||
<span class="connection-status connecting">连接中...</span>
|
||||
</div>
|
||||
<div class="tool-log-content">
|
||||
<div class="waiting-message">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="waiting-text">正在等待工具执行推送...</div>
|
||||
<div class="waiting-hint">AI正在分析您的请求并准备执行相应的工具操作</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(waitingCard);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 处理流式响应
|
||||
function handleStreamResponse(userMessage) {
|
||||
console.log('🌊 开始处理流式响应,消息:', userMessage);
|
||||
|
||||
// 参数验证
|
||||
if (!userMessage) {
|
||||
console.error('handleStreamResponse: userMessage is undefined or empty');
|
||||
showStatus('流式响应参数错误', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建流式消息容器
|
||||
const streamMessageId = 'stream-message-' + Date.now();
|
||||
const streamContainer = document.createElement('div');
|
||||
streamContainer.className = 'message assistant streaming';
|
||||
streamContainer.id = streamMessageId;
|
||||
streamContainer.innerHTML = `
|
||||
<div class="message-content">
|
||||
<div class="stream-content"></div>
|
||||
<div class="stream-indicator">
|
||||
<div class="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(streamContainer);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
|
||||
// 使用fetch API处理流式响应
|
||||
const streamContent = streamContainer.querySelector('.stream-content');
|
||||
const streamIndicator = streamContainer.querySelector('.stream-indicator');
|
||||
let fullContent = '';
|
||||
|
||||
fetch('/api/chat/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ message: userMessage })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
function readStream() {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
console.log('✅ 流式响应完成');
|
||||
streamIndicator.style.display = 'none';
|
||||
streamContainer.classList.remove('streaming');
|
||||
showStatus('流式对话完成', 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
console.log('📨 收到流式数据块:', chunk);
|
||||
|
||||
// 处理SSE格式的数据
|
||||
const lines = chunk.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.substring(6);
|
||||
if (data === '[DONE]') {
|
||||
console.log('✅ 流式响应完成');
|
||||
streamIndicator.style.display = 'none';
|
||||
streamContainer.classList.remove('streaming');
|
||||
showStatus('流式对话完成', 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
// 追加内容
|
||||
fullContent += data;
|
||||
streamContent.textContent = fullContent;
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return readStream();
|
||||
});
|
||||
}
|
||||
|
||||
return readStream();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ 流式响应错误:', error);
|
||||
const errorMessage = error && error.message ? error.message : 'Unknown stream error';
|
||||
streamIndicator.innerHTML = '<span class="error">连接错误: ' + errorMessage + '</span>';
|
||||
showStatus('流式对话连接错误: ' + errorMessage, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 移除等待状态卡片
|
||||
function removeWaitingToolCard() {
|
||||
const waitingCard = document.getElementById('waiting-tool-card');
|
||||
if (waitingCard) {
|
||||
waitingCard.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 事件监听器
|
||||
messageInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 调试函数
|
||||
function debugVariables() {
|
||||
console.log('=== Debug Variables ===');
|
||||
console.log('messagesContainer:', messagesContainer);
|
||||
console.log('messageInput:', messageInput);
|
||||
console.log('sendBtn:', sendBtn);
|
||||
console.log('clearBtn:', clearBtn);
|
||||
console.log('loading:', loading);
|
||||
console.log('status:', status);
|
||||
console.log('addMessage function:', typeof addMessage);
|
||||
console.log('showStatus function:', typeof showStatus);
|
||||
console.log('logStreamManager:', typeof logStreamManager);
|
||||
}
|
||||
|
||||
// 页面加载完成后聚焦输入框
|
||||
window.addEventListener('load', function() {
|
||||
messageInput.focus();
|
||||
|
||||
// 确保函数在全局作用域中可用
|
||||
window.addMessage = addMessage;
|
||||
window.showStatus = showStatus;
|
||||
|
||||
// 调试信息
|
||||
debugVariables();
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* SSE日志流管理器
|
||||
* 负责管理Server-Sent Events连接和工具日志显示
|
||||
*/
|
||||
|
||||
// SSE实时日志管理器
|
||||
class LogStreamManager {
|
||||
constructor() {
|
||||
this.activeConnections = new Map(); // taskId -> EventSource
|
||||
this.toolLogDisplays = new Map(); // taskId -> ToolLogDisplay
|
||||
}
|
||||
|
||||
// 建立SSE连接
|
||||
startLogStream(taskId) {
|
||||
if (this.activeConnections.has(taskId)) {
|
||||
console.log('SSE连接已存在:', taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔗 建立SSE连接:', taskId);
|
||||
|
||||
// 创建工具日志显示组件
|
||||
const toolLogDisplay = new ToolLogDisplay(taskId);
|
||||
this.toolLogDisplays.set(taskId, toolLogDisplay);
|
||||
|
||||
// 建立EventSource连接
|
||||
const eventSource = new EventSource(`/api/logs/stream/${taskId}`);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('✅ SSE连接建立成功:', taskId);
|
||||
toolLogDisplay.showConnectionStatus('已连接');
|
||||
};
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const logEvent = JSON.parse(event.data);
|
||||
console.log('📨 收到日志事件:', logEvent);
|
||||
this.handleLogEvent(taskId, logEvent);
|
||||
} catch (error) {
|
||||
console.error('解析日志事件失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听特定的 "log" 事件
|
||||
eventSource.addEventListener('log', (event) => {
|
||||
try {
|
||||
const logEvent = JSON.parse(event.data);
|
||||
console.log('📨 收到log事件:', logEvent);
|
||||
this.handleLogEvent(taskId, logEvent);
|
||||
} catch (error) {
|
||||
console.error('解析log事件失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('❌ SSE连接错误:', error);
|
||||
toolLogDisplay.showConnectionStatus('连接错误');
|
||||
this.handleConnectionError(taskId);
|
||||
};
|
||||
|
||||
this.activeConnections.set(taskId, eventSource);
|
||||
}
|
||||
|
||||
// 处理日志事件
|
||||
handleLogEvent(taskId, logEvent) {
|
||||
const toolLogDisplay = this.toolLogDisplays.get(taskId);
|
||||
if (!toolLogDisplay) {
|
||||
console.warn('找不到工具日志显示组件:', taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (logEvent.type) {
|
||||
case 'CONNECTION_ESTABLISHED':
|
||||
toolLogDisplay.showConnectionStatus('已连接');
|
||||
// 连接建立后,如果5秒内没有工具事件,显示提示
|
||||
setTimeout(() => {
|
||||
const waitingCard = document.getElementById('waiting-tool-card');
|
||||
if (waitingCard) {
|
||||
const waitingText = waitingCard.querySelector('.waiting-text');
|
||||
if (waitingText) {
|
||||
waitingText.textContent = '连接已建立,等待AI开始执行工具...';
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
break;
|
||||
case 'TOOL_START':
|
||||
toolLogDisplay.addToolStart(logEvent);
|
||||
break;
|
||||
case 'TOOL_SUCCESS':
|
||||
toolLogDisplay.updateToolSuccess(logEvent);
|
||||
break;
|
||||
case 'TOOL_ERROR':
|
||||
toolLogDisplay.updateToolError(logEvent);
|
||||
break;
|
||||
case 'TASK_COMPLETE':
|
||||
toolLogDisplay.showTaskComplete();
|
||||
this.handleTaskComplete(taskId);
|
||||
this.closeConnection(taskId);
|
||||
break;
|
||||
default:
|
||||
console.log('未知日志事件类型:', logEvent.type);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭SSE连接
|
||||
closeConnection(taskId) {
|
||||
const eventSource = this.activeConnections.get(taskId);
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
this.activeConnections.delete(taskId);
|
||||
console.log('🔚 关闭SSE连接:', taskId);
|
||||
}
|
||||
|
||||
// 延迟移除显示组件
|
||||
setTimeout(() => {
|
||||
const toolLogDisplay = this.toolLogDisplays.get(taskId);
|
||||
if (toolLogDisplay) {
|
||||
toolLogDisplay.fadeOut();
|
||||
this.toolLogDisplays.delete(taskId);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// 处理任务完成
|
||||
async handleTaskComplete(taskId) {
|
||||
try {
|
||||
// 获取对话结果
|
||||
const response = await fetch(`/api/task/result/${taskId}`);
|
||||
const resultData = await response.json();
|
||||
|
||||
// 安全地显示最终结果
|
||||
if (typeof addMessage === 'function' && resultData && resultData.fullResponse) {
|
||||
addMessage('assistant', resultData.fullResponse);
|
||||
} else {
|
||||
console.error('addMessage function not available or invalid result data');
|
||||
}
|
||||
|
||||
// 显示统计信息
|
||||
let statusMessage = '对话完成';
|
||||
if (resultData.totalTurns > 1) {
|
||||
statusMessage += ` (${resultData.totalTurns} 轮`;
|
||||
if (resultData.totalDurationMs) {
|
||||
statusMessage += `, ${(resultData.totalDurationMs / 1000).toFixed(1)}秒`;
|
||||
}
|
||||
statusMessage += ')';
|
||||
|
||||
if (resultData.reachedMaxTurns) {
|
||||
statusMessage += ' - 达到最大轮次限制';
|
||||
}
|
||||
if (resultData.stopReason) {
|
||||
statusMessage += ` - ${resultData.stopReason}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 安全地调用showStatus函数
|
||||
if (typeof showStatus === 'function') {
|
||||
showStatus(statusMessage, 'success');
|
||||
} else {
|
||||
console.log(statusMessage);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取对话结果失败:', error);
|
||||
// 安全地调用showStatus函数
|
||||
if (typeof showStatus === 'function') {
|
||||
showStatus('获取对话结果失败', 'error');
|
||||
} else {
|
||||
console.error('获取对话结果失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理连接错误
|
||||
handleConnectionError(taskId) {
|
||||
// 可以实现重连逻辑
|
||||
console.log('处理连接错误:', taskId);
|
||||
setTimeout(() => {
|
||||
if (!this.activeConnections.has(taskId)) {
|
||||
console.log('尝试重连:', taskId);
|
||||
this.startLogStream(taskId);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建SSE日志流管理器实例
|
||||
const logStreamManager = new LogStreamManager();
|
||||
|
||||
// 确保在全局作用域中可用
|
||||
window.logStreamManager = logStreamManager;
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 工具日志显示组件
|
||||
* 负责显示工具执行的实时状态和结果
|
||||
*/
|
||||
|
||||
class ToolLogDisplay {
|
||||
constructor(taskId) {
|
||||
this.taskId = taskId;
|
||||
this.toolCards = new Map(); // toolName -> DOM element
|
||||
this.container = this.createContainer();
|
||||
this.appendToPage();
|
||||
}
|
||||
|
||||
// 创建容器
|
||||
createContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'tool-log-container';
|
||||
container.id = `tool-log-${this.taskId}`;
|
||||
container.innerHTML = `
|
||||
<div class="tool-log-header">
|
||||
<span class="tool-log-title">🔧 工具执行日志</span>
|
||||
<span class="connection-status">连接中...</span>
|
||||
</div>
|
||||
<div class="tool-log-content">
|
||||
<!-- 工具卡片将在这里动态添加 -->
|
||||
</div>
|
||||
`;
|
||||
return container;
|
||||
}
|
||||
|
||||
// 添加到页面
|
||||
appendToPage() {
|
||||
messagesContainer.appendChild(this.container);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 显示连接状态
|
||||
showConnectionStatus(status) {
|
||||
const statusElement = this.container.querySelector('.connection-status');
|
||||
if (statusElement) {
|
||||
statusElement.textContent = status;
|
||||
statusElement.className = `connection-status ${status === '已连接' ? 'connected' : 'error'}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加工具开始执行
|
||||
addToolStart(logEvent) {
|
||||
// 移除等待状态卡片(如果存在)
|
||||
removeWaitingToolCard();
|
||||
|
||||
const toolCard = this.createToolCard(logEvent);
|
||||
const content = this.container.querySelector('.tool-log-content');
|
||||
content.appendChild(toolCard);
|
||||
|
||||
this.toolCards.set(logEvent.toolName, toolCard);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
// 更新工具执行成功
|
||||
updateToolSuccess(logEvent) {
|
||||
const toolCard = this.toolCards.get(logEvent.toolName);
|
||||
if (toolCard) {
|
||||
this.updateToolCard(toolCard, logEvent, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新工具执行失败
|
||||
updateToolError(logEvent) {
|
||||
const toolCard = this.toolCards.get(logEvent.toolName);
|
||||
if (toolCard) {
|
||||
this.updateToolCard(toolCard, logEvent, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建工具卡片
|
||||
createToolCard(logEvent) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'tool-card running';
|
||||
card.innerHTML = `
|
||||
<div class="tool-header">
|
||||
<span class="tool-icon">${logEvent.icon}</span>
|
||||
<span class="tool-name">${logEvent.toolName}</span>
|
||||
<span class="tool-status">⏳ 执行中</span>
|
||||
</div>
|
||||
<div class="tool-file">📁 ${this.getFileName(logEvent.filePath)}</div>
|
||||
<div class="tool-message">${logEvent.message}</div>
|
||||
<div class="tool-time">开始时间: ${logEvent.timestamp}</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
// 更新工具卡片
|
||||
updateToolCard(toolCard, logEvent, status) {
|
||||
toolCard.className = `tool-card ${status}`;
|
||||
|
||||
const statusElement = toolCard.querySelector('.tool-status');
|
||||
const messageElement = toolCard.querySelector('.tool-message');
|
||||
const timeElement = toolCard.querySelector('.tool-time');
|
||||
|
||||
if (status === 'success') {
|
||||
statusElement.innerHTML = '✅ 完成';
|
||||
statusElement.className = 'tool-status success';
|
||||
} else if (status === 'error') {
|
||||
statusElement.innerHTML = '❌ 失败';
|
||||
statusElement.className = 'tool-status error';
|
||||
}
|
||||
|
||||
messageElement.textContent = logEvent.message;
|
||||
|
||||
if (logEvent.executionTime) {
|
||||
timeElement.textContent = `完成时间: ${logEvent.timestamp} (耗时: ${logEvent.executionTime}ms)`;
|
||||
}
|
||||
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
// 显示任务完成
|
||||
showTaskComplete() {
|
||||
const header = this.container.querySelector('.tool-log-header');
|
||||
header.innerHTML = `
|
||||
<span class="tool-log-title">🎉 任务执行完成</span>
|
||||
<span class="connection-status completed">已完成</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// 淡出效果
|
||||
fadeOut() {
|
||||
this.container.style.transition = 'opacity 1s ease-out';
|
||||
this.container.style.opacity = '0.5';
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
}, 10000); // 10秒后移除
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
scrollToBottom() {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
getFileName(filePath) {
|
||||
if (!filePath) return '未知文件';
|
||||
const parts = filePath.split(/[/\\]/);
|
||||
return parts[parts.length - 1] || filePath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SpringAI Alibaba Copilot</title>
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🤖 SpringAI Alibaba 编码助手</h1>
|
||||
<p>AI助手将分析您的需求,制定执行计划,并逐步完成任务</p>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="chat-area">
|
||||
<div class="messages" id="messages">
|
||||
<div class="message assistant">
|
||||
<div>
|
||||
<div class="message-role">Assistant</div>
|
||||
<div class="message-content">
|
||||
👋 Hello! I'm your AI file operations assistant. I can help you:
|
||||
<br><br>
|
||||
📁 <strong>Read files</strong> - View file contents with pagination support
|
||||
<br>✏️ <strong>Write files</strong> - Create new files or overwrite existing ones
|
||||
<br>🔧 <strong>Edit files</strong> - Make precise edits with diff preview
|
||||
<br>📋 <strong>List directories</strong> - Browse directory structure
|
||||
<br><br>
|
||||
Try asking me to create a simple project, read a file, or explore the workspace!
|
||||
<br><br>
|
||||
<em>Workspace: /workspace</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div>🤔 AI is thinking...</div>
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<input type="text" id="messageInput" placeholder="Ask me to create files, read content, or manage your project..." />
|
||||
<button onclick="sendMessage()" id="sendBtn">Send</button>
|
||||
<button onclick="clearHistory()" class="clear-btn" id="clearBtn">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar">
|
||||
<h3>🚀 Quick Actions</h3>
|
||||
<div class="quick-actions">
|
||||
<div class="quick-action" onclick="quickAction('List the workspace directory')">
|
||||
📁 List workspace directory
|
||||
</div>
|
||||
<div class="quick-action" onclick="quickAction('Create a simple Java Hello World project')">
|
||||
☕ Create Java Hello World
|
||||
</div>
|
||||
<div class="quick-action" onclick="quickAction('Create a simple web project with HTML, CSS and JS')">
|
||||
🌐 Create web project
|
||||
</div>
|
||||
<div class="quick-action" onclick="quickAction('Create a README.md file for this project')">
|
||||
📝 Create README.md
|
||||
</div>
|
||||
<div class="quick-action" onclick="quickAction('Show me the structure of the current directory recursively')">
|
||||
🌳 Show directory tree
|
||||
</div>
|
||||
<div class="quick-action" onclick="quickAction('Create a simple Python script that prints hello world')">
|
||||
🐍 Create Python script
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>🔄 Continuous Task Tests</h3>
|
||||
<div class="quick-actions">
|
||||
<div class="quick-action" onclick="quickAction('Create a complete React project with components, styles, and package.json')">
|
||||
⚛️ Create React project
|
||||
</div>
|
||||
<div class="quick-action" onclick="quickAction('Create a full-stack todo app with HTML, CSS, JavaScript frontend and Node.js backend')">
|
||||
📋 Create Todo App
|
||||
</div>
|
||||
<div class="quick-action" onclick="quickAction('Create a Spring Boot REST API project with controller, service, and model classes')">
|
||||
🍃 Create Spring Boot API
|
||||
</div>
|
||||
<div class="quick-action" onclick="quickAction('Create a complete blog website with multiple HTML pages, CSS styles, and JavaScript functionality')">
|
||||
📰 Create Blog Website
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status" style="display: none;"></div>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<h3>💡 Tips</h3>
|
||||
<div style="font-size: 12px; color: #666; line-height: 1.4;">
|
||||
• Ask for step-by-step project creation<br>
|
||||
• Request file content before editing<br>
|
||||
• Use specific file paths<br>
|
||||
• Ask for directory structure first<br>
|
||||
• Try continuous operations
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript文件引用 -->
|
||||
<script src="/js/tool-log-display.js"></script>
|
||||
<script src="/js/sse-manager.js"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user