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