auto-coder 0.1.374__py3-none-any.whl → 0.1.375__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from autocoder.agent.base_agentic.tools.base_tool_resolver import BaseToolResolver
3
3
  from autocoder.agent.base_agentic.types import ListFilesTool, ToolResult # Import ToolResult from types
4
- from typing import Optional, Dict, Any, List
4
+ from typing import Optional, Dict, Any, List, Set, Union
5
5
  import fnmatch
6
6
  import re
7
7
  import json
@@ -21,13 +21,40 @@ class ListFilesToolResolver(BaseToolResolver):
21
21
  self.tool: ListFilesTool = tool # For type hinting
22
22
  self.shadow_manager = self.agent.shadow_manager
23
23
 
24
- def resolve(self) -> ToolResult:
25
- list_path_str = self.tool.path
26
- recursive = self.tool.recursive or False
27
- source_dir = self.args.source_dir or "."
28
- absolute_source_dir = os.path.abspath(source_dir)
29
- absolute_list_path = os.path.abspath(os.path.join(source_dir, list_path_str))
30
-
24
+ def list_files_in_dir(self, base_dir: str, recursive: bool, source_dir: str, is_outside_source: bool) -> Set[str]:
25
+ """Helper function to list files in a directory"""
26
+ result = set()
27
+ try:
28
+ if recursive:
29
+ for root, dirs, files in os.walk(base_dir):
30
+ # Modify dirs in-place to skip ignored dirs early
31
+ dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
32
+ for name in files:
33
+ full_path = os.path.join(root, name)
34
+ if should_ignore(full_path):
35
+ continue
36
+ display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
37
+ result.add(display_path)
38
+ for d in dirs:
39
+ full_path = os.path.join(root, d)
40
+ display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
41
+ result.add(display_path + "/")
42
+ else:
43
+ for item in os.listdir(base_dir):
44
+ full_path = os.path.join(base_dir, item)
45
+ if should_ignore(full_path):
46
+ continue
47
+ display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
48
+ if os.path.isdir(full_path):
49
+ result.add(display_path + "/")
50
+ else:
51
+ result.add(display_path)
52
+ except Exception as e:
53
+ logger.warning(f"Error listing files in {base_dir}: {e}")
54
+ return result
55
+
56
+ def list_files_with_shadow(self, list_path_str: str, recursive: bool, source_dir: str, absolute_source_dir: str, absolute_list_path: str) -> Union[ToolResult, List[str]]:
57
+ """List files using shadow manager for path translation"""
31
58
  # Security check: Allow listing outside source_dir IF the original path is outside?
32
59
  is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
33
60
  if is_outside_source:
@@ -52,46 +79,14 @@ class ListFilesToolResolver(BaseToolResolver):
52
79
  if shadow_exists and not os.path.isdir(shadow_dir_path):
53
80
  return ToolResult(success=False, message=f"Error: Shadow path is not a directory: {shadow_dir_path}")
54
81
 
55
- # Helper function to list files in a directory
56
- def list_files_in_dir(base_dir: str) -> set:
57
- result = set()
58
- try:
59
- if recursive:
60
- for root, dirs, files in os.walk(base_dir):
61
- # Modify dirs in-place to skip ignored dirs early
62
- dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
63
- for name in files:
64
- full_path = os.path.join(root, name)
65
- if should_ignore(full_path):
66
- continue
67
- display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
68
- result.add(display_path)
69
- for d in dirs:
70
- full_path = os.path.join(root, d)
71
- display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
72
- result.add(display_path + "/")
73
- else:
74
- for item in os.listdir(base_dir):
75
- full_path = os.path.join(base_dir, item)
76
- if should_ignore(full_path):
77
- continue
78
- display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
79
- if os.path.isdir(full_path):
80
- result.add(display_path + "/")
81
- else:
82
- result.add(display_path)
83
- except Exception as e:
84
- logger.warning(f"Error listing files in {base_dir}: {e}")
85
- return result
86
-
87
82
  # Collect files from shadow and/or source directory
88
83
  shadow_files_set = set()
89
84
  if shadow_exists:
90
- shadow_files_set = list_files_in_dir(shadow_dir_path)
85
+ shadow_files_set = self.list_files_in_dir(shadow_dir_path, recursive, source_dir, is_outside_source)
91
86
 
92
87
  source_files_set = set()
93
88
  if os.path.exists(absolute_list_path) and os.path.isdir(absolute_list_path):
94
- source_files_set = list_files_in_dir(absolute_list_path)
89
+ source_files_set = self.list_files_in_dir(absolute_list_path, recursive, source_dir, is_outside_source)
95
90
 
96
91
  # Merge results, prioritizing shadow files if exist
97
92
  if shadow_exists:
@@ -104,7 +99,52 @@ class ListFilesToolResolver(BaseToolResolver):
104
99
  try:
105
100
  message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(merged_files)} items."
106
101
  logger.info(message)
107
- return ToolResult(success=True, message=message, content=sorted(merged_files))
102
+ return sorted(merged_files)
103
+ except Exception as e:
104
+ logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
105
+ return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
106
+
107
+ def list_files_normal(self, list_path_str: str, recursive: bool, source_dir: str, absolute_source_dir: str, absolute_list_path: str) -> Union[ToolResult, List[str]]:
108
+ """List files directly without using shadow manager"""
109
+ # Security check: Allow listing outside source_dir IF the original path is outside?
110
+ is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
111
+ if is_outside_source:
112
+ logger.warning(f"Listing path is outside the project source directory: {list_path_str}")
113
+
114
+ # Validate that the directory exists
115
+ if not os.path.exists(absolute_list_path):
116
+ return ToolResult(success=False, message=f"Error: Path not found: {list_path_str}")
117
+ if not os.path.isdir(absolute_list_path):
118
+ return ToolResult(success=False, message=f"Error: Path is not a directory: {list_path_str}")
119
+
120
+ # Collect files from the directory
121
+ files_set = self.list_files_in_dir(absolute_list_path, recursive, source_dir, is_outside_source)
122
+
123
+ try:
124
+ message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(files_set)} items."
125
+ logger.info(message)
126
+ return sorted(files_set)
108
127
  except Exception as e:
109
128
  logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
110
129
  return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
130
+
131
+ def resolve(self) -> ToolResult:
132
+ """Resolve the list files tool by calling the appropriate implementation"""
133
+ list_path_str = self.tool.path
134
+ recursive = self.tool.recursive or False
135
+ source_dir = self.args.source_dir or "."
136
+ absolute_source_dir = os.path.abspath(source_dir)
137
+ absolute_list_path = os.path.abspath(os.path.join(source_dir, list_path_str))
138
+
139
+ # Choose the appropriate implementation based on whether shadow_manager is available
140
+ if self.shadow_manager:
141
+ result = self.list_files_with_shadow(list_path_str, recursive, source_dir, absolute_source_dir, absolute_list_path)
142
+ else:
143
+ result = self.list_files_normal(list_path_str, recursive, source_dir, absolute_source_dir, absolute_list_path)
144
+
145
+ # Handle the case where the implementation returns a sorted list instead of a ToolResult
146
+ if isinstance(result, list):
147
+ message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(result)} items."
148
+ return ToolResult(success=True, message=message, content=result)
149
+ else:
150
+ return result
@@ -5,6 +5,16 @@ from autocoder.agent.base_agentic.types import ReadFileTool, ToolResult # Impor
5
5
  from loguru import logger
6
6
  import typing
7
7
  from autocoder.common import AutoCoderArgs
8
+ from autocoder.common.context_pruner import PruneContext
9
+ from autocoder.common import SourceCode
10
+ from autocoder.rag.token_counter import count_tokens
11
+ from loguru import logger
12
+ import typing
13
+ from autocoder.rag.loaders import (
14
+ extract_text_from_pdf,
15
+ extract_text_from_docx,
16
+ extract_text_from_ppt
17
+ )
8
18
 
9
19
  if typing.TYPE_CHECKING:
10
20
  from ..base_agent import BaseAgent
@@ -15,40 +25,93 @@ class ReadFileToolResolver(BaseToolResolver):
15
25
  super().__init__(agent, tool, args)
16
26
  self.tool: ReadFileTool = tool # For type hinting
17
27
  self.shadow_manager = self.agent.shadow_manager if self.agent else None
28
+ self.context_pruner = PruneContext(
29
+ max_tokens=self.args.context_prune_safe_zone_tokens,
30
+ args=self.args,
31
+ llm=self.agent.context_prune_llm
32
+ )
18
33
 
19
- def resolve(self) -> ToolResult:
20
- file_path = self.tool.path
21
- source_dir = self.args.source_dir or "."
22
- abs_project_dir = os.path.abspath(source_dir)
23
- abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
34
+ def _prune_file_content(self, content: str, file_path: str) -> str:
35
+ """对文件内容进行剪枝处理"""
36
+ if not self.context_pruner:
37
+ return content
38
+
39
+ # 计算 token 数量
40
+ tokens = count_tokens(content)
41
+ if tokens <= self.args.context_prune_safe_zone_tokens:
42
+ return content
43
+
44
+ # 创建 SourceCode 对象
45
+ source_code = SourceCode(
46
+ module_name=file_path,
47
+ source_code=content,
48
+ tokens=tokens
49
+ )
50
+
51
+ # 使用 context_pruner 进行剪枝
52
+ pruned_sources = self.context_pruner.handle_overflow(
53
+ file_sources=[source_code],
54
+ conversations=self.agent.current_conversations if self.agent else [],
55
+ strategy=self.args.context_prune_strategy
56
+ )
57
+
58
+ if not pruned_sources:
59
+ return content
60
+
61
+ return pruned_sources[0].source_code
24
62
 
25
- # # Security check: ensure the path is within the source directory
26
- # if not abs_file_path.startswith(abs_project_dir):
27
- # return ToolResult(success=False, message=f"Error: Access denied. Attempted to read file outside the project directory: {file_path}")
63
+ def _read_file_content(self, file_path_to_read: str) -> str:
64
+ content = ""
65
+ ext = os.path.splitext(file_path_to_read)[1].lower()
28
66
 
67
+ if ext == '.pdf':
68
+ logger.info(f"Extracting text from PDF: {file_path_to_read}")
69
+ content = extract_text_from_pdf(file_path_to_read)
70
+ elif ext == '.docx':
71
+ logger.info(f"Extracting text from DOCX: {file_path_to_read}")
72
+ content = extract_text_from_docx(file_path_to_read)
73
+ elif ext in ('.pptx', '.ppt'):
74
+ logger.info(f"Extracting text from PPT/PPTX: {file_path_to_read}")
75
+ slide_texts = []
76
+ for slide_identifier, slide_text_content in extract_text_from_ppt(file_path_to_read):
77
+ slide_texts.append(f"--- Slide {slide_identifier} ---\n{slide_text_content}")
78
+ content = "\n\n".join(slide_texts) if slide_texts else ""
79
+ else:
80
+ logger.info(f"Reading plain text file: {file_path_to_read}")
81
+ with open(file_path_to_read, 'r', encoding='utf-8', errors='replace') as f:
82
+ content = f.read()
83
+
84
+ # 对内容进行剪枝处理
85
+ return self._prune_file_content(content, file_path_to_read)
86
+
87
+
88
+ def read_file_normal(self, file_path: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
89
+ """Read file directly without using shadow manager"""
29
90
  try:
30
- try:
31
- if self.shadow_manager:
32
- shadow_path = self.shadow_manager.to_shadow_path(abs_file_path)
33
- # If shadow file exists, read from it
34
- if os.path.exists(shadow_path) and os.path.isfile(shadow_path):
35
- with open(shadow_path, 'r', encoding='utf-8', errors='replace') as f:
36
- content = f.read()
37
- logger.info(f"[Shadow] Successfully read shadow file: {shadow_path}")
38
- return ToolResult(success=True, message=f"Successfully read file (shadow): {file_path}", content=content)
39
- except Exception as e:
40
- pass
41
- # else fallback to original file
42
- # Fallback to original file
91
+ # Security check: ensure the path is within the source directory
92
+ if not abs_file_path.startswith(abs_project_dir):
93
+ return ToolResult(success=False, message=f"Error: Access denied. Attempted to read file outside the project directory: {file_path}")
94
+
95
+ # Check if file exists
43
96
  if not os.path.exists(abs_file_path):
44
- return ToolResult(success=False, message=f"Error: File not found at path: {file_path}")
97
+ return ToolResult(success=False, message=f"Error: File not found: {file_path}")
45
98
  if not os.path.isfile(abs_file_path):
46
- return ToolResult(success=False, message=f"Error: Path is not a file: {file_path}")
99
+ return ToolResult(success=False, message=f"Error: Not a file: {file_path}")
47
100
 
48
- with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
49
- content = f.read()
101
+ # Read file content
102
+ content = self._read_file_content(abs_file_path)
50
103
  logger.info(f"Successfully read file: {file_path}")
51
104
  return ToolResult(success=True, message=f"Successfully read file: {file_path}", content=content)
52
105
  except Exception as e:
53
106
  logger.error(f"Error reading file '{file_path}': {str(e)}")
54
107
  return ToolResult(success=False, message=f"An error occurred while reading the file: {str(e)}")
108
+
109
+ def resolve(self) -> ToolResult:
110
+ """Resolve the read file tool by calling the appropriate implementation"""
111
+ file_path = self.tool.path
112
+ source_dir = self.args.source_dir or "."
113
+ abs_project_dir = os.path.abspath(source_dir)
114
+ abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
115
+
116
+ return self.read_file_normal(file_path, source_dir, abs_project_dir, abs_file_path)
117
+
@@ -10,6 +10,9 @@ from autocoder.common.printer import Printer
10
10
  from autocoder.common import AutoCoderArgs
11
11
  from loguru import logger
12
12
  from autocoder.common.auto_coder_lang import get_message_with_format
13
+ from autocoder.common.file_checkpoint.models import FileChange as CheckpointFileChange
14
+ from autocoder.common.file_checkpoint.manager import FileChangeManager as CheckpointFileChangeManager
15
+ from autocoder.linters.models import IssueSeverity, FileLintResult
13
16
  if typing.TYPE_CHECKING:
14
17
  from ..base_agent import BaseAgent
15
18
 
@@ -17,7 +20,9 @@ class ReplaceInFileToolResolver(BaseToolResolver):
17
20
  def __init__(self, agent: Optional['BaseAgent'], tool: ReplaceInFileTool, args: AutoCoderArgs):
18
21
  super().__init__(agent, tool, args)
19
22
  self.tool: ReplaceInFileTool = tool # For type hinting
23
+ self.args = args
20
24
  self.shadow_manager = self.agent.shadow_manager if self.agent else None
25
+ self.shadow_linter = self.agent.shadow_linter if self.agent else None
21
26
 
22
27
  def parse_diff(self, diff_content: str) -> List[Tuple[str, str]]:
23
28
  """
@@ -61,78 +66,155 @@ class ReplaceInFileToolResolver(BaseToolResolver):
61
66
  logger.warning(f"Could not parse any SEARCH/REPLACE blocks from diff: {diff_content}")
62
67
  return blocks
63
68
 
64
- def resolve(self) -> ToolResult:
65
- file_path = self.tool.path
66
- diff_content = self.tool.diff
67
- source_dir = self.args.source_dir or "."
68
- abs_project_dir = os.path.abspath(source_dir)
69
- abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
70
-
71
- # Security check
72
- if not abs_file_path.startswith(abs_project_dir):
73
- return ToolResult(success=False, message=get_message_with_format("replace_in_file.access_denied", file_path=file_path))
69
+ def _filter_lint_issues(self, lint_result:FileLintResult, levels: List[IssueSeverity] = [IssueSeverity.ERROR, IssueSeverity.WARNING]):
70
+ """
71
+ 过滤 lint 结果,只保留指定级别的问题
72
+
73
+ 参数:
74
+ lint_result: 单个文件的 lint 结果对象
75
+ levels: 要保留的问题级别列表,默认保留 ERROR 和 WARNING 级别
76
+
77
+ 返回:
78
+ 过滤后的 lint 结果对象(原对象的副本)
79
+ """
80
+ if not lint_result or not lint_result.issues:
81
+ return lint_result
82
+
83
+ # 创建一个新的 issues 列表,只包含指定级别的问题
84
+ filtered_issues = []
85
+ for issue in lint_result.issues:
86
+ if issue.severity in levels:
87
+ filtered_issues.append(issue)
88
+
89
+ # 更新 lint_result 的副本
90
+ filtered_result = lint_result
91
+ filtered_result.issues = filtered_issues
92
+
93
+ # 更新计数
94
+ filtered_result.error_count = sum(1 for issue in filtered_result.issues if issue.severity == IssueSeverity.ERROR)
95
+ filtered_result.warning_count = sum(1 for issue in filtered_result.issues if issue.severity == IssueSeverity.WARNING)
96
+ filtered_result.info_count = sum(1 for issue in filtered_result.issues if issue.severity == IssueSeverity.INFO)
97
+
98
+ return filtered_result
99
+
100
+ def _format_lint_issues(self, lint_result:FileLintResult):
101
+ """
102
+ 将 lint 结果格式化为可读的文本格式
103
+
104
+ 参数:
105
+ lint_result: 单个文件的 lint 结果对象
106
+
107
+ 返回:
108
+ str: 格式化的问题描述
109
+ """
110
+ formatted_issues = []
111
+
112
+ for issue in lint_result.issues:
113
+ severity = "错误" if issue.severity.value == 3 else "警告" if issue.severity.value == 2 else "信息"
114
+ line_info = f"第{issue.position.line}行"
115
+ if issue.position.column:
116
+ line_info += f", 第{issue.position.column}列"
117
+
118
+ formatted_issues.append(
119
+ f" - [{severity}] {line_info}: {issue.message} (规则: {issue.code})"
120
+ )
121
+
122
+ return "\n".join(formatted_issues)
74
123
 
75
- # Determine target path: shadow file if shadow_manager exists
76
- target_path = abs_file_path
77
- if self.shadow_manager:
78
- target_path = self.shadow_manager.to_shadow_path(abs_file_path)
124
+
79
125
 
80
- # If shadow file does not exist yet, but original file exists, copy original content into shadow first? No, just treat as normal file.
81
- # For now, read from shadow if exists, else fallback to original file
126
+ def replace_in_file_normal(self, file_path: str, diff_content: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
127
+ """Replace content in file directly without using shadow manager"""
82
128
  try:
83
- if os.path.exists(target_path) and os.path.isfile(target_path):
84
- with open(target_path, 'r', encoding='utf-8', errors='replace') as f:
85
- original_content = f.read()
86
- elif self.shadow_manager and os.path.exists(abs_file_path) and os.path.isfile(abs_file_path):
87
- # If shadow doesn't exist, but original exists, read original content (create shadow implicitly later)
88
- with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
89
- original_content = f.read()
90
- # create parent dirs of shadow if needed
91
- os.makedirs(os.path.dirname(target_path), exist_ok=True)
92
- # write original content into shadow file as baseline
93
- with open(target_path, 'w', encoding='utf-8') as f:
94
- f.write(original_content)
95
- logger.info(f"[Shadow] Initialized shadow file from original: {target_path}")
96
- else:
129
+ # Read original content
130
+ if not os.path.exists(abs_file_path):
97
131
  return ToolResult(success=False, message=get_message_with_format("replace_in_file.file_not_found", file_path=file_path))
98
- except Exception as e:
99
- logger.error(f"Error reading file for replace '{file_path}': {str(e)}")
100
- return ToolResult(success=False, message=get_message_with_format("replace_in_file.read_error", error=str(e)))
132
+ if not os.path.isfile(abs_file_path):
133
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.not_a_file", file_path=file_path))
101
134
 
102
- parsed_blocks = self.parse_diff(diff_content)
103
- if not parsed_blocks:
104
- return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
135
+ with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
136
+ original_content = f.read()
105
137
 
106
- current_content = original_content
107
- applied_count = 0
108
- errors = []
138
+ parsed_blocks = self.parse_diff(diff_content)
139
+ if not parsed_blocks:
140
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
109
141
 
110
- # Apply blocks sequentially
111
- for i, (search_block, replace_block) in enumerate(parsed_blocks):
112
- start_index = current_content.find(search_block)
142
+ current_content = original_content
143
+ applied_count = 0
144
+ errors = []
113
145
 
114
- if start_index != -1:
115
- current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
116
- applied_count += 1
117
- logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
118
- else:
119
- error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
120
- logger.warning(error_message)
121
- context_start = max(0, original_content.find(search_block[:20]) - 100)
122
- context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
123
- logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
124
- errors.append(error_message)
125
- # continue applying remaining blocks
146
+ # Apply blocks sequentially
147
+ for i, (search_block, replace_block) in enumerate(parsed_blocks):
148
+ start_index = current_content.find(search_block)
126
149
 
127
- if applied_count == 0 and errors:
128
- return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
150
+ if start_index != -1:
151
+ current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
152
+ applied_count += 1
153
+ logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
154
+ else:
155
+ error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
156
+ logger.warning(error_message)
157
+ context_start = max(0, original_content.find(search_block[:20]) - 100)
158
+ context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
159
+ logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
160
+ errors.append(error_message)
129
161
 
130
- try:
131
- os.makedirs(os.path.dirname(target_path), exist_ok=True)
132
- with open(target_path, 'w', encoding='utf-8') as f:
133
- f.write(current_content)
162
+ if applied_count == 0 and errors:
163
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
164
+
165
+ # Write the modified content back to file
166
+ if self.agent and self.agent.checkpoint_manager:
167
+ changes = {
168
+ file_path: CheckpointFileChange(
169
+ file_path=file_path,
170
+ content=current_content,
171
+ is_deletion=False,
172
+ is_new=True
173
+ )
174
+ }
175
+ change_group_id = self.args.event_file
176
+
177
+ self.agent.checkpoint_manager.apply_changes_with_conversation(
178
+ changes=changes,
179
+ conversations=self.agent.current_conversations,
180
+ change_group_id=change_group_id,
181
+ metadata={"event_file": self.args.event_file}
182
+ )
183
+ else:
184
+ with open(abs_file_path, 'w', encoding='utf-8') as f:
185
+ f.write(current_content)
186
+
134
187
  logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
135
188
 
189
+ # 新增:执行代码质量检查
190
+ lint_results = None
191
+ lint_message = ""
192
+ formatted_issues = ""
193
+ has_lint_issues = False
194
+
195
+ # 检查是否启用了Lint功能
196
+ enable_lint = self.args.enable_auto_fix_lint
197
+ logger.info(f"检查Lint功能状态: enable_lint={enable_lint}")
198
+
199
+ if enable_lint:
200
+ try:
201
+ if self.agent.linter:
202
+ lint_results = self.agent.linter.lint_file(file_path)
203
+ if lint_results and lint_results.issues:
204
+ # 过滤 lint 结果,只保留 ERROR 和 WARNING 级别的问题
205
+ filtered_results = self._filter_lint_issues(lint_results)
206
+ if filtered_results.issues:
207
+ has_lint_issues = True
208
+ # 格式化 lint 问题
209
+ formatted_issues = self._format_lint_issues(filtered_results)
210
+ lint_message = f"\n\n代码质量检查发现 {len(filtered_results.issues)} 个问题"
211
+ except Exception as e:
212
+ logger.error(f"Lint 检查失败: {str(e)}")
213
+ lint_message = "\n\n尝试进行代码质量检查时出错。"
214
+ else:
215
+ logger.info("代码质量检查已禁用")
216
+
217
+ # 构建包含 lint 结果的返回消息
136
218
  if errors:
137
219
  message = get_message_with_format("replace_in_file.apply_success_with_warnings",
138
220
  applied=applied_count,
@@ -143,14 +225,41 @@ class ReplaceInFileToolResolver(BaseToolResolver):
143
225
  message = get_message_with_format("replace_in_file.apply_success",
144
226
  applied=applied_count,
145
227
  total=len(parsed_blocks),
146
- file_path=file_path)
228
+ file_path=file_path)
147
229
 
148
230
  # 变更跟踪,回调AgenticEdit
149
231
  if self.agent:
150
232
  rel_path = os.path.relpath(abs_file_path, abs_project_dir)
151
233
  self.agent.record_file_change(rel_path, "modified", diff=diff_content, content=current_content)
234
+
235
+ # 附加 lint 结果到返回内容
236
+ result_content = {
237
+ "content": current_content,
238
+ }
239
+
240
+ # 只有在启用Lint时才添加Lint结果
241
+ if enable_lint:
242
+ message = message + "\n" + lint_message
243
+ result_content["lint_results"] = {
244
+ "has_issues": has_lint_issues,
245
+ "issues": formatted_issues if has_lint_issues else None
246
+ }
152
247
 
153
- return ToolResult(success=True, message=message, content=current_content)
248
+ return ToolResult(success=True, message=message, content=result_content)
154
249
  except Exception as e:
155
250
  logger.error(f"Error writing replaced content to file '{file_path}': {str(e)}")
156
251
  return ToolResult(success=False, message=get_message_with_format("replace_in_file.write_error", error=str(e)))
252
+
253
+ def resolve(self) -> ToolResult:
254
+ """Resolve the replace in file tool by calling the appropriate implementation"""
255
+ file_path = self.tool.path
256
+ diff_content = self.tool.diff
257
+ source_dir = self.args.source_dir or "."
258
+ abs_project_dir = os.path.abspath(source_dir)
259
+ abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
260
+
261
+ # Security check
262
+ if not abs_file_path.startswith(abs_project_dir):
263
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.access_denied", file_path=file_path))
264
+
265
+ return self.replace_in_file_normal(file_path, diff_content, source_dir, abs_project_dir, abs_file_path)