ai编程助手

This commit is contained in:
ageerle
2025-07-07 12:36:53 +08:00
parent c1a178c0be
commit 0eff37fa51
50 changed files with 11899 additions and 0 deletions

View 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>

View File

@@ -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();
}
}

View File

@@ -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 // 完全自动模式,所有操作都不需要确认
}
}

View File

@@ -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 "未找到路径参数";
}
}

View File

@@ -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; }
}
}

View File

@@ -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("🎉 ========================================");
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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();
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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";
}
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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";
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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>&copy; %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")
);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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");
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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();
});

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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>