feat: 全局格式化代码

This commit is contained in:
evo
2025-12-06 14:38:41 +08:00
parent 96c53390aa
commit 7c7d5838cd
538 changed files with 21132 additions and 14160 deletions

View File

@@ -1,6 +1,6 @@
<?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"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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>

View File

@@ -1,6 +1,6 @@
<?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"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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>

View File

@@ -1,5 +1,7 @@
package com.example.demo;
import com.example.demo.config.AppProperties;
import com.example.demo.util.BrowserUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -11,9 +13,6 @@ 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. 文件读取、写入、编辑

View File

@@ -17,17 +17,46 @@ public class AppProperties {
private Browser browser = new Browser();
// Getters and Setters
public Workspace getWorkspace() { return workspace; }
public void setWorkspace(Workspace workspace) { this.workspace = workspace; }
public Workspace getWorkspace() {
return workspace;
}
public Security getSecurity() { return security; }
public void setSecurity(Security security) { this.security = security; }
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
public Tools getTools() { return tools; }
public void setTools(Tools tools) { this.tools = tools; }
public Security getSecurity() {
return security;
}
public Browser getBrowser() { return browser; }
public void setBrowser(Browser browser) { this.browser = browser; }
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 enum ApprovalMode {
DEFAULT, // 默认模式,危险操作需要确认
AUTO_EDIT, // 自动编辑模式,文件编辑不需要确认
YOLO // 完全自动模式,所有操作都不需要确认
}
/**
* 工作空间配置
@@ -37,22 +66,35 @@ public class AppProperties {
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"
".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) {
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 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;
}
}
/**
@@ -63,11 +105,21 @@ public class AppProperties {
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 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;
}
}
/**
@@ -81,20 +133,45 @@ public class AppProperties {
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 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;
}
}
/**
@@ -103,11 +180,20 @@ public class AppProperties {
public static class ToolConfig {
private boolean enabled;
public ToolConfig() {}
public ToolConfig(boolean enabled) { this.enabled = enabled; }
public ToolConfig() {
}
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public ToolConfig(boolean enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
/**
@@ -119,22 +205,28 @@ public class AppProperties {
private int delaySeconds = 2;
// Getters and Setters
public boolean isAutoOpen() { return autoOpen; }
public void setAutoOpen(boolean autoOpen) { this.autoOpen = autoOpen; }
public boolean isAutoOpen() {
return autoOpen;
}
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public void setAutoOpen(boolean autoOpen) {
this.autoOpen = autoOpen;
}
public int getDelaySeconds() { return delaySeconds; }
public void setDelaySeconds(int delaySeconds) { this.delaySeconds = delaySeconds; }
}
public String getUrl() {
return url;
}
/**
* 审批模式
*/
public enum ApprovalMode {
DEFAULT, // 默认模式,危险操作需要确认
AUTO_EDIT, // 自动编辑模式,文件编辑不需要确认
YOLO // 完全自动模式,所有操作都不需要确认
public void setUrl(String url) {
this.url = url;
}
public int getDelaySeconds() {
return delaySeconds;
}
public void setDelaySeconds(int delaySeconds) {
this.delaySeconds = delaySeconds;
}
}
}

View File

@@ -9,50 +9,50 @@ import org.springframework.stereotype.Component;
/**
* 自定义工具执行监听器
* 提供中文日志和详细的文件操作信息记录
*
* <p>
* 注意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));
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);
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);
logger.error("❌ [Spring AI] 工具执行失败: {} | 耗时: {}ms | 文件/目录: {} | 错误: {}",
toolName, executionTime, fileInfo, errorMessage);
executionLogger.logToolError(callId, toolName, errorMessage, executionTime);
}
/**
* 提取文件信息用于日志记录
*/
@@ -86,7 +86,7 @@ public class CustomToolExecutionMonitor {
return "解析文件路径失败: " + e.getMessage();
}
}
/**
* 从参数中提取路径
*/

View File

@@ -25,13 +25,13 @@ public class GlobalExceptionHandler {
@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."
"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);
}
@@ -41,24 +41,24 @@ public class GlobalExceptionHandler {
@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."
"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."
"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);
}
@@ -68,13 +68,13 @@ public class GlobalExceptionHandler {
@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."
"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);
}
@@ -95,16 +95,36 @@ public class GlobalExceptionHandler {
}
// Getters and setters
public String getErrorCode() { return errorCode; }
public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
public String getErrorCode() {
return errorCode;
}
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getSuggestion() { return suggestion; }
public void setSuggestion(String suggestion) { this.suggestion = suggestion; }
public String getMessage() {
return message;
}
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
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

@@ -16,9 +16,9 @@ import java.time.format.DateTimeFormatter;
*/
@Configuration
public class LoggingConfiguration {
private static final Logger logger = LoggerFactory.getLogger(LoggingConfiguration.class);
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
// 确保日志目录存在
@@ -29,7 +29,7 @@ public class LoggingConfiguration {
logger.info("📁 创建日志目录: {}", logsDir.getAbsolutePath());
}
}
// 记录应用启动信息
String startTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
logger.info("🎉 ========================================");

View File

@@ -18,74 +18,73 @@ public class SpringAIConfiguration {
@Bean
public ChatClient chatClient(ChatModel chatModel,
FileOperationTools fileOperationTools,
SmartEditTool smartEditTool,
AnalyzeProjectTool analyzeProjectTool,
ProjectScaffoldTool projectScaffoldTool,
AppProperties appProperties) {
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.
.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
# 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
# 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
# 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
# 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
# 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..."
# 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();
Remember: Your goal is to deliver COMPLETE solutions through continuous execution!
""")
.defaultTools(fileOperationTools, smartEditTool, analyzeProjectTool, projectScaffoldTool)
.build();
}
/**
* 为所有工具注入Schema验证器
*/

View File

@@ -5,30 +5,30 @@ 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 setCurrentTaskId(String taskId) {
taskIdHolder.set(taskId);
}
/**
* 清除当前任务ID
*/
public static void clearCurrentTaskId() {
taskIdHolder.remove();
}
/**
* 检查是否有当前任务ID
*/

View File

@@ -1,7 +1,7 @@
package com.example.demo.config;
import com.example.demo.service.ToolExecutionLogger;
import com.example.demo.service.LogStreamService;
import com.example.demo.service.ToolExecutionLogger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@@ -10,7 +10,6 @@ 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;
@@ -21,15 +20,15 @@ import java.util.regex.Pattern;
@Aspect
@Component
public class ToolCallLoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(ToolCallLoggingAspect.class);
@Autowired
private ToolExecutionLogger executionLogger;
@Autowired
private LogStreamService logStreamService;
/**
* 拦截使用@Tool注解的方法执行
*/
@@ -44,7 +43,7 @@ public class ToolCallLoggingAspect {
String fileInfo = extractFileInfoFromMethodArgs(methodName, args);
logger.debug("🚀 [Spring AI @Tool] 执行工具: {}.{} | 参数: {} | 文件/目录: {}",
className, methodName, parametersInfo, fileInfo);
className, methodName, parametersInfo, fileInfo);
// 获取当前任务ID (从线程本地变量或其他方式)
String taskId = getCurrentTaskId();
@@ -61,7 +60,7 @@ public class ToolCallLoggingAspect {
long executionTime = System.currentTimeMillis() - startTime;
logger.debug("✅ [Spring AI @Tool] 工具执行成功: {}.{} | 耗时: {}ms | 文件/目录: {} | 参数: {}",
className, methodName, executionTime, fileInfo, parametersInfo);
className, methodName, executionTime, fileInfo, parametersInfo);
// 推送工具执行成功事件
if (taskId != null) {
@@ -74,7 +73,7 @@ public class ToolCallLoggingAspect {
long executionTime = System.currentTimeMillis() - startTime;
logger.error("❌ [Spring AI @Tool] 工具执行失败: {}.{} | 耗时: {}ms | 文件/目录: {} | 参数: {} | 错误: {}",
className, methodName, executionTime, fileInfo, parametersInfo, e.getMessage());
className, methodName, executionTime, fileInfo, parametersInfo, e.getMessage());
// 推送工具执行失败事件
if (taskId != null) {
@@ -198,7 +197,7 @@ public class ToolCallLoggingAspect {
return "解析文件路径失败";
}
}
/**
* 从字符串中提取路径
*/
@@ -210,7 +209,7 @@ public class ToolCallLoggingAspect {
if (jsonMatcher.find()) {
return jsonMatcher.group(1);
}
// 键值对格式
Pattern kvPattern = Pattern.compile(key + "=([^,\\s\\]]+)");
Matcher kvMatcher = kvPattern.matcher(text);

View File

@@ -8,7 +8,6 @@ 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;
@@ -47,7 +46,6 @@ public class ChatController {
* 发送消息给AI - 支持连续工具调用
*/
// 在现有ChatController中修改sendMessage方法
@PostMapping("/message")
public Mono<ChatResponseDto> sendMessage(@RequestBody ChatRequestDto request) {
return Mono.fromCallable(() -> {
@@ -73,7 +71,7 @@ public class ChatController {
try {
logger.info("🚀 开始异步执行连续对话任务: {}", taskId);
continuousConversationService.executeContinuousConversation(
taskId, request.getMessage(), conversationHistory
taskId, request.getMessage(), conversationHistory
);
logger.info("✅ 连续对话任务完成: {}", taskId);
} catch (Exception e) {
@@ -105,7 +103,7 @@ public class ChatController {
logger.info("📤 返回流式响应标识");
return responseDto;
}
} catch (Exception e) {
logger.error("Error processing chat message", e);
ChatResponseDto errorResponse = new ChatResponseDto();
@@ -115,7 +113,6 @@ public class ChatController {
}
});
}
/**
@@ -132,27 +129,27 @@ public class ChatController {
// 使用Spring AI的流式API
Flux<String> contentStream = chatClient.prompt()
.messages(conversationHistory)
.stream()
.content();
.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();
.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());
@@ -177,13 +174,13 @@ public class ChatController {
@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();
.map(message -> {
MessageDto dto = new MessageDto();
dto.setContent(message.getText());
dto.setRole(message instanceof UserMessage ? "user" : "assistant");
return dto;
})
.toList();
return Mono.just(history);
}
@@ -202,57 +199,97 @@ public class ChatController {
private String stopReason;
private long totalDurationMs;
public String getTaskId() {
return taskId;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getMessage() {
return message;
}
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public void setMessage(String message) {
this.message = message;
}
public boolean isAsyncTask() {
return asyncTask;
}
public boolean isSuccess() {
return success;
}
public void setAsyncTask(boolean asyncTask) {
this.asyncTask = asyncTask;
}
public void setSuccess(boolean success) {
this.success = success;
}
public boolean isStreamResponse() {
return streamResponse;
}
public boolean isAsyncTask() {
return asyncTask;
}
public void setStreamResponse(boolean streamResponse) {
this.streamResponse = streamResponse;
}
public void setAsyncTask(boolean asyncTask) {
this.asyncTask = asyncTask;
}
public int getTotalTurns() { return totalTurns; }
public void setTotalTurns(int totalTurns) { this.totalTurns = totalTurns; }
public boolean isStreamResponse() {
return streamResponse;
}
public boolean isReachedMaxTurns() { return reachedMaxTurns; }
public void setReachedMaxTurns(boolean reachedMaxTurns) { this.reachedMaxTurns = reachedMaxTurns; }
public void setStreamResponse(boolean streamResponse) {
this.streamResponse = streamResponse;
}
public String getStopReason() { return stopReason; }
public void setStopReason(String stopReason) { this.stopReason = stopReason; }
public int getTotalTurns() {
return totalTurns;
}
public long getTotalDurationMs() { return totalDurationMs; }
public void setTotalDurationMs(long totalDurationMs) { this.totalDurationMs = totalDurationMs; }
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 getContent() {
return content;
}
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public void setContent(String content) {
this.content = content;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
}

View File

@@ -16,12 +16,12 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RequestMapping("/api/logs")
@CrossOrigin(origins = "*")
public class LogStreamController {
private static final Logger logger = LoggerFactory.getLogger(LogStreamController.class);
@Autowired
private LogStreamService logStreamService;
/**
* 建立SSE连接
* 前端通过此端点建立实时日志推送连接
@@ -29,7 +29,7 @@ public class LogStreamController {
@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);
@@ -39,7 +39,7 @@ public class LogStreamController {
throw new RuntimeException("Failed to create SSE connection: " + e.getMessage());
}
}
/**
* 关闭SSE连接
*/
@@ -48,7 +48,7 @@ public class LogStreamController {
logger.info("🔚 收到关闭SSE连接请求: taskId={}", taskId);
logStreamService.closeConnection(taskId);
}
/**
* 获取连接状态
*/
@@ -56,32 +56,32 @@ public class LogStreamController {
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

@@ -9,13 +9,13 @@ import reactor.core.publisher.Mono;
@RequestMapping("/api/task")
@CrossOrigin(origins = "*")
public class TaskStatusController {
private final ContinuousConversationService conversationService;
public TaskStatusController(ContinuousConversationService conversationService) {
this.conversationService = conversationService;
}
/**
* 获取任务状态
*/
@@ -65,7 +65,7 @@ public class TaskStatusController {
return dto;
});
}
// DTO类
public static class TaskStatusDto {
private String taskId;
@@ -77,34 +77,79 @@ public class TaskStatusController {
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; }
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类
@@ -118,25 +163,60 @@ public class TaskStatusController {
private long totalDurationMs;
// Getters and Setters
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getTaskId() {
return taskId;
}
public String getFullResponse() { return fullResponse; }
public void setFullResponse(String fullResponse) { this.fullResponse = fullResponse; }
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public java.util.List<String> getTurnResponses() { return turnResponses; }
public void setTurnResponses(java.util.List<String> turnResponses) { this.turnResponses = turnResponses; }
public String getFullResponse() {
return fullResponse;
}
public int getTotalTurns() { return totalTurns; }
public void setTotalTurns(int totalTurns) { this.totalTurns = totalTurns; }
public void setFullResponse(String fullResponse) {
this.fullResponse = fullResponse;
}
public boolean isReachedMaxTurns() { return reachedMaxTurns; }
public void setReachedMaxTurns(boolean reachedMaxTurns) { this.reachedMaxTurns = reachedMaxTurns; }
public java.util.List<String> getTurnResponses() {
return turnResponses;
}
public String getStopReason() { return stopReason; }
public void setStopReason(String stopReason) { this.stopReason = stopReason; }
public void setTurnResponses(java.util.List<String> turnResponses) {
this.turnResponses = turnResponses;
}
public long getTotalDurationMs() { return totalDurationMs; }
public void setTotalDurationMs(long totalDurationMs) { this.totalDurationMs = totalDurationMs; }
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

@@ -6,35 +6,35 @@ 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{" +

View File

@@ -19,58 +19,19 @@ public class ProjectStructure {
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
*/
@@ -78,14 +39,14 @@ public class ProjectStructure {
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
*/
@@ -94,7 +55,7 @@ public class ProjectStructure {
this.keyFiles.add(fileName);
}
}
/**
* Get project structure summary
*/
@@ -104,46 +65,46 @@ public class ProjectStructure {
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(") "));
.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();
.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")) {
dirName.equals("test") || dirName.equals("tests") ||
dirName.equals("config") || dirName.equals("conf") ||
dirName.equals("docs") || dirName.equals("doc")) {
dir.setImportant(true);
continue;
}
@@ -154,60 +115,164 @@ public class ProjectStructure {
case JAVA_GRADLE:
case SPRING_BOOT:
if (dirName.equals("main") || dirName.equals("resources") ||
dirName.equals("webapp") || dirName.equals("target") ||
dirName.equals("build")) {
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")) {
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")) {
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; }
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;
}
/**
* 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++;
}
}
}

View File

@@ -34,59 +34,59 @@ public enum ProjectType {
// 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;
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;
return isJavaScriptProject() || this == HTML_STATIC ||
this == DJANGO || this == FLASK || this == FASTAPI || this == SPRING_BOOT;
}
/**
* Get the primary programming language of the project
*/
@@ -94,17 +94,23 @@ public enum ProjectType {
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";
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
*/

View File

@@ -1,7 +1,7 @@
package com.example.demo.model;
import java.util.List;
import java.util.ArrayList;
import java.util.List;
public class TaskStatus {
private String taskId;
@@ -15,7 +15,7 @@ public class TaskStatus {
private List<String> actionHistory;
private String errorMessage;
private double progressPercentage;
public TaskStatus(String taskId) {
this.taskId = taskId;
this.status = "RUNNING";
@@ -24,57 +24,93 @@ public class TaskStatus {
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) {
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) {
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) {
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) {
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; }
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

@@ -3,7 +3,10 @@ package com.example.demo.schema;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* JSON Schema definition class
@@ -11,25 +14,26 @@ import java.util.*;
*/
@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() {}
public JsonSchema() {
}
// Static factory methods
public static JsonSchema object() {
@@ -117,30 +121,75 @@ public class JsonSchema {
}
// Getters and Setters
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getType() {
return type;
}
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public void setType(String type) {
this.type = type;
}
public String getPattern() { return pattern; }
public void setPattern(String pattern) { this.pattern = pattern; }
public String getDescription() {
return description;
}
public Number getMinimum() { return minimum; }
public void setMinimum(Number minimum) { this.minimum = minimum; }
public void setDescription(String description) {
this.description = description;
}
public Number getMaximum() { return maximum; }
public void setMaximum(Number maximum) { this.maximum = maximum; }
public String getPattern() {
return pattern;
}
public List<Object> getEnumValues() { return enumValues; }
public void setEnumValues(List<Object> enumValues) { this.enumValues = enumValues; }
public void setPattern(String pattern) {
this.pattern = pattern;
}
public Map<String, JsonSchema> getProperties() { return properties; }
public void setProperties(Map<String, JsonSchema> properties) { this.properties = properties; }
public Number getMinimum() {
return minimum;
}
public List<String> getRequiredFields() { return requiredFields; }
public void setRequiredFields(List<String> requiredFields) { this.requiredFields = requiredFields; }
public void setMinimum(Number minimum) {
this.minimum = minimum;
}
public JsonSchema getItems() { return items; }
public void setItems(JsonSchema items) { this.items = items; }
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

@@ -18,9 +18,9 @@ import java.util.stream.Collectors;
*/
@Component
public class SchemaValidator {
private static final Logger logger = LoggerFactory.getLogger(SchemaValidator.class);
private final ObjectMapper objectMapper;
private final JsonSchemaFactory schemaFactory;
@@ -33,7 +33,7 @@ public class SchemaValidator {
* Validate data against schema
*
* @param schema JSON Schema definition
* @param data Data to validate
* @param data Data to validate
* @return Validation error message, null means validation passed
*/
public String validate(JsonSchema schema, Object data) {
@@ -58,12 +58,12 @@ public class SchemaValidator {
return null; // Validation passed
} else {
String errorMessage = errors.stream()
.map(ValidationMessage::getMessage)
.collect(Collectors.joining("; "));
.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);
@@ -94,10 +94,10 @@ public class SchemaValidator {
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;
@@ -123,12 +123,12 @@ public class SchemaValidator {
if (expectedType.equals(actualType)) {
return true;
}
// Number type compatibility
if ("number".equals(expectedType) && "integer".equals(actualType)) {
return true;
}
return false;
}
}

View File

@@ -26,47 +26,40 @@ import java.util.concurrent.ConcurrentHashMap;
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."
"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 ChatClient chatClient;
private final NextSpeakerService nextSpeakerService;
// 添加依赖注入
private final TaskSummaryService taskSummaryService;
// 在现有的ContinuousConversationService中添加以下改进
private final Map<String, TaskStatus> taskStatusMap = new ConcurrentHashMap<>();
private final Map<String, ConversationResult> conversationResults = new ConcurrentHashMap<>();
@Autowired
private LogStreamService logStreamService;
// 修改构造函数
public ContinuousConversationService(ChatClient chatClient,
NextSpeakerService nextSpeakerService,
TaskSummaryService taskSummaryService) {
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);
@@ -81,7 +74,7 @@ public class ContinuousConversationService {
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);
@@ -108,37 +101,37 @@ public class ContinuousConversationService {
// 明确的简单对话模式 - 不需要工具
String[] simplePatterns = {
"你好", "hello", "hi", "", "哈喽",
"谢谢", "thank you", "thanks", "感谢",
"再见", "goodbye", "bye", "拜拜",
"好的", "ok", "okay", "", "可以",
"不用了", "算了", "没事", "不需要",
"怎么样", "如何", "什么意思", "是什么",
"介绍一下", "解释一下", "说明一下"
"你好", "hello", "hi", "", "哈喽",
"谢谢", "thank you", "thanks", "感谢",
"再见", "goodbye", "bye", "拜拜",
"好的", "ok", "okay", "", "可以",
"不用了", "算了", "没事", "不需要",
"怎么样", "如何", "什么意思", "是什么",
"介绍一下", "解释一下", "说明一下"
};
// 检查是否是简单问候或确认
for (String pattern : simplePatterns) {
if (lowerMessage.equals(pattern) ||
(lowerMessage.length() <= 10 && lowerMessage.contains(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"
"创建", "create", "新建", "生成", "建立",
"编辑", "edit", "修改", "更新", "改变",
"删除", "delete", "移除", "清除",
"文件", "file", "目录", "folder", "项目", "project",
"代码", "code", "程序", "script", "函数", "function",
"分析", "analyze", "检查", "查看", "读取", "read",
"写入", "write", "保存", "save",
"搜索", "search", "查找", "find",
"下载", "download", "获取", "fetch",
"安装", "install", "配置", "config",
"运行", "run", "执行", "execute",
"测试", "test", "调试", "debug"
};
// 检查是否包含工具相关关键词
@@ -156,9 +149,9 @@ public class ContinuousConversationService {
// 包含路径、URL、代码片段等的消息
if (lowerMessage.contains("/") || lowerMessage.contains("\\") ||
lowerMessage.contains("http") || lowerMessage.contains("www") ||
lowerMessage.contains("{") || lowerMessage.contains("}") ||
lowerMessage.contains("<") || lowerMessage.contains(">")) {
lowerMessage.contains("http") || lowerMessage.contains("www") ||
lowerMessage.contains("{") || lowerMessage.contains("}") ||
lowerMessage.contains("<") || lowerMessage.contains(">")) {
return true;
}
@@ -166,7 +159,7 @@ public class ContinuousConversationService {
// 这样可以避免不必要的工具准备状态显示
return false;
}
// 修改executeContinuousConversation方法
public ConversationResult executeContinuousConversation(String taskId, String initialMessage, List<Message> conversationHistory) {
TaskStatus taskStatus = taskStatusMap.get(taskId);
@@ -239,7 +232,7 @@ public class ContinuousConversationService {
// 更新任务状态 - 显示当前响应的简短摘要
String responseSummary = responseText.length() > 100 ?
responseText.substring(0, 100) + "..." : responseText;
responseText.substring(0, 100) + "..." : responseText;
taskStatus.setCurrentAction(String.format("第 %d 轮完成: %s", turnCount, responseSummary));
}
@@ -280,24 +273,24 @@ public class ContinuousConversationService {
long totalDuration = System.currentTimeMillis() - conversationStartTime;
logger.info("Continuous conversation completed after {} turns in {}ms. Stop reason: {}",
turnCount, totalDuration, stopReason);
turnCount, totalDuration, stopReason);
// 创建结果对象
ConversationResult result = new ConversationResult(
fullResponse.toString(),
turnResponses,
workingHistory,
turnCount,
turnCount >= MAX_TURNS,
stopReason,
totalDuration
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);
turnCount, totalDuration / 1000.0);
if (stopReason != null) {
summary += ",停止原因: " + stopReason;
}
@@ -334,9 +327,9 @@ public class ContinuousConversationService {
// 调用AI这里可以添加超时控制但Spring AI目前不直接支持
ChatResponse response = chatClient.prompt()
.messages(conversationHistory)
.call()
.chatResponse();
.messages(conversationHistory)
.call()
.chatResponse();
// 处理响应
Generation generation = response.getResult();
@@ -345,7 +338,7 @@ public class ContinuousConversationService {
long turnDuration = System.currentTimeMillis() - turnStartTime;
logger.debug("Turn {} completed in {}ms, response length: {} characters",
turnNumber, turnDuration, responseText != null ? responseText.length() : 0);
turnNumber, turnDuration, responseText != null ? responseText.length() : 0);
return new TurnResult(true, responseText, null);
@@ -393,7 +386,7 @@ public class ContinuousConversationService {
// 只有在不确定的情况下才使用智能判断服务包含LLM调用
try {
NextSpeakerService.NextSpeakerResponse nextSpeaker =
nextSpeakerService.checkNextSpeaker(conversationHistory);
nextSpeakerService.checkNextSpeaker(conversationHistory);
long duration = System.currentTimeMillis() - startTime;
logger.debug("Next speaker check completed in {}ms, result: {}", duration, nextSpeaker);
@@ -417,25 +410,25 @@ public class ContinuousConversationService {
// 如果是前几轮且包含工具调用成功的标志,很可能需要继续
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"
"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"
"all files created", "project complete", "setup complete",
"everything is ready", "task completed", "all done"
};
boolean hasCompletionSignal = false;
@@ -478,9 +471,17 @@ public class ContinuousConversationService {
this.errorMessage = errorMessage;
}
public boolean isSuccess() { return success; }
public String getResponse() { return response; }
public String getErrorMessage() { return errorMessage; }
public boolean isSuccess() {
return success;
}
public String getResponse() {
return response;
}
public String getErrorMessage() {
return errorMessage;
}
}
/**
@@ -496,8 +497,8 @@ public class ContinuousConversationService {
private final long totalDurationMs;
public ConversationResult(String fullResponse, List<String> turnResponses,
List<Message> finalHistory, int totalTurns, boolean reachedMaxTurns,
String stopReason, long totalDurationMs) {
List<Message> finalHistory, int totalTurns, boolean reachedMaxTurns,
String stopReason, long totalDurationMs) {
this.fullResponse = fullResponse;
this.turnResponses = turnResponses;
this.finalHistory = finalHistory;
@@ -507,12 +508,32 @@ public class ContinuousConversationService {
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; }
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

@@ -7,22 +7,23 @@ 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() {
}
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();
@@ -30,43 +31,43 @@ public class LogEvent {
event.setTaskId(taskId);
event.setMessage("SSE连接已建立");
event.setTimestamp(java.time.LocalDateTime.now().format(
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
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{" +

View File

@@ -18,49 +18,49 @@ import java.util.concurrent.ConcurrentHashMap;
*/
@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连接
*/
@@ -75,7 +75,7 @@ public class LogStreamService {
}
}
}
/**
* 推送工具开始执行事件
*/
@@ -89,10 +89,10 @@ public class LogStreamService {
event.setTimestamp(LocalDateTime.now().format(formatter));
event.setIcon(getToolIcon(toolName));
event.setStatus("RUNNING");
sendLogEvent(taskId, event);
}
/**
* 推送工具执行成功事件
*/
@@ -107,10 +107,10 @@ public class LogStreamService {
event.setIcon(getToolIcon(toolName));
event.setStatus("SUCCESS");
event.setExecutionTime(executionTime);
sendLogEvent(taskId, event);
}
/**
* 推送工具执行失败事件
*/
@@ -125,10 +125,10 @@ public class LogStreamService {
event.setIcon("");
event.setStatus("ERROR");
event.setExecutionTime(executionTime);
sendLogEvent(taskId, event);
}
/**
* 推送任务完成事件
*/
@@ -138,9 +138,9 @@ public class LogStreamService {
event.setTaskId(taskId);
event.setMessage("任务执行完成");
event.setTimestamp(LocalDateTime.now().format(formatter));
sendLogEvent(taskId, event);
// 延迟关闭连接
new Thread(() -> {
try {
@@ -151,7 +151,7 @@ public class LogStreamService {
}
}).start();
}
/**
* 发送日志事件到前端
*/
@@ -161,11 +161,11 @@ public class LogStreamService {
try {
String jsonData = objectMapper.writeValueAsString(event);
logger.info("📤 准备推送日志事件: taskId={}, type={}, data={}", taskId,
event instanceof LogEvent ? ((LogEvent) event).getType() : "unknown", jsonData);
event instanceof LogEvent ? ((LogEvent) event).getType() : "unknown", jsonData);
emitter.send(SseEmitter.event()
.name("log")
.data(jsonData));
.name("log")
.data(jsonData));
logger.info("✅ 日志事件推送成功: taskId={}", taskId);
} catch (IOException e) {
@@ -176,23 +176,31 @@ public class LogStreamService {
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 "⚙️";
case "readFile":
return "📖";
case "writeFile":
return "✏️";
case "editFile":
return "📝";
case "listDirectory":
return "📁";
case "analyzeProject":
return "🔍";
case "scaffoldProject":
return "🏗️";
case "smartEdit":
return "🧠";
default:
return "⚙️";
}
}
/**
* 获取活跃连接数
*/

View File

@@ -22,7 +22,32 @@ import java.util.List;
public class NextSpeakerService {
private static final Logger logger = LoggerFactory.getLogger(NextSpeakerService.class);
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"}
""";
private final ChatModel chatModel;
private final ObjectMapper objectMapper;
@@ -31,34 +56,6 @@ public class NextSpeakerService {
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"}
""";
/**
* 判断下一步应该由谁发言 - 优化版本,添加快速路径
*/
@@ -115,15 +112,15 @@ public class NextSpeakerService {
// 明确的停止信号 - 直接返回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"
"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) {
@@ -134,17 +131,17 @@ public class NextSpeakerService {
// 明确的继续信号 - 直接返回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"
"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) {
@@ -205,8 +202,8 @@ public class NextSpeakerService {
*/
private boolean containsCompletionSignal(String lowerContent) {
String[] completionSignals = {
"all done", "complete", "finished", "ready", "that's it",
"we're done", "task complete", "project complete"
"all done", "complete", "finished", "ready", "that's it",
"we're done", "task complete", "project complete"
};
for (String signal : completionSignals) {
@@ -222,15 +219,15 @@ public class NextSpeakerService {
*/
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"
"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) {
@@ -250,39 +247,39 @@ public class NextSpeakerService {
// 创建用于判断的对话历史 - 简化版本
List<Message> checkMessages = recentHistory.stream()
.map(msg -> {
if (msg instanceof UserMessage) {
// 截断过长的用户消息
String text = msg.getText();
if (text.length() > 500) {
text = text.substring(0, 500) + "...";
.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 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());
return msg;
})
.collect(java.util.stream.Collectors.toList());
// 添加简化的检查提示
checkMessages.add(new UserMessage(SIMPLIFIED_CHECK_PROMPT));
// 使用输出转换器
BeanOutputConverter<NextSpeakerResponse> outputConverter =
new BeanOutputConverter<>(NextSpeakerResponse.class);
new BeanOutputConverter<>(NextSpeakerResponse.class);
// 调用LLM - 这里可以考虑添加超时但Spring AI目前不直接支持
ChatResponse response = ChatClient.create(chatModel)
.prompt()
.messages(checkMessages)
.call()
.chatResponse();
.prompt()
.messages(checkMessages)
.call()
.chatResponse();
long duration = System.currentTimeMillis() - startTime;
logger.debug("LLM check completed in {}ms", duration);
@@ -318,8 +315,8 @@ public class NextSpeakerService {
private NextSpeakerResponse parseManually(String responseText) {
try {
// 简单的手动解析
if (responseText.toLowerCase().contains("\"next_speaker\"") &&
responseText.toLowerCase().contains("\"model\"")) {
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");
@@ -340,13 +337,13 @@ public class NextSpeakerService {
// 优先检查明确的停止指示词 - 扩展版本
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"
"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"
};
// 检查停止指示词
@@ -365,14 +362,14 @@ public class NextSpeakerService {
// 扩展的继续指示词
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"
"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"
};
// 检查继续指示词
@@ -406,25 +403,25 @@ public class NextSpeakerService {
// 工具调用成功的典型模式
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"
"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) {
@@ -441,16 +438,16 @@ public class NextSpeakerService {
*/
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"
"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) {
@@ -468,11 +465,12 @@ public class NextSpeakerService {
public static class NextSpeakerResponse {
@JsonProperty("next_speaker")
private String nextSpeaker;
@JsonProperty("reasoning")
private String reasoning;
public NextSpeakerResponse() {}
public NextSpeakerResponse() {
}
public NextSpeakerResponse(String nextSpeaker, String reasoning) {
this.nextSpeaker = nextSpeaker;

View File

@@ -22,98 +22,99 @@ import java.util.stream.Stream;
*/
@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());
logger.debug("Analyzed project structure with {} directories",
structure.getDirectories().size());
// 3. 分析依赖关系
List<ProjectContext.DependencyInfo> dependencies =
projectDiscoveryService.analyzeDependencies(projectRoot);
List<ProjectContext.DependencyInfo> dependencies =
projectDiscoveryService.analyzeDependencies(projectRoot);
context.setDependencies(dependencies);
logger.debug("Found {} dependencies", dependencies.size());
// 4. 查找配置文件
List<ProjectContext.ConfigFile> configFiles =
projectDiscoveryService.findConfigurationFiles(projectRoot);
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) {
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 {
@@ -134,26 +135,26 @@ public class ProjectContextAnalyzer {
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()) {
@@ -164,17 +165,17 @@ public class ProjectContextAnalyzer {
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);
@@ -183,12 +184,12 @@ public class ProjectContextAnalyzer {
} else if (extension.equals(".py")) {
analyzePythonFile(lines, stats);
}
} catch (IOException e) {
logger.warn("Error reading file for code analysis: " + filePath, e);
}
}
/**
* 分析Java文件
*/
@@ -203,21 +204,21 @@ public class ProjectContextAnalyzer {
}
}
}
/**
* 分析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*=>.*")) {
if (trimmedLine.matches(".*\\bfunction\\s+\\w+.*") ||
trimmedLine.matches(".*\\w+\\s*:\\s*function.*") ||
trimmedLine.matches(".*\\w+\\s*=\\s*\\(.*\\)\\s*=>.*")) {
stats.setTotalFunctions(stats.getTotalFunctions() + 1);
}
}
}
/**
* 分析Python文件
*/
@@ -232,76 +233,76 @@ public class ProjectContextAnalyzer {
}
}
}
/**
* 收集项目元数据
*/
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(".");
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");
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":
@@ -319,21 +320,35 @@ public class ProjectContextAnalyzer {
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";
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

@@ -23,84 +23,83 @@ import java.util.stream.Stream;
*/
@Service
public class ProjectDiscoveryService {
private static final Logger logger = LoggerFactory.getLogger(ProjectDiscoveryService.class);
private final ObjectMapper objectMapper = new ObjectMapper();
@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 {
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);
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));
@@ -114,31 +113,31 @@ public class ProjectDiscoveryService {
});
}
}
/**
* 分析目录中的文件
*/
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);
});
.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:
@@ -164,95 +163,95 @@ public class ProjectDiscoveryService {
} 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"));
"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"));
"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"));
"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"));
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");
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);
@@ -269,26 +268,26 @@ public class ProjectDiscoveryService {
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:
@@ -311,47 +310,47 @@ public class ProjectDiscoveryService {
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) {
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);
ProjectContext.ConfigFile configFile =
new ProjectContext.ConfigFile(fileName, relativePath, fileType);
// 标记主要配置文件
if (fileName.equals("pom.xml") || fileName.equals("package.json") ||
fileName.startsWith("application.")) {
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(".");
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(".");
}
/**
* 获取文件扩展名
*/
@@ -359,17 +358,17 @@ public class ProjectDiscoveryService {
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")) {
if (fileName.equals("README.md") || fileName.equals("LICENSE") ||
fileName.equals("Dockerfile") || fileName.equals(".gitignore")) {
return true;
}
// 项目类型特定的关键文件
if (projectType != null) {
String keyFile = projectType.getKeyFile();
@@ -377,7 +376,7 @@ public class ProjectDiscoveryService {
return fileName.equals(keyFile) || fileName.matches(keyFile);
}
}
return false;
}
}

View File

@@ -19,12 +19,13 @@ 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 检测到的项目类型
*/
@@ -33,33 +34,33 @@ public class ProjectTypeDetector {
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;
}
}
/**
* 基于关键文件检测项目类型
*/
@@ -72,56 +73,56 @@ public class ProjectTypeDetector {
}
return ProjectType.JAVA_MAVEN;
}
// Java Gradle项目
if (Files.exists(projectRoot.resolve("build.gradle")) ||
Files.exists(projectRoot.resolve("build.gradle.kts"))) {
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"))) {
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"))) {
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项目
*/
@@ -131,16 +132,16 @@ public class ProjectTypeDetector {
if (!Files.exists(pomFile)) {
return false;
}
String pomContent = Files.readString(pomFile);
return pomContent.contains("spring-boot-starter") ||
pomContent.contains("org.springframework.boot");
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项目类型
*/
@@ -149,36 +150,36 @@ public class ProjectTypeDetector {
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")) {
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项目类型
*/
@@ -187,13 +188,13 @@ public class ProjectTypeDetector {
if (Files.exists(projectRoot.resolve("manage.py"))) {
return ProjectType.DJANGO;
}
// 检查Flask项目
if (Files.exists(projectRoot.resolve("app.py")) ||
Files.exists(projectRoot.resolve("application.py"))) {
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 {
@@ -205,54 +206,54 @@ public class ProjectTypeDetector {
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();
.filter(Files::isDirectory)
.map(path -> path.getFileName().toString().toLowerCase())
.toList();
// Java项目特征目录
if (directories.contains("src") &&
(directories.contains("target") || directories.contains("build"))) {
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")) {
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__")) {
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);
}
/**
* 获取项目类型的详细信息
*/
@@ -261,7 +262,7 @@ public class ProjectTypeDetector {
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:
@@ -278,7 +279,7 @@ public class ProjectTypeDetector {
break;
// 可以添加更多项目类型的详细信息
}
return details.toString();
}
}

View File

@@ -1,32 +1,33 @@
package com.example.demo.service;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@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)
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"
"创建", "写入", "读取", "编辑", "列出", "分析", "生成", "构建",
"creating", "writing", "reading", "editing", "listing", "analyzing", "generating", "building"
};
/**
* 从AI响应中提取任务摘要
*/
@@ -34,10 +35,10 @@ public class TaskSummaryService {
if (aiResponse == null || aiResponse.trim().isEmpty()) {
return "处理中...";
}
// 清理响应文本
String cleanResponse = aiResponse.replaceAll("```[\\s\\S]*?```", "").trim();
// 尝试匹配具体操作
for (Pattern pattern : ACTION_PATTERNS) {
Matcher matcher = pattern.matcher(cleanResponse);
@@ -49,7 +50,7 @@ public class TaskSummaryService {
return action;
}
}
// 查找动作词汇
String lowerResponse = cleanResponse.toLowerCase();
for (String verb : ACTION_VERBS) {
@@ -67,24 +68,24 @@ public class TaskSummaryService {
}
}
}
// 如果没有找到具体操作,返回通用描述
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;
@@ -92,24 +93,24 @@ public class TaskSummaryService {
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();
.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()) {
@@ -117,27 +118,27 @@ public class TaskSummaryService {
} 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

@@ -16,45 +16,45 @@ 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) {
@@ -62,18 +62,18 @@ public class ToolExecutionLogger {
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) {
@@ -81,48 +81,48 @@ public class ToolExecutionLogger {
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("🔧 工具: {} | 调用次数: {} | 成功: {} | 失败: {} | 平均耗时: {}ms",
toolName, stats.getTotalCalls(), stats.getSuccessCount(),
stats.getErrorCount(), stats.getAverageExecutionTime());
});
logger.info("📈 ================================");
}
/**
* 格式化参数显示
*/
@@ -133,7 +133,7 @@ public class ToolExecutionLogger {
String paramStr = parameters.toString();
return paramStr.length() > 200 ? paramStr.substring(0, 200) + "..." : paramStr;
}
/**
* 截断结果显示
*/
@@ -143,7 +143,7 @@ public class ToolExecutionLogger {
}
return result.length() > 300 ? result.substring(0, 300) + "..." : result;
}
/**
* 工具统计信息内部类
*/
@@ -152,17 +152,37 @@ public class ToolExecutionLogger {
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;
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

@@ -8,77 +8,77 @@ import com.fasterxml.jackson.annotation.JsonInclude;
*/
@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) {
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{" +

View File

@@ -4,7 +4,6 @@ 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;
@@ -13,16 +12,16 @@ import java.util.concurrent.CompletableFuture;
* 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) {
@@ -30,7 +29,7 @@ public abstract class BaseTool<P> {
}
public BaseTool(String name, String displayName, String description, JsonSchema parameterSchema,
boolean isOutputMarkdown, boolean canUpdateOutput) {
boolean isOutputMarkdown, boolean canUpdateOutput) {
this.name = name;
this.displayName = displayName;
this.description = description;
@@ -57,7 +56,7 @@ public abstract class BaseTool<P> {
logger.warn("Schema validator or parameter schema is null, skipping validation");
return null;
}
try {
return schemaValidator.validate(parameterSchema, params);
} catch (Exception e) {
@@ -95,12 +94,29 @@ public abstract class BaseTool<P> {
}
// 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; }
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() {

View File

@@ -37,13 +37,13 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
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()
"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();
@@ -59,25 +59,25 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
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");
.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
@@ -101,7 +101,7 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
}
Path filePath = Paths.get(params.filePath);
// Validate if it's an absolute path
if (!filePath.isAbsolute()) {
return "File path must be absolute: " + params.filePath;
@@ -126,31 +126,31 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
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) {
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;
@@ -164,9 +164,9 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
@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));
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 {
@@ -188,7 +188,7 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
}
String editDetails = startLine != null && endLine != null ?
String.format("行号范围编辑: %d-%d行", startLine, endLine) : "字符串替换编辑";
String.format("行号范围编辑: %d-%d行", startLine, endLine) : "字符串替换编辑";
executionLogger.logFileOperation(callId, "编辑文件", filePath, editDetails);
// Execute the tool
@@ -217,7 +217,7 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
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);
@@ -230,26 +230,26 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
// 读取原始内容
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) {
@@ -276,13 +276,13 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
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());
}
@@ -302,7 +302,7 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
}
targetContent.append(lines[i]);
}
// 检查是否匹配
if (!targetContent.toString().equals(params.oldStr)) {
return null; // 指定行范围的内容与old_str不匹配
@@ -316,17 +316,17 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
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();
}
@@ -334,16 +334,16 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
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
fileName + " (Original)",
fileName + " (Edited)",
oldLines,
patch,
3 // context lines
);
return String.join("\n", unifiedDiff);
} catch (Exception e) {
logger.warn("Could not generate diff", e);
@@ -355,7 +355,7 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
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);
}
@@ -390,21 +390,22 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
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() {
}
public EditFileParams(String filePath, String oldStr, String newStr) {
this.filePath = filePath;
@@ -413,28 +414,53 @@ public class EditFileTool extends BaseTool<EditFileTool.EditFileParams> {
}
// Getters and Setters
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public String getFilePath() {
return filePath;
}
public String getOldStr() { return oldStr; }
public void setOldStr(String oldStr) { this.oldStr = oldStr; }
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getNewStr() { return newStr; }
public void setNewStr(String newStr) { this.newStr = newStr; }
public String getOldStr() {
return oldStr;
}
public Integer getStartLine() { return startLine; }
public void setStartLine(Integer startLine) { this.startLine = startLine; }
public void setOldStr(String oldStr) {
this.oldStr = oldStr;
}
public Integer getEndLine() { return endLine; }
public void setEndLine(Integer endLine) { this.endLine = endLine; }
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);
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

@@ -2,7 +2,6 @@ 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;
@@ -26,7 +25,7 @@ import java.util.stream.Stream;
public class FileOperationTools {
private static final Logger logger = LoggerFactory.getLogger(FileOperationTools.class);
private final String rootDirectory;
private final AppProperties appProperties;
@@ -71,14 +70,14 @@ public class FileOperationTools {
long fileSize = Files.size(filePath);
if (fileSize > appProperties.getWorkspace().getMaxFileSize()) {
return "Error: File too large: " + fileSize + " bytes. Maximum allowed: " +
appProperties.getWorkspace().getMaxFileSize() + " bytes";
appProperties.getWorkspace().getMaxFileSize() + " bytes";
}
// 检查文件扩展名
String fileName = filePath.getFileName().toString();
if (!isAllowedFileType(fileName)) {
return "Error: File type not allowed: " + fileName +
". Allowed extensions: " + appProperties.getWorkspace().getAllowedExtensions();
". Allowed extensions: " + appProperties.getWorkspace().getAllowedExtensions();
}
// 读取文件
@@ -92,12 +91,12 @@ public class FileOperationTools {
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());
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());
absolutePath, duration, e.getMessage());
} finally {
long duration = System.currentTimeMillis() - startTime;
logger.debug("Completed readFile operation for: {} (duration: {}ms)", absolutePath, duration);
@@ -123,42 +122,42 @@ public class FileOperationTools {
// 验证内容大小
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";
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);
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);
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);
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());
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());
filePath, duration, e.getMessage());
} finally {
long duration = System.currentTimeMillis() - startTime;
logger.debug("Completed writeFile operation for: {} (duration: {}ms)", filePath, duration);
@@ -167,13 +166,13 @@ public class FileOperationTools {
@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.")
@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")
@ToolParam(description = "The text to find and replace in the file")
String oldText,
@ToolParam(description = "The new text to replace the old text with")
@ToolParam(description = "The new text to replace the old text with")
String newText) {
try {
// 验证路径
String validationError = validatePath(filePath);
@@ -182,7 +181,7 @@ public class FileOperationTools {
}
Path path = Paths.get(filePath);
// 检查文件是否存在
if (!Files.exists(path)) {
return "Error: File not found: " + filePath;
@@ -195,21 +194,21 @@ public class FileOperationTools {
// 读取原始内容
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);
absolutePath, relativePath);
} catch (IOException e) {
logger.error("Error editing file: " + filePath, e);
@@ -222,11 +221,11 @@ public class FileOperationTools {
@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.")
@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)
@ToolParam(description = "Whether to list contents recursively", required = false)
Boolean recursive) {
try {
// 验证路径
String validationError = validatePath(directoryPath);
@@ -235,7 +234,7 @@ public class FileOperationTools {
}
Path path = Paths.get(directoryPath);
// 检查目录是否存在
if (!Files.exists(path)) {
return "Error: Directory not found: " + directoryPath;
@@ -272,7 +271,7 @@ public class FileOperationTools {
}
Path filePath = Paths.get(path);
// 验证是否为绝对路径
if (!filePath.isAbsolute()) {
return "Path must be absolute: " + path;
@@ -306,7 +305,7 @@ public class FileOperationTools {
private boolean isAllowedFileType(String fileName) {
List<String> allowedExtensions = appProperties.getWorkspace().getAllowedExtensions();
return allowedExtensions.stream()
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
}
private String getRelativePath(Path path) {
@@ -325,7 +324,7 @@ public class FileOperationTools {
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);
absolutePath, relativePath, lineCount, content.getBytes(StandardCharsets.UTF_8).length, content);
}
private String readFileWithPagination(Path filePath, int offset, int limit) throws IOException {
@@ -342,7 +341,7 @@ public class FileOperationTools {
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);
absolutePath, relativePath, offset + 1, endIndex, allLines.size(), content);
}
private String listDirectorySimple(Path path, String absolutePath, String relativePath) throws IOException {
@@ -379,30 +378,30 @@ public class FileOperationTools {
try (Stream<Path> entries = Files.walk(path)) {
entries.sorted()
.forEach(entry -> {
if (!entry.equals(path)) {
String entryAbsolutePath = entry.toAbsolutePath().toString();
String entryRelativePath = getRelativePath(entry);
.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);
// 计算缩进级别
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");
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

@@ -31,13 +31,13 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
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()
"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();
@@ -53,22 +53,22 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
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");
.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
@@ -84,7 +84,7 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
}
Path dirPath = Paths.get(params.path);
// 验证是否为绝对路径
if (!dirPath.isAbsolute()) {
return "Directory path must be absolute: " + params.path;
@@ -108,7 +108,7 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
return CompletableFuture.supplyAsync(() -> {
try {
Path dirPath = Paths.get(params.path);
// 检查目录是否存在
if (!Files.exists(dirPath)) {
return ToolResult.error("Directory not found: " + params.path);
@@ -121,13 +121,13 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
// 列出文件和目录
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());
String displayMessage = String.format("Listed directory: %s (%d items)",
relativePath, fileInfos.size());
return ToolResult.success(content, displayMessage);
} catch (IOException e) {
@@ -142,19 +142,19 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
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));
.comparing((FileInfo f) -> !f.isDirectory())
.thenComparing(FileInfo::getName));
return fileInfos;
}
@@ -163,12 +163,12 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
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) {
@@ -182,22 +182,22 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
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);
@@ -213,14 +213,14 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
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()
Files.getLastModifiedTime(path).toInstant(),
ZoneId.systemDefault()
);
String relativePath = basePath.relativize(path).toString();
return new FileInfo(name, relativePath, isDirectory, size, lastModified);
}
@@ -228,32 +228,32 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
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(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()
type,
truncate(fileInfo.getName(), 40),
sizeStr,
modifiedStr,
fileInfo.getRelativePath()
));
}
return sb.toString();
}
@@ -310,11 +310,25 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
}
// 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 String getName() {
return name;
}
public String getRelativePath() {
return relativePath;
}
public boolean isDirectory() {
return isDirectory;
}
public long getSize() {
return size;
}
public LocalDateTime getLastModified() {
return lastModified;
}
}
/**
@@ -323,37 +337,58 @@ public class ListDirectoryTool extends BaseTool<ListDirectoryTool.ListDirectoryP
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() {
}
public ListDirectoryParams(String path) {
this.path = path;
}
// Getters and Setters
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
public String getPath() {
return path;
}
public Boolean getRecursive() { return recursive; }
public void setRecursive(Boolean recursive) { this.recursive = recursive; }
public void setPath(String path) {
this.path = path;
}
public Integer getMaxDepth() { return maxDepth; }
public void setMaxDepth(Integer maxDepth) { this.maxDepth = maxDepth; }
public Boolean getRecursive() {
return recursive;
}
public Boolean getShowHidden() { return showHidden; }
public void setShowHidden(Boolean showHidden) { this.showHidden = showHidden; }
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);
return String.format("ListDirectoryParams{path='%s', recursive=%s, maxDepth=%d}",
path, recursive, maxDepth);
}
}
}

View File

@@ -32,11 +32,11 @@ public class ProjectScaffoldTool extends BaseTool<ProjectScaffoldTool.ScaffoldPa
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()
"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();
@@ -44,32 +44,32 @@ public class ProjectScaffoldTool extends BaseTool<ProjectScaffoldTool.ScaffoldPa
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");
.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
@@ -113,7 +113,7 @@ public class ProjectScaffoldTool extends BaseTool<ProjectScaffoldTool.ScaffoldPa
/**
* Project scaffold tool method for Spring AI integration
*/
@Tool(name = "scaffold_project", description = "Create a new project with standard structure and template files")
@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();
@@ -258,7 +258,7 @@ public class ProjectScaffoldTool extends BaseTool<ProjectScaffoldTool.ScaffoldPa
// User-provided variables
if (params.templateVariables != null) {
params.templateVariables.forEach((key, value) ->
variables.put(key.toUpperCase(), String.valueOf(value)));
variables.put(key.toUpperCase(), String.valueOf(value)));
}
return variables;
@@ -536,26 +536,61 @@ public class ProjectScaffoldTool extends BaseTool<ProjectScaffoldTool.ScaffoldPa
private Boolean includeGitignore = true;
// Getters and Setters
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getProjectName() {
return projectName;
}
public String getProjectType() { return projectType; }
public void setProjectType(String projectType) { this.projectType = projectType; }
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getProjectPath() { return projectPath; }
public void setProjectPath(String projectPath) { this.projectPath = projectPath; }
public String getProjectType() {
return projectType;
}
public Map<String, Object> getTemplateVariables() { return templateVariables; }
public void setTemplateVariables(Map<String, Object> templateVariables) { this.templateVariables = templateVariables; }
public void setProjectType(String projectType) {
this.projectType = projectType;
}
public Boolean getIncludeGit() { return includeGit; }
public void setIncludeGit(Boolean includeGit) { this.includeGit = includeGit; }
public String getProjectPath() {
return projectPath;
}
public Boolean getIncludeReadme() { return includeReadme; }
public void setIncludeReadme(Boolean includeReadme) { this.includeReadme = includeReadme; }
public void setProjectPath(String projectPath) {
this.projectPath = projectPath;
}
public Boolean getIncludeGitignore() { return includeGitignore; }
public void setIncludeGitignore(Boolean includeGitignore) { this.includeGitignore = includeGitignore; }
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;
}
}
/**
@@ -572,9 +607,9 @@ public class ProjectScaffoldTool extends BaseTool<ProjectScaffoldTool.ScaffoldPa
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());
projectType.getDisplayName(),
projectPath.getFileName(),
createdItems.size());
StringBuilder detailsBuilder = new StringBuilder();
detailsBuilder.append("Created project structure:\n");
@@ -584,7 +619,12 @@ public class ProjectScaffoldTool extends BaseTool<ProjectScaffoldTool.ScaffoldPa
this.details = detailsBuilder.toString();
}
public String getSummary() { return summary; }
public String getDetails() { return details; }
public String getSummary() {
return summary;
}
public String getDetails() {
return details;
}
}
}

View File

@@ -31,12 +31,12 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
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()
"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();
@@ -52,21 +52,21 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
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");
.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
@@ -115,7 +115,7 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
@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));
String.format("文件路径=%s, 偏移量=%s, 限制行数=%s", absolutePath, offset, limit));
long startTime = System.currentTimeMillis();
try {
@@ -135,7 +135,7 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
}
executionLogger.logFileOperation(callId, "读取文件", absolutePath,
offset != null ? String.format("分页读取: 偏移=%d, 限制=%d", offset, limit) : "完整读取");
offset != null ? String.format("分页读取: 偏移=%d, 限制=%d", offset, limit) : "完整读取");
// Execute the tool
ToolResult result = execute(params).join();
@@ -178,14 +178,14 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
long fileSize = Files.size(filePath);
if (fileSize > appProperties.getWorkspace().getMaxFileSize()) {
return ToolResult.error("File too large: " + fileSize + " bytes. Maximum allowed: " +
appProperties.getWorkspace().getMaxFileSize() + " bytes");
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());
". Allowed extensions: " + appProperties.getWorkspace().getAllowedExtensions());
}
// 读取文件
@@ -211,7 +211,7 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
long lineCount = content.lines().count();
String displayMessage = String.format("Read file: %s (%d lines, %d bytes)",
relativePath, lineCount, content.getBytes(StandardCharsets.UTF_8).length);
relativePath, lineCount, content.getBytes(StandardCharsets.UTF_8).length);
return ToolResult.success(content, displayMessage);
}
@@ -229,7 +229,7 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
String relativePath = getRelativePath(filePath);
String displayMessage = String.format("Read file: %s (lines %d-%d of %d total)",
relativePath, offset + 1, endIndex, allLines.size());
relativePath, offset + 1, endIndex, allLines.size());
return ToolResult.success(content, displayMessage);
}
@@ -255,7 +255,7 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
private boolean isAllowedFileType(String fileName) {
List<String> allowedExtensions = appProperties.getWorkspace().getAllowedExtensions();
return allowedExtensions.stream()
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
}
private String getRelativePath(Path filePath) {
@@ -278,7 +278,8 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
private Integer limit;
// 构造器
public ReadFileParams() {}
public ReadFileParams() {
}
public ReadFileParams(String absolutePath) {
this.absolutePath = absolutePath;
@@ -291,19 +292,34 @@ public class ReadFileTool extends BaseTool<ReadFileTool.ReadFileParams> {
}
// Getters and Setters
public String getAbsolutePath() { return absolutePath; }
public void setAbsolutePath(String absolutePath) { this.absolutePath = absolutePath; }
public String getAbsolutePath() {
return absolutePath;
}
public Integer getOffset() { return offset; }
public void setOffset(Integer offset) { this.offset = offset; }
public void setAbsolutePath(String absolutePath) {
this.absolutePath = absolutePath;
}
public Integer getLimit() { return limit; }
public void setLimit(Integer limit) { this.limit = limit; }
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);
absolutePath, offset, limit);
}
}
}

View File

@@ -7,7 +7,7 @@ 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;
@@ -34,10 +34,21 @@ public class ToolResult {
}
// Getters
public boolean isSuccess() { return success; }
public String getLlmContent() { return llmContent; }
public Object getReturnDisplay() { return returnDisplay; }
public String getErrorMessage() { return errorMessage; }
public boolean isSuccess() {
return success;
}
public String getLlmContent() {
return llmContent;
}
public Object getReturnDisplay() {
return returnDisplay;
}
public String getErrorMessage() {
return errorMessage;
}
@Override
public String toString() {
@@ -62,8 +73,13 @@ class FileDiff {
this.fileName = fileName;
}
public String getFileDiff() { return fileDiff; }
public String getFileName() { return fileName; }
public String getFileDiff() {
return fileDiff;
}
public String getFileName() {
return fileName;
}
@Override
public String toString() {
@@ -89,8 +105,8 @@ class ToolConfirmationDetails {
}
public static ToolConfirmationDetails edit(String title, String fileName, String fileDiff) {
return new ToolConfirmationDetails("edit", title, "File edit confirmation",
new FileDiff(fileDiff, fileName));
return new ToolConfirmationDetails("edit", title, "File edit confirmation",
new FileDiff(fileDiff, fileName));
}
public static ToolConfirmationDetails exec(String title, String command) {
@@ -98,8 +114,19 @@ class ToolConfirmationDetails {
}
// Getters
public String getType() { return type; }
public String getTitle() { return title; }
public String getDescription() { return description; }
public Object getDetails() { return details; }
public String getType() {
return type;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public Object getDetails() {
return details;
}
}

View File

@@ -38,12 +38,12 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
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()
"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();
@@ -59,16 +59,16 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
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");
.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
@@ -88,7 +88,7 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
}
Path filePath = Paths.get(params.filePath);
// 验证是否为绝对路径
if (!filePath.isAbsolute()) {
return "File path must be absolute: " + params.filePath;
@@ -102,15 +102,15 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
// 验证文件扩展名
String fileName = filePath.getFileName().toString();
if (!isAllowedFileType(fileName)) {
return "File type not allowed: " + fileName +
". Allowed extensions: " + appProperties.getWorkspace().getAllowedExtensions();
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 "Content too large: " + contentBytes.length + " bytes. Maximum allowed: " +
appProperties.getWorkspace().getMaxFileSize() + " bytes";
}
return null;
@@ -120,7 +120,7 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
public CompletableFuture<ToolConfirmationDetails> shouldConfirmExecute(WriteFileParams params) {
// 根据配置决定是否需要确认
if (appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.AUTO_EDIT ||
appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.YOLO) {
appProperties.getSecurity().getApprovalMode() == AppProperties.ApprovalMode.YOLO) {
return CompletableFuture.completedFuture(null);
}
@@ -129,24 +129,24 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
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
filePath.getFileName().toString(),
currentContent,
params.content
);
String title = isNewFile ?
"Confirm Create: " + getRelativePath(filePath) :
"Confirm Write: " + getRelativePath(filePath);
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; // 如果无法读取文件,直接执行
@@ -160,7 +160,7 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
@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));
String.format("文件路径=%s, 内容长度=%d字符", filePath, content != null ? content.length() : 0));
long startTime = System.currentTimeMillis();
try {
@@ -179,7 +179,7 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
}
executionLogger.logFileOperation(callId, "写入文件", filePath,
String.format("内容长度: %d字符", content != null ? content.length() : 0));
String.format("内容长度: %d字符", content != null ? content.length() : 0));
// Execute the tool
ToolResult result = execute(params).join();
@@ -209,41 +209,41 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
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);
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 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);
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());
@@ -258,16 +258,16 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
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
fileName + " (Original)",
fileName + " (New)",
oldLines,
patch,
3 // context lines
);
return String.join("\n", unifiedDiff);
} catch (Exception e) {
logger.warn("Could not generate diff", e);
@@ -279,7 +279,7 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
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);
}
@@ -303,7 +303,7 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
private boolean isAllowedFileType(String fileName) {
List<String> allowedExtensions = appProperties.getWorkspace().getAllowedExtensions();
return allowedExtensions.stream()
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
.anyMatch(ext -> fileName.toLowerCase().endsWith(ext.toLowerCase()));
}
private String getRelativePath(Path filePath) {
@@ -321,11 +321,12 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
public static class WriteFileParams {
@JsonProperty("file_path")
private String filePath;
private String content;
// 构造器
public WriteFileParams() {}
public WriteFileParams() {
}
public WriteFileParams(String filePath, String content) {
this.filePath = filePath;
@@ -333,16 +334,26 @@ public class WriteFileTool extends BaseTool<WriteFileTool.WriteFileParams> {
}
// Getters and Setters
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public String getFilePath() {
return filePath;
}
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
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);
return String.format("WriteFileParams{path='%s', contentLength=%d}",
filePath, content != null ? content.length() : 0);
}
}
}

View File

@@ -3,7 +3,7 @@ package com.example.demo.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.awt.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -13,12 +13,12 @@ import java.net.URISyntaxException;
* 用于跨平台打开默认浏览器
*/
public class BrowserUtil {
private static final Logger logger = LoggerFactory.getLogger(BrowserUtil.class);
/**
* 打开默认浏览器访问指定URL
*
*
* @param url 要访问的URL
* @return 是否成功打开
*/
@@ -27,7 +27,7 @@ public class BrowserUtil {
logger.warn("URL is null or empty, cannot open browser");
return false;
}
try {
// 方法1: 使用Desktop API (推荐)
if (Desktop.isDesktopSupported()) {
@@ -38,20 +38,20 @@ public class BrowserUtil {
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 是否成功打开
*/
@@ -59,7 +59,7 @@ public class BrowserUtil {
try {
String os = System.getProperty("os.name").toLowerCase();
ProcessBuilder processBuilder;
if (os.contains("win")) {
// Windows
processBuilder = new ProcessBuilder("rundll32", "url.dll,FileProtocolHandler", url);
@@ -70,24 +70,24 @@ public class BrowserUtil {
// 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 是否有效
*/
@@ -95,7 +95,7 @@ public class BrowserUtil {
if (url == null || url.trim().isEmpty()) {
return false;
}
try {
new URI(url);
return url.startsWith("http://") || url.startsWith("https://");

View File

@@ -6,45 +6,50 @@ import java.nio.file.Paths;
* 跨平台路径处理工具类
*/
public class PathUtils {
/**
* 构建跨平台兼容的绝对路径
* @param basePath 基础路径
*
* @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() {

View File

@@ -18,7 +18,7 @@ body {
.container {
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 1200px;
height: 80vh;
@@ -89,7 +89,7 @@ body {
.message > div {
max-width: 70%;
padding: 12px 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.message-role {
@@ -263,7 +263,7 @@ body {
border: 1px solid #e0e0e0;
border-radius: 12px;
margin: 15px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
}
@@ -447,18 +447,32 @@ body {
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.5; }
0%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0.5;
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */

View File

@@ -12,7 +12,7 @@ const loading = document.getElementById('loading');
const status = document.getElementById('status');
// 全局错误处理
window.addEventListener('error', function(event) {
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');
@@ -40,7 +40,7 @@ async function sendMessage() {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message })
body: JSON.stringify({message: message})
});
const data = await response.json();
@@ -104,7 +104,7 @@ function quickAction(message) {
// 清除历史
async function clearHistory() {
try {
await fetch('/api/chat/clear', { method: 'POST' });
await fetch('/api/chat/clear', {method: 'POST'});
messagesContainer.innerHTML = '';
showStatus('History cleared', 'success');
} catch (error) {
@@ -232,61 +232,61 @@ function handleStreamResponse(userMessage) {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: userMessage })
body: JSON.stringify({message: userMessage})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
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;
function readStream() {
return reader.read().then(({done, value}) => {
if (done) {
console.log('✅ 流式响应完成');
streamIndicator.style.display = 'none';
streamContainer.classList.remove('streaming');
showStatus('流式对话完成', 'success');
return;
}
}
return readStream();
});
}
const chunk = decoder.decode(value, {stream: true});
console.log('📨 收到流式数据块:', chunk);
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');
});
// 处理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');
});
}
// 移除等待状态卡片
@@ -298,7 +298,7 @@ function removeWaitingToolCard() {
}
// 事件监听器
messageInput.addEventListener('keypress', function(e) {
messageInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
@@ -320,7 +320,7 @@ function debugVariables() {
}
// 页面加载完成后聚焦输入框
window.addEventListener('load', function() {
window.addEventListener('load', function () {
messageInput.focus();
// 确保函数在全局作用域中可用

View File

@@ -47,7 +47,7 @@ class ToolLogDisplay {
addToolStart(logEvent) {
// 移除等待状态卡片(如果存在)
removeWaitingToolCard();
const toolCard = this.createToolCard(logEvent);
const content = this.container.querySelector('.tool-log-content');
content.appendChild(toolCard);

View File

@@ -2,108 +2,115 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>SpringAI Alibaba Copilot</title>
<link rel="stylesheet" href="/css/main.css">
<link href="/css/main.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="header">
<h1>🤖 SpringAI Alibaba 编码助手</h1>
<p>AI助手将分析您的需求制定执行计划并逐步完成任务</p>
</div>
<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 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 class="loading" id="loading">
<div>🤔 AI is thinking...</div>
</div>
<div class="input-area">
<input id="messageInput" placeholder="Ask me to create files, read content, or manage your project..."
type="text"/>
<button id="sendBtn" onclick="sendMessage()">Send</button>
<button class="clear-btn" id="clearBtn" onclick="clearHistory()">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="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 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>
<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>
<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>
<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 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 class="status" id="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 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>
<!-- JavaScript文件引用 -->
<script src="/js/tool-log-display.js"></script>
<script src="/js/sse-manager.js"></script>
<script src="/js/main.js"></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<?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"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -48,9 +48,6 @@
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>