jarvis-ai-assistant 0.1.88__py3-none-any.whl → 0.1.89__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 jarvis-ai-assistant might be problematic. Click here for more details.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_coder/git_utils.py +67 -0
- jarvis/jarvis_coder/main.py +50 -398
- jarvis/jarvis_coder/model_utils.py +30 -0
- jarvis/jarvis_coder/patch_handler.py +390 -0
- {jarvis_ai_assistant-0.1.88.dist-info → jarvis_ai_assistant-0.1.89.dist-info}/METADATA +1 -1
- {jarvis_ai_assistant-0.1.88.dist-info → jarvis_ai_assistant-0.1.89.dist-info}/RECORD +11 -8
- {jarvis_ai_assistant-0.1.88.dist-info → jarvis_ai_assistant-0.1.89.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.88.dist-info → jarvis_ai_assistant-0.1.89.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.88.dist-info → jarvis_ai_assistant-0.1.89.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.1.88.dist-info → jarvis_ai_assistant-0.1.89.dist-info}/top_level.txt +0 -0
jarvis/__init__.py
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import List
|
|
3
|
+
import yaml
|
|
4
|
+
import time
|
|
5
|
+
from jarvis.utils import OutputType, PrettyOutput
|
|
6
|
+
from jarvis.models.registry import PlatformRegistry
|
|
7
|
+
from .model_utils import call_model_with_retry
|
|
8
|
+
|
|
9
|
+
def has_uncommitted_files() -> bool:
|
|
10
|
+
"""判断代码库是否有未提交的文件"""
|
|
11
|
+
# 获取未暂存的修改
|
|
12
|
+
unstaged = os.popen("git diff --name-only").read()
|
|
13
|
+
# 获取已暂存但未提交的修改
|
|
14
|
+
staged = os.popen("git diff --cached --name-only").read()
|
|
15
|
+
# 获取未跟踪的文件
|
|
16
|
+
untracked = os.popen("git ls-files --others --exclude-standard").read()
|
|
17
|
+
|
|
18
|
+
return bool(unstaged or staged or untracked)
|
|
19
|
+
|
|
20
|
+
def generate_commit_message(git_diff: str, feature: str) -> str:
|
|
21
|
+
"""根据git diff和功能描述生成commit信息"""
|
|
22
|
+
prompt = f"""你是一个经验丰富的程序员,请根据以下代码变更和功能描述生成简洁明了的commit信息:
|
|
23
|
+
|
|
24
|
+
功能描述:
|
|
25
|
+
{feature}
|
|
26
|
+
|
|
27
|
+
代码变更:
|
|
28
|
+
Git Diff:
|
|
29
|
+
{git_diff}
|
|
30
|
+
|
|
31
|
+
请遵循以下规则:
|
|
32
|
+
1. 使用英文编写
|
|
33
|
+
2. 采用常规的commit message格式:<type>(<scope>): <subject>
|
|
34
|
+
3. 保持简洁,不超过50个字符
|
|
35
|
+
4. 准确描述代码变更的主要内容
|
|
36
|
+
5. 优先考虑功能描述和git diff中的变更内容
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
model = PlatformRegistry().get_global_platform_registry().get_codegen_platform()
|
|
40
|
+
model.set_suppress_output(True)
|
|
41
|
+
success, response = call_model_with_retry(model, prompt)
|
|
42
|
+
if not success:
|
|
43
|
+
return "Update code changes"
|
|
44
|
+
|
|
45
|
+
return response.strip().split("\n")[0]
|
|
46
|
+
|
|
47
|
+
def save_edit_record(record_dir: str, commit_message: str, git_diff: str) -> None:
|
|
48
|
+
"""保存代码修改记录"""
|
|
49
|
+
# 获取下一个序号
|
|
50
|
+
existing_records = [f for f in os.listdir(record_dir) if f.endswith('.yaml')]
|
|
51
|
+
next_num = 1
|
|
52
|
+
if existing_records:
|
|
53
|
+
last_num = max(int(f[:4]) for f in existing_records)
|
|
54
|
+
next_num = last_num + 1
|
|
55
|
+
|
|
56
|
+
# 创建记录文件
|
|
57
|
+
record = {
|
|
58
|
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
59
|
+
"commit_message": commit_message,
|
|
60
|
+
"git_diff": git_diff
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
record_path = os.path.join(record_dir, f"{next_num:04d}.yaml")
|
|
64
|
+
with open(record_path, "w", encoding="utf-8") as f:
|
|
65
|
+
yaml.safe_dump(record, f, allow_unicode=True)
|
|
66
|
+
|
|
67
|
+
PrettyOutput.print(f"已保存修改记录: {record_path}", OutputType.SUCCESS)
|
jarvis/jarvis_coder/main.py
CHANGED
|
@@ -14,6 +14,8 @@ from prompt_toolkit.completion import WordCompleter, Completer, Completion
|
|
|
14
14
|
from prompt_toolkit.formatted_text import FormattedText
|
|
15
15
|
from prompt_toolkit.styles import Style
|
|
16
16
|
import fnmatch
|
|
17
|
+
from .patch_handler import PatchHandler
|
|
18
|
+
from .git_utils import has_uncommitted_files, generate_commit_message, save_edit_record
|
|
17
19
|
|
|
18
20
|
# 全局锁对象
|
|
19
21
|
index_lock = threading.Lock()
|
|
@@ -21,13 +23,18 @@ index_lock = threading.Lock()
|
|
|
21
23
|
class JarvisCoder:
|
|
22
24
|
def __init__(self, root_dir: str, language: str):
|
|
23
25
|
"""初始化代码修改工具"""
|
|
24
|
-
|
|
26
|
+
self.root_dir = root_dir
|
|
27
|
+
self.language = language
|
|
28
|
+
self._init_directories()
|
|
29
|
+
self._init_codebase()
|
|
30
|
+
|
|
31
|
+
def _init_directories(self):
|
|
32
|
+
"""初始化目录"""
|
|
25
33
|
self.max_context_length = get_max_context_length()
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
self.root_dir = find_git_root(root_dir)
|
|
35
|
+
self.root_dir = find_git_root(self.root_dir)
|
|
29
36
|
if not self.root_dir:
|
|
30
|
-
self.root_dir = root_dir
|
|
37
|
+
self.root_dir = self.root_dir
|
|
31
38
|
|
|
32
39
|
PrettyOutput.print(f"Git根目录: {self.root_dir}", OutputType.INFO)
|
|
33
40
|
|
|
@@ -57,13 +64,22 @@ class JarvisCoder:
|
|
|
57
64
|
# 2.2 提交
|
|
58
65
|
os.system(f"git commit -m 'Initial commit'")
|
|
59
66
|
|
|
67
|
+
PrettyOutput.print("代码库有未提交的文件,提交一次", OutputType.INFO)
|
|
68
|
+
os.system(f"git add .")
|
|
69
|
+
os.system(f"git commit -m 'commit before code edit'")
|
|
60
70
|
# 3. 查看代码库是否有未提交的文件,如果有,提交一次
|
|
61
71
|
if self._has_uncommitted_files():
|
|
72
|
+
PrettyOutput.print("代码库有未提交的文件,提交一次", OutputType.INFO)
|
|
73
|
+
os.system(f"git add .")
|
|
74
|
+
git_diff = os.popen("git diff --cached").read()
|
|
75
|
+
commit_message = generate_commit_message(git_diff, "Pre-edit commit")
|
|
76
|
+
os.system(f"git commit -m '{commit_message}'")
|
|
62
77
|
PrettyOutput.print("代码库有未提交的文件,提交一次", OutputType.INFO)
|
|
63
78
|
os.system(f"git add .")
|
|
64
79
|
os.system(f"git commit -m 'commit before code edit'")
|
|
65
80
|
|
|
66
|
-
|
|
81
|
+
def _init_codebase(self):
|
|
82
|
+
"""初始化代码库"""
|
|
67
83
|
self._codebase = CodeBase(self.root_dir)
|
|
68
84
|
|
|
69
85
|
def _new_model(self):
|
|
@@ -82,260 +98,11 @@ class JarvisCoder:
|
|
|
82
98
|
|
|
83
99
|
return bool(unstaged or staged or untracked)
|
|
84
100
|
|
|
85
|
-
def _call_model_with_retry(self, model: BasePlatform, prompt: str, max_retries: int = 3, initial_delay: float = 1.0) -> Tuple[bool, str]:
|
|
86
|
-
"""调用模型并支持重试
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
prompt: 提示词
|
|
90
|
-
max_retries: 最大重试次数
|
|
91
|
-
initial_delay: 初始延迟时间(秒)
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
Tuple[bool, str]: (是否成功, 响应内容)
|
|
95
|
-
"""
|
|
96
|
-
delay = initial_delay
|
|
97
|
-
for attempt in range(max_retries):
|
|
98
|
-
try:
|
|
99
|
-
response = model.chat(prompt)
|
|
100
|
-
return True, response
|
|
101
|
-
except Exception as e:
|
|
102
|
-
if attempt == max_retries - 1: # 最后一次尝试
|
|
103
|
-
PrettyOutput.print(f"调用模型失败: {str(e)}", OutputType.ERROR)
|
|
104
|
-
return False, str(e)
|
|
105
|
-
|
|
106
|
-
PrettyOutput.print(f"调用模型失败,{delay}秒后重试: {str(e)}", OutputType.WARNING)
|
|
107
|
-
time.sleep(delay)
|
|
108
|
-
delay *= 2 # 指数退避
|
|
109
|
-
|
|
110
|
-
def _remake_patch(self, prompt: str) -> List[str]:
|
|
111
|
-
success, response = self._call_model_with_retry(self.main_model, prompt, max_retries=5) # 增加重试次数
|
|
112
|
-
if not success:
|
|
113
|
-
return []
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
patches = re.findall(r'<PATCH>.*?</PATCH>', response, re.DOTALL)
|
|
117
|
-
return [patch.replace('<PATCH>', '').replace('</PATCH>', '').strip()
|
|
118
|
-
for patch in patches if patch.strip()]
|
|
119
|
-
except Exception as e:
|
|
120
|
-
PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.WARNING)
|
|
121
|
-
return []
|
|
122
|
-
|
|
123
|
-
def _make_patch(self, related_files: List[Dict], feature: str) -> List[str]:
|
|
124
|
-
"""生成修改方案"""
|
|
125
|
-
prompt = """你是一个资深程序员,请根据需求描述,修改文件内容。
|
|
126
|
-
|
|
127
|
-
修改格式说明:
|
|
128
|
-
1. 每个修改块格式如下:
|
|
129
|
-
<PATCH>
|
|
130
|
-
> path/to/file
|
|
131
|
-
def old_function():
|
|
132
|
-
print("old code")
|
|
133
|
-
return False
|
|
134
|
-
=======
|
|
135
|
-
def old_function():
|
|
136
|
-
print("new code")
|
|
137
|
-
return True
|
|
138
|
-
</PATCH>
|
|
139
|
-
|
|
140
|
-
2. 如果是新文件或者替换整个文件内容,格式如下:
|
|
141
|
-
<PATCH>
|
|
142
|
-
> src/new_module.py
|
|
143
|
-
=======
|
|
144
|
-
from typing import List
|
|
145
|
-
|
|
146
|
-
def new_function():
|
|
147
|
-
return "This is a new file"
|
|
148
|
-
</PATCH>
|
|
149
|
-
|
|
150
|
-
3. 如果要删除文件中的某一段,格式如下:
|
|
151
|
-
<PATCH>
|
|
152
|
-
> path/to/file
|
|
153
|
-
# 这是要删除的注释
|
|
154
|
-
deprecated_code = True
|
|
155
|
-
if deprecated_code:
|
|
156
|
-
print("old feature")
|
|
157
|
-
=======
|
|
158
|
-
</PATCH>
|
|
159
|
-
|
|
160
|
-
4. 如果要修改导入语句,格式如下:
|
|
161
|
-
<PATCH>
|
|
162
|
-
> src/main.py
|
|
163
|
-
from old_module import old_class
|
|
164
|
-
=======
|
|
165
|
-
from new_module import new_class
|
|
166
|
-
</PATCH>
|
|
167
|
-
|
|
168
|
-
5. 如果要修改类定义,格式如下:
|
|
169
|
-
<PATCH>
|
|
170
|
-
> src/models.py
|
|
171
|
-
class OldModel:
|
|
172
|
-
def __init__(self):
|
|
173
|
-
self.value = 0
|
|
174
|
-
=======
|
|
175
|
-
class OldModel:
|
|
176
|
-
def __init__(self):
|
|
177
|
-
self.value = 1
|
|
178
|
-
self.name = "new"
|
|
179
|
-
</PATCH>
|
|
180
|
-
|
|
181
|
-
文件列表如下:
|
|
182
|
-
"""
|
|
183
|
-
for i, file in enumerate(related_files):
|
|
184
|
-
if len(prompt) > self.max_context_length:
|
|
185
|
-
PrettyOutput.print(f'避免上下文超限,丢弃低相关度文件:{file["file_path"]}', OutputType.WARNING)
|
|
186
|
-
continue
|
|
187
|
-
prompt += f"""{i}. {file["file_path"]}\n"""
|
|
188
|
-
prompt += f"""文件内容:\n"""
|
|
189
|
-
prompt += f"<FILE_CONTENT>\n"
|
|
190
|
-
prompt += f'{file["file_content"]}\n'
|
|
191
|
-
prompt += f"</FILE_CONTENT>\n"
|
|
192
|
-
|
|
193
|
-
prompt += f"\n需求描述: {feature}\n"
|
|
194
|
-
prompt += """
|
|
195
|
-
注意事项:
|
|
196
|
-
1、仅输出补丁内容,不要输出任何其他内容,每个补丁必须用<PATCH>和</PATCH>标记
|
|
197
|
-
2、如果在大段代码中有零星修改,生成多个补丁
|
|
198
|
-
3、要替换的内容,一定要与文件内容完全一致,不要有任何多余或者缺失的内容
|
|
199
|
-
4、每个patch不超过20行,超出20行,请生成多个patch
|
|
200
|
-
"""
|
|
201
|
-
|
|
202
|
-
success, response = self._call_model_with_retry(self.main_model, prompt)
|
|
203
|
-
if not success:
|
|
204
|
-
return []
|
|
205
|
-
|
|
206
|
-
try:
|
|
207
|
-
# 使用正则表达式匹配每个patch块
|
|
208
|
-
patches = re.findall(r'<PATCH>.*?</PATCH>', response, re.DOTALL)
|
|
209
|
-
return [patch.replace('<PATCH>', '').replace('</PATCH>', '').strip()
|
|
210
|
-
for patch in patches if patch.strip()]
|
|
211
|
-
except Exception as e:
|
|
212
|
-
PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.WARNING)
|
|
213
|
-
return []
|
|
214
|
-
|
|
215
|
-
def _apply_patch(self, related_files: List[Dict], patches: List[str]) -> Tuple[bool, str]:
|
|
216
|
-
"""应用补丁"""
|
|
217
|
-
error_info = []
|
|
218
|
-
modified_files = set()
|
|
219
|
-
|
|
220
|
-
# 创建文件内容映射
|
|
221
|
-
file_map = {file["file_path"]: file["file_content"] for file in related_files}
|
|
222
|
-
temp_map = file_map.copy() # 创建临时映射用于尝试应用
|
|
223
|
-
|
|
224
|
-
# 尝试应用所有补丁
|
|
225
|
-
for i, patch in enumerate(patches):
|
|
226
|
-
PrettyOutput.print(f"正在应用补丁 {i+1}/{len(patches)}", OutputType.INFO)
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
# 解析补丁
|
|
230
|
-
lines = patch.split("\n")
|
|
231
|
-
if not lines:
|
|
232
|
-
continue
|
|
233
|
-
|
|
234
|
-
# 获取文件路径
|
|
235
|
-
file_path_match = re.search(r'> (.*)', lines[0])
|
|
236
|
-
if not file_path_match:
|
|
237
|
-
error_info.append(f"无法解析文件路径: {lines[0]}")
|
|
238
|
-
return False, "\n".join(error_info)
|
|
239
|
-
|
|
240
|
-
file_path = file_path_match.group(1).strip()
|
|
241
|
-
|
|
242
|
-
# 解析补丁内容
|
|
243
|
-
patch_content = "\n".join(lines[1:])
|
|
244
|
-
parts = patch_content.split("=======")
|
|
245
|
-
|
|
246
|
-
if len(parts) != 2:
|
|
247
|
-
error_info.append(f"补丁格式错误: {file_path}")
|
|
248
|
-
return False, "\n".join(error_info)
|
|
249
|
-
|
|
250
|
-
old_content = parts[0]
|
|
251
|
-
new_content = parts[1].split("</PATCH>")[0]
|
|
252
|
-
|
|
253
|
-
# 处理新文件
|
|
254
|
-
if not old_content:
|
|
255
|
-
temp_map[file_path] = new_content
|
|
256
|
-
modified_files.add(file_path)
|
|
257
|
-
continue
|
|
258
|
-
|
|
259
|
-
# 处理文件修改
|
|
260
|
-
if file_path not in temp_map:
|
|
261
|
-
error_info.append(f"文件不存在: {file_path}")
|
|
262
|
-
return False, "\n".join(error_info)
|
|
263
|
-
|
|
264
|
-
current_content = temp_map[file_path]
|
|
265
|
-
|
|
266
|
-
# 查找并替换代码块
|
|
267
|
-
if old_content not in current_content:
|
|
268
|
-
error_info.append(
|
|
269
|
-
f"补丁应用失败: {file_path}\n"
|
|
270
|
-
f"原因: 未找到要替换的代码\n"
|
|
271
|
-
f"期望找到的代码:\n{old_content}\n"
|
|
272
|
-
f"实际文件内容:\n{current_content[:200]}..." # 只显示前200个字符
|
|
273
|
-
)
|
|
274
|
-
return False, "\n".join(error_info)
|
|
275
|
-
|
|
276
|
-
# 应用更改
|
|
277
|
-
temp_map[file_path] = current_content.replace(old_content, new_content)
|
|
278
|
-
modified_files.add(file_path)
|
|
279
|
-
|
|
280
|
-
except Exception as e:
|
|
281
|
-
error_info.append(f"处理补丁时发生错误: {str(e)}")
|
|
282
|
-
return False, "\n".join(error_info)
|
|
283
|
-
|
|
284
|
-
# 所有补丁都应用成功,更新实际文件
|
|
285
|
-
for file_path in modified_files:
|
|
286
|
-
try:
|
|
287
|
-
dir_path = os.path.dirname(file_path)
|
|
288
|
-
if dir_path and not os.path.exists(dir_path):
|
|
289
|
-
os.makedirs(dir_path, exist_ok=True)
|
|
290
|
-
|
|
291
|
-
with open(file_path, "w", encoding="utf-8") as f:
|
|
292
|
-
f.write(temp_map[file_path])
|
|
293
|
-
|
|
294
|
-
PrettyOutput.print(f"成功修改文件: {file_path}", OutputType.SUCCESS)
|
|
295
|
-
|
|
296
|
-
except Exception as e:
|
|
297
|
-
error_info.append(f"写入文件失败 {file_path}: {str(e)}")
|
|
298
|
-
return False, "\n".join(error_info)
|
|
299
|
-
|
|
300
|
-
return True, ""
|
|
301
|
-
|
|
302
|
-
def _save_edit_record(self, commit_message: str, git_diff: str) -> None:
|
|
303
|
-
"""保存代码修改记录
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
commit_message: 提交信息
|
|
307
|
-
git_diff: git diff --cached的输出
|
|
308
|
-
"""
|
|
309
|
-
|
|
310
|
-
# 获取下一个序号
|
|
311
|
-
existing_records = [f for f in os.listdir(self.record_dir) if f.endswith('.yaml')]
|
|
312
|
-
next_num = 1
|
|
313
|
-
if existing_records:
|
|
314
|
-
last_num = max(int(f[:4]) for f in existing_records)
|
|
315
|
-
next_num = last_num + 1
|
|
316
|
-
|
|
317
|
-
# 创建记录文件
|
|
318
|
-
record = {
|
|
319
|
-
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
320
|
-
"commit_message": commit_message,
|
|
321
|
-
"git_diff": git_diff
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
record_path = os.path.join(self.record_dir, f"{next_num:04d}.yaml")
|
|
325
|
-
with open(record_path, "w", encoding="utf-8") as f:
|
|
326
|
-
yaml.safe_dump(record, f, allow_unicode=True)
|
|
327
|
-
|
|
328
|
-
PrettyOutput.print(f"已保存修改记录: {record_path}", OutputType.SUCCESS)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
101
|
def _prepare_execution(self) -> None:
|
|
334
102
|
"""准备执行环境"""
|
|
335
103
|
self.main_model = self._new_model()
|
|
336
104
|
self._codebase.generate_codebase()
|
|
337
105
|
|
|
338
|
-
|
|
339
106
|
def _load_related_files(self, feature: str) -> List[Dict]:
|
|
340
107
|
"""加载相关文件内容"""
|
|
341
108
|
ret = []
|
|
@@ -352,135 +119,6 @@ class OldModel:
|
|
|
352
119
|
ret.append({"file_path": file, "file_content": content})
|
|
353
120
|
return ret
|
|
354
121
|
|
|
355
|
-
def _handle_patch_application(self, related_files: List[Dict], patches: List[str], feature: str) -> Dict[str, Any]:
|
|
356
|
-
"""处理补丁应用流程"""
|
|
357
|
-
while True:
|
|
358
|
-
PrettyOutput.print(f"生成{len(patches)}个补丁", OutputType.INFO)
|
|
359
|
-
|
|
360
|
-
if not patches:
|
|
361
|
-
retry_prompt = f"""未生成补丁,请重新生成补丁"""
|
|
362
|
-
patches = self._remake_patch(retry_prompt)
|
|
363
|
-
continue
|
|
364
|
-
|
|
365
|
-
success, error_info = self._apply_patch(related_files, patches)
|
|
366
|
-
|
|
367
|
-
if success:
|
|
368
|
-
user_confirm = input("是否确认修改?(y/n)")
|
|
369
|
-
if user_confirm.lower() == "y":
|
|
370
|
-
self._finalize_changes(feature)
|
|
371
|
-
return {
|
|
372
|
-
"success": True,
|
|
373
|
-
"stdout": f"已完成功能开发{feature}",
|
|
374
|
-
"stderr": "",
|
|
375
|
-
"error": None
|
|
376
|
-
}
|
|
377
|
-
else:
|
|
378
|
-
self._revert_changes()
|
|
379
|
-
|
|
380
|
-
# 让用户输入调整意见
|
|
381
|
-
user_feedback = get_multiline_input("""
|
|
382
|
-
请提供修改建议,帮助生成更好的补丁:
|
|
383
|
-
1. 修改的位置是否正确?
|
|
384
|
-
2. 修改的内容是否合适?
|
|
385
|
-
3. 是否有遗漏的修改?
|
|
386
|
-
4. 其他调整建议?
|
|
387
|
-
|
|
388
|
-
请输入调整意见(直接回车跳过):""")
|
|
389
|
-
|
|
390
|
-
if not user_feedback:
|
|
391
|
-
return {
|
|
392
|
-
"success": False,
|
|
393
|
-
"stdout": "",
|
|
394
|
-
"stderr": "修改被用户取消,文件未发生任何变化",
|
|
395
|
-
"error": UserWarning("用户取消修改")
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
retry_prompt = f"""补丁被用户拒绝,请根据用户意见重新生成补丁:
|
|
399
|
-
|
|
400
|
-
用户意见:
|
|
401
|
-
{user_feedback}
|
|
402
|
-
|
|
403
|
-
请重新生成补丁,确保:
|
|
404
|
-
1. 按照用户意见调整修改内容
|
|
405
|
-
2. 准确定位要修改的代码位置
|
|
406
|
-
3. 正确处理代码缩进
|
|
407
|
-
4. 考虑代码上下文
|
|
408
|
-
"""
|
|
409
|
-
patches = self._remake_patch(retry_prompt)
|
|
410
|
-
continue
|
|
411
|
-
else:
|
|
412
|
-
PrettyOutput.print(f"补丁应用失败: {error_info}", OutputType.WARNING)
|
|
413
|
-
|
|
414
|
-
# 让用户输入补充信息
|
|
415
|
-
user_info = get_multiline_input("""
|
|
416
|
-
补丁应用失败。请提供更多信息来帮助修复问题:
|
|
417
|
-
1. 是否需要调整代码位置?
|
|
418
|
-
2. 是否有特殊的格式要求?
|
|
419
|
-
3. 是否需要考虑其他文件的依赖?
|
|
420
|
-
4. 其他补充说明?
|
|
421
|
-
|
|
422
|
-
请输入补充信息(直接回车跳过):""")
|
|
423
|
-
|
|
424
|
-
retry_prompt = f"""补丁应用失败,请根据以下信息重新生成补丁:
|
|
425
|
-
|
|
426
|
-
错误信息:
|
|
427
|
-
{error_info}
|
|
428
|
-
|
|
429
|
-
用户补充信息:
|
|
430
|
-
{user_info if user_info else "用户未提供补充信息"}
|
|
431
|
-
|
|
432
|
-
请确保:
|
|
433
|
-
1. 准确定位要修改的代码位置
|
|
434
|
-
2. 正确处理代码缩进
|
|
435
|
-
3. 考虑代码上下文
|
|
436
|
-
4. 对新文件不要包含原始内容
|
|
437
|
-
"""
|
|
438
|
-
patches = self._remake_patch(retry_prompt)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
def _generate_commit_message(self, git_diff: str, feature: str) -> str:
|
|
444
|
-
"""根据git diff和功能描述生成commit信息
|
|
445
|
-
|
|
446
|
-
Args:
|
|
447
|
-
git_diff: git diff --cached的输出
|
|
448
|
-
feature: 用户的功能描述
|
|
449
|
-
|
|
450
|
-
Returns:
|
|
451
|
-
str: 生成的commit信息
|
|
452
|
-
"""
|
|
453
|
-
|
|
454
|
-
# 生成提示词
|
|
455
|
-
prompt = f"""你是一个经验丰富的程序员,请根据以下代码变更和功能描述生成简洁明了的commit信息:
|
|
456
|
-
|
|
457
|
-
功能描述:
|
|
458
|
-
{feature}
|
|
459
|
-
|
|
460
|
-
代码变更:
|
|
461
|
-
"""
|
|
462
|
-
# 添加git diff内容
|
|
463
|
-
prompt += f"Git Diff:\n{git_diff}\n\n"
|
|
464
|
-
|
|
465
|
-
prompt += """
|
|
466
|
-
请遵循以下规则:
|
|
467
|
-
1. 使用英文编写
|
|
468
|
-
2. 采用常规的commit message格式:<type>(<scope>): <subject>
|
|
469
|
-
3. 保持简洁,不超过50个字符
|
|
470
|
-
4. 准确描述代码变更的主要内容
|
|
471
|
-
5. 优先考虑功能描述和git diff中的变更内容
|
|
472
|
-
"""
|
|
473
|
-
|
|
474
|
-
# 使用normal模型生成commit信息
|
|
475
|
-
model = PlatformRegistry().get_global_platform_registry().get_codegen_platform()
|
|
476
|
-
model.set_suppress_output(True)
|
|
477
|
-
success, response = self._call_model_with_retry(model, prompt)
|
|
478
|
-
if not success:
|
|
479
|
-
return "Update code changes"
|
|
480
|
-
|
|
481
|
-
# 清理响应内容
|
|
482
|
-
return response.strip().split("\n")[0]
|
|
483
|
-
|
|
484
122
|
def _finalize_changes(self, feature: str) -> None:
|
|
485
123
|
"""完成修改并提交"""
|
|
486
124
|
PrettyOutput.print("修改确认成功,提交修改", OutputType.INFO)
|
|
@@ -492,7 +130,7 @@ class OldModel:
|
|
|
492
130
|
git_diff = os.popen("git diff --cached").read()
|
|
493
131
|
|
|
494
132
|
# 自动生成commit信息,传入feature
|
|
495
|
-
commit_message =
|
|
133
|
+
commit_message = generate_commit_message(git_diff, feature)
|
|
496
134
|
|
|
497
135
|
# 显示并确认commit信息
|
|
498
136
|
PrettyOutput.print(f"自动生成的commit信息: {commit_message}", OutputType.INFO)
|
|
@@ -503,7 +141,7 @@ class OldModel:
|
|
|
503
141
|
|
|
504
142
|
# 不需要再次 git add,因为已经添加过了
|
|
505
143
|
os.system(f"git commit -m '{commit_message}'")
|
|
506
|
-
self.
|
|
144
|
+
save_edit_record(self.record_dir, commit_message, git_diff)
|
|
507
145
|
|
|
508
146
|
def _revert_changes(self) -> None:
|
|
509
147
|
"""回退所有修改"""
|
|
@@ -519,26 +157,36 @@ class OldModel:
|
|
|
519
157
|
|
|
520
158
|
Returns:
|
|
521
159
|
Dict[str, Any]: 包含执行结果的字典
|
|
522
|
-
- success: 是否成功
|
|
523
|
-
- stdout: 标准输出信息
|
|
524
|
-
- stderr: 错误信息
|
|
525
|
-
- error: 错误对象(如果有)
|
|
526
160
|
"""
|
|
527
161
|
try:
|
|
528
162
|
self._prepare_execution()
|
|
529
163
|
related_files = self._load_related_files(feature)
|
|
530
|
-
|
|
531
|
-
|
|
164
|
+
|
|
165
|
+
patch_handler = PatchHandler(self.main_model)
|
|
166
|
+
if patch_handler.handle_patch_application(related_files, feature):
|
|
167
|
+
self._finalize_changes(feature)
|
|
168
|
+
return {
|
|
169
|
+
"success": True,
|
|
170
|
+
"stdout": "代码修改成功",
|
|
171
|
+
"stderr": "",
|
|
172
|
+
}
|
|
173
|
+
else:
|
|
174
|
+
self._revert_changes()
|
|
175
|
+
return {
|
|
176
|
+
"success": False,
|
|
177
|
+
"stdout": "",
|
|
178
|
+
"stderr": "代码修改失败,请修改需求后重试",
|
|
179
|
+
}
|
|
532
180
|
|
|
533
181
|
except Exception as e:
|
|
182
|
+
self._revert_changes()
|
|
534
183
|
return {
|
|
535
184
|
"success": False,
|
|
536
185
|
"stdout": "",
|
|
537
|
-
"stderr": f"执行失败: {str(e)}",
|
|
186
|
+
"stderr": f"执行失败: {str(e)},请修改需求后重试",
|
|
538
187
|
"error": e
|
|
539
188
|
}
|
|
540
189
|
|
|
541
|
-
|
|
542
190
|
def main():
|
|
543
191
|
"""命令行入口"""
|
|
544
192
|
import argparse
|
|
@@ -550,7 +198,6 @@ def main():
|
|
|
550
198
|
parser.add_argument('-l', '--language', help='编程语言', default="python")
|
|
551
199
|
args = parser.parse_args()
|
|
552
200
|
|
|
553
|
-
|
|
554
201
|
tool = JarvisCoder(args.dir, args.language)
|
|
555
202
|
|
|
556
203
|
# 循环处理需求
|
|
@@ -569,16 +216,21 @@ def main():
|
|
|
569
216
|
if result["success"]:
|
|
570
217
|
PrettyOutput.print(result["stdout"], OutputType.SUCCESS)
|
|
571
218
|
else:
|
|
572
|
-
if result
|
|
219
|
+
if result.get("stderr"):
|
|
573
220
|
PrettyOutput.print(result["stderr"], OutputType.WARNING)
|
|
574
|
-
if result
|
|
575
|
-
|
|
221
|
+
if result.get("error"): # 使用 get() 方法避免 KeyError
|
|
222
|
+
error = result["error"]
|
|
223
|
+
PrettyOutput.print(f"错误类型: {type(error).__name__}", OutputType.WARNING)
|
|
224
|
+
PrettyOutput.print(f"错误信息: {str(error)}", OutputType.WARNING)
|
|
225
|
+
# 提示用户可以继续输入
|
|
226
|
+
PrettyOutput.print("\n您可以修改需求后重试", OutputType.INFO)
|
|
576
227
|
|
|
577
228
|
except KeyboardInterrupt:
|
|
578
229
|
print("\n用户中断执行")
|
|
579
230
|
break
|
|
580
231
|
except Exception as e:
|
|
581
232
|
PrettyOutput.print(f"执行出错: {str(e)}", OutputType.ERROR)
|
|
233
|
+
PrettyOutput.print("\n您可以修改需求后重试", OutputType.INFO)
|
|
582
234
|
continue
|
|
583
235
|
|
|
584
236
|
return 0
|
|
@@ -703,4 +355,4 @@ def get_multiline_input(prompt_text: str, root_dir: str = None) -> str:
|
|
|
703
355
|
except EOFError:
|
|
704
356
|
pass
|
|
705
357
|
|
|
706
|
-
return "\n".join(lines)
|
|
358
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
import time
|
|
3
|
+
from jarvis.models.base import BasePlatform
|
|
4
|
+
from jarvis.utils import OutputType, PrettyOutput
|
|
5
|
+
|
|
6
|
+
def call_model_with_retry(model: BasePlatform, prompt: str, max_retries: int = 3, initial_delay: float = 1.0) -> Tuple[bool, str]:
|
|
7
|
+
"""调用模型并支持重试
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
model: 模型实例
|
|
11
|
+
prompt: 提示词
|
|
12
|
+
max_retries: 最大重试次数
|
|
13
|
+
initial_delay: 初始延迟时间(秒)
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Tuple[bool, str]: (是否成功, 响应内容)
|
|
17
|
+
"""
|
|
18
|
+
delay = initial_delay
|
|
19
|
+
for attempt in range(max_retries):
|
|
20
|
+
try:
|
|
21
|
+
response = model.chat(prompt)
|
|
22
|
+
return True, response
|
|
23
|
+
except Exception as e:
|
|
24
|
+
if attempt == max_retries - 1: # 最后一次尝试
|
|
25
|
+
PrettyOutput.print(f"调用模型失败: {str(e)}", OutputType.ERROR)
|
|
26
|
+
return False, str(e)
|
|
27
|
+
|
|
28
|
+
PrettyOutput.print(f"调用模型失败,{delay}秒后重试: {str(e)}", OutputType.WARNING)
|
|
29
|
+
time.sleep(delay)
|
|
30
|
+
delay *= 2 # 指数退避
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
from typing import List, Tuple, Dict
|
|
4
|
+
from jarvis.utils import OutputType, PrettyOutput
|
|
5
|
+
from .model_utils import call_model_with_retry
|
|
6
|
+
|
|
7
|
+
class PatchHandler:
|
|
8
|
+
def __init__(self, model):
|
|
9
|
+
self.model = model
|
|
10
|
+
|
|
11
|
+
def _extract_patches(self, response: str) -> List[Tuple[str, str, str]]:
|
|
12
|
+
"""从响应中提取补丁
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
response: 模型响应内容
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
List[Tuple[str, str, str]]: 补丁列表,每个补丁是 (格式, 文件路径, 补丁内容) 的元组
|
|
19
|
+
"""
|
|
20
|
+
patches = []
|
|
21
|
+
|
|
22
|
+
# 匹配两种格式的补丁
|
|
23
|
+
fmt1_patches = re.finditer(r'<PATCH_FMT1>\n?(.*?)\n?</PATCH_FMT1>', response, re.DOTALL)
|
|
24
|
+
fmt2_patches = re.finditer(r'<PATCH_FMT2>\n?(.*?)\n?</PATCH_FMT2>', response, re.DOTALL)
|
|
25
|
+
|
|
26
|
+
# 处理 FMT1 格式的补丁
|
|
27
|
+
for match in fmt1_patches:
|
|
28
|
+
patch_content = match.group(1)
|
|
29
|
+
if not patch_content:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
# 提取文件路径和补丁内容
|
|
33
|
+
lines = patch_content.split('\n')
|
|
34
|
+
file_path_match = re.search(r'> (.*)', lines[0])
|
|
35
|
+
if not file_path_match:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
file_path = file_path_match.group(1).strip()
|
|
39
|
+
patch_content = '\n'.join(lines[1:])
|
|
40
|
+
patches.append(("FMT1", file_path, patch_content))
|
|
41
|
+
|
|
42
|
+
# 处理 FMT2 格式的补丁
|
|
43
|
+
for match in fmt2_patches:
|
|
44
|
+
patch_content = match.group(1)
|
|
45
|
+
if not patch_content:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# 提取文件路径和补丁内容
|
|
49
|
+
lines = patch_content.split('\n')
|
|
50
|
+
file_path_match = re.search(r'> (.*)', lines[0])
|
|
51
|
+
if not file_path_match:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
file_path = file_path_match.group(1).strip()
|
|
55
|
+
patch_content = '\n'.join(lines[1:])
|
|
56
|
+
patches.append(("FMT2", file_path, patch_content))
|
|
57
|
+
|
|
58
|
+
return patches
|
|
59
|
+
|
|
60
|
+
def make_patch(self, related_files: List[Dict], feature: str) -> List[Tuple[str, str, str]]:
|
|
61
|
+
"""生成修改方案"""
|
|
62
|
+
prompt = """你是一个资深程序员,请根据需求描述,修改文件内容。
|
|
63
|
+
|
|
64
|
+
修改格式说明:
|
|
65
|
+
1. 第一种格式 - 完整代码块替换:
|
|
66
|
+
<PATCH_FMT1>
|
|
67
|
+
> path/to/file
|
|
68
|
+
old_content
|
|
69
|
+
@@@@@@
|
|
70
|
+
new_content
|
|
71
|
+
</PATCH_FMT1>
|
|
72
|
+
|
|
73
|
+
例:
|
|
74
|
+
<PATCH_FMT1>
|
|
75
|
+
> src/main.py
|
|
76
|
+
def old_function():
|
|
77
|
+
print("old code")
|
|
78
|
+
return False
|
|
79
|
+
@@@@@@
|
|
80
|
+
def old_function():
|
|
81
|
+
print("new code")
|
|
82
|
+
return True
|
|
83
|
+
</PATCH_FMT1>
|
|
84
|
+
|
|
85
|
+
2. 第二种格式 - 通过首尾行定位要修改的代码范围:
|
|
86
|
+
<PATCH_FMT2>
|
|
87
|
+
> path/to/file
|
|
88
|
+
start_line_content
|
|
89
|
+
end_line_content
|
|
90
|
+
new_content
|
|
91
|
+
...
|
|
92
|
+
</PATCH_FMT2>
|
|
93
|
+
|
|
94
|
+
例:
|
|
95
|
+
<PATCH_FMT2>
|
|
96
|
+
> src/main.py
|
|
97
|
+
def old_function():
|
|
98
|
+
return False
|
|
99
|
+
def new_function():
|
|
100
|
+
print("new code")
|
|
101
|
+
return True
|
|
102
|
+
</PATCH_FMT2>
|
|
103
|
+
|
|
104
|
+
例子中 `def old_function():` 是首行内容,`return False` 是尾行内容,第三行开始是新的代码内容,将替换第一行到最后一行之间的所有内容
|
|
105
|
+
|
|
106
|
+
注意事项:
|
|
107
|
+
1、仅输出补丁内容,不要输出任何其他内容
|
|
108
|
+
2、如果在大段代码中有零星修改,生成多个补丁
|
|
109
|
+
3、要替换的内容,一定要与文件内容完全一致,不要有任何多余或者缺失的内容
|
|
110
|
+
4、每个patch不超过20行,超出20行,请生成多个patch
|
|
111
|
+
5、务必保留原始文件的缩进和格式
|
|
112
|
+
6、优先使用第二种格式(PATCH_FMT2),因为它更准确地定位要修改的代码范围
|
|
113
|
+
7、第二种格式(PATCH_FMT2)的前两行必须完全匹配文件中要修改的代码块的首尾行
|
|
114
|
+
8、如果第二种格式无法准确定位到要修改的代码(比如有重复的行),请使用第一种格式(PATCH_FMT1)
|
|
115
|
+
"""
|
|
116
|
+
# 添加文件内容到提示
|
|
117
|
+
for i, file in enumerate(related_files):
|
|
118
|
+
prompt += f"""\n{i}. {file["file_path"]}\n"""
|
|
119
|
+
prompt += f"""文件内容:\n"""
|
|
120
|
+
prompt += f"<FILE_CONTENT>\n"
|
|
121
|
+
prompt += f'{file["file_content"]}\n'
|
|
122
|
+
prompt += f"</FILE_CONTENT>\n"
|
|
123
|
+
|
|
124
|
+
prompt += f"\n需求描述: {feature}\n"
|
|
125
|
+
|
|
126
|
+
# 调用模型生成补丁
|
|
127
|
+
success, response = call_model_with_retry(self.model, prompt)
|
|
128
|
+
if not success:
|
|
129
|
+
PrettyOutput.print("生成补丁失败", OutputType.ERROR)
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
patches = self._extract_patches(response)
|
|
134
|
+
|
|
135
|
+
if not patches:
|
|
136
|
+
PrettyOutput.print("未生成任何有效补丁", OutputType.WARNING)
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
PrettyOutput.print(f"生成了 {len(patches)} 个补丁", OutputType.SUCCESS)
|
|
140
|
+
return patches
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.WARNING)
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
def apply_patch(self, related_files: List[Dict], patches: List[Tuple[str, str, str]]) -> Tuple[bool, str]:
|
|
147
|
+
"""应用补丁
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
related_files: 相关文件列表
|
|
151
|
+
patches: 补丁列表,每个补丁是 (格式, 文件路径, 补丁内容) 的元组
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Tuple[bool, str]: (是否成功, 错误信息)
|
|
155
|
+
"""
|
|
156
|
+
error_info = []
|
|
157
|
+
modified_files = set()
|
|
158
|
+
|
|
159
|
+
# 创建文件内容映射
|
|
160
|
+
file_map = {file["file_path"]: file["file_content"] for file in related_files}
|
|
161
|
+
temp_map = file_map.copy() # 创建临时映射用于尝试应用
|
|
162
|
+
|
|
163
|
+
# 尝试应用所有补丁
|
|
164
|
+
for i, (fmt, file_path, patch_content) in enumerate(patches):
|
|
165
|
+
PrettyOutput.print(f"正在应用补丁 {i+1}/{len(patches)}", OutputType.INFO)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
# 处理文件修改
|
|
169
|
+
if file_path not in temp_map:
|
|
170
|
+
error_info.append(f"文件不存在: {file_path}")
|
|
171
|
+
return False, "\n".join(error_info)
|
|
172
|
+
|
|
173
|
+
current_content = temp_map[file_path]
|
|
174
|
+
|
|
175
|
+
if fmt == "FMT1": # 完整代码块替换格式
|
|
176
|
+
parts = patch_content.split("@@@@@@")
|
|
177
|
+
if len(parts) != 2:
|
|
178
|
+
error_info.append(f"FMT1补丁格式错误: {file_path},缺少分隔符")
|
|
179
|
+
return False, "\n".join(error_info)
|
|
180
|
+
|
|
181
|
+
old_content, new_content = parts
|
|
182
|
+
|
|
183
|
+
# 处理新文件
|
|
184
|
+
if not old_content:
|
|
185
|
+
temp_map[file_path] = new_content
|
|
186
|
+
modified_files.add(file_path)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# 查找并替换代码块
|
|
190
|
+
if old_content not in current_content:
|
|
191
|
+
error_info.append(
|
|
192
|
+
f"补丁应用失败: {file_path}\n"
|
|
193
|
+
f"原因: 未找到要替换的代码\n"
|
|
194
|
+
f"期望找到的代码:\n{old_content}\n"
|
|
195
|
+
f"实际文件内容:\n{current_content[:200]}..."
|
|
196
|
+
)
|
|
197
|
+
return False, "\n".join(error_info)
|
|
198
|
+
|
|
199
|
+
# 应用更改
|
|
200
|
+
temp_map[file_path] = current_content.replace(old_content, new_content)
|
|
201
|
+
|
|
202
|
+
else: # FMT2 - 首尾行定位格式
|
|
203
|
+
lines = patch_content.splitlines()
|
|
204
|
+
if len(lines) < 3:
|
|
205
|
+
error_info.append(f"FMT2补丁格式错误: {file_path},行数不足")
|
|
206
|
+
return False, "\n".join(error_info)
|
|
207
|
+
|
|
208
|
+
first_line = lines[0]
|
|
209
|
+
last_line = lines[1]
|
|
210
|
+
new_content = '\n'.join(lines[2:])
|
|
211
|
+
|
|
212
|
+
# 在文件内容中定位要替换的区域
|
|
213
|
+
content_lines = current_content.splitlines()
|
|
214
|
+
start_idx = -1
|
|
215
|
+
end_idx = -1
|
|
216
|
+
|
|
217
|
+
# 查找匹配的起始行和结束行
|
|
218
|
+
for idx, line in enumerate(content_lines):
|
|
219
|
+
if line.rstrip() == first_line.rstrip():
|
|
220
|
+
start_idx = idx
|
|
221
|
+
if start_idx != -1 and line.rstrip() == last_line.rstrip():
|
|
222
|
+
end_idx = idx
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
if start_idx == -1 or end_idx == -1:
|
|
226
|
+
error_info.append(
|
|
227
|
+
f"补丁应用失败: {file_path}\n"
|
|
228
|
+
f"原因: 未找到匹配的代码范围\n"
|
|
229
|
+
f"起始行: {first_line}\n"
|
|
230
|
+
f"结束行: {last_line}"
|
|
231
|
+
)
|
|
232
|
+
return False, "\n".join(error_info)
|
|
233
|
+
|
|
234
|
+
# 替换内容
|
|
235
|
+
content_lines[start_idx:end_idx + 1] = new_content.splitlines()
|
|
236
|
+
temp_map[file_path] = "\n".join(content_lines)
|
|
237
|
+
|
|
238
|
+
modified_files.add(file_path)
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
error_info.append(f"处理补丁时发生错误: {str(e)}")
|
|
242
|
+
return False, "\n".join(error_info)
|
|
243
|
+
|
|
244
|
+
# 所有补丁都应用成功,更新实际文件
|
|
245
|
+
for file_path in modified_files:
|
|
246
|
+
try:
|
|
247
|
+
dir_path = os.path.dirname(file_path)
|
|
248
|
+
if dir_path and not os.path.exists(dir_path):
|
|
249
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
250
|
+
|
|
251
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
252
|
+
f.write(temp_map[file_path])
|
|
253
|
+
|
|
254
|
+
PrettyOutput.print(f"成功修改文件: {file_path}", OutputType.SUCCESS)
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
error_info.append(f"写入文件失败 {file_path}: {str(e)}")
|
|
258
|
+
return False, "\n".join(error_info)
|
|
259
|
+
|
|
260
|
+
return True, ""
|
|
261
|
+
|
|
262
|
+
def handle_patch_feedback(self, error_msg: str, feature: str) -> List[Tuple[str, str, str]]:
|
|
263
|
+
"""处理补丁应用失败的反馈
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
error_msg: 错误信息
|
|
267
|
+
feature: 功能描述
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List[Tuple[str, str, str]]: 新的补丁列表
|
|
271
|
+
"""
|
|
272
|
+
PrettyOutput.print("补丁应用失败,尝试重新生成", OutputType.WARNING)
|
|
273
|
+
|
|
274
|
+
# 获取用户补充信息
|
|
275
|
+
additional_info = input("\n请输入补充信息(直接回车跳过):")
|
|
276
|
+
PrettyOutput.print(f"开始重新生成补丁", OutputType.INFO)
|
|
277
|
+
|
|
278
|
+
# 构建重试提示
|
|
279
|
+
retry_prompt = f"""补丁应用失败,请根据以下信息重新生成补丁:
|
|
280
|
+
|
|
281
|
+
错误信息:
|
|
282
|
+
{error_msg}
|
|
283
|
+
|
|
284
|
+
原始需求:
|
|
285
|
+
{feature}
|
|
286
|
+
|
|
287
|
+
用户补充信息:
|
|
288
|
+
{additional_info}
|
|
289
|
+
|
|
290
|
+
请重新生成补丁,确保:
|
|
291
|
+
1. 代码匹配完全准确
|
|
292
|
+
2. 保持正确的缩进和格式
|
|
293
|
+
3. 避免之前的错误
|
|
294
|
+
"""
|
|
295
|
+
success, response = call_model_with_retry(self.model, retry_prompt)
|
|
296
|
+
if not success:
|
|
297
|
+
return []
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
patches = self._extract_patches(response)
|
|
301
|
+
return patches
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.WARNING)
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
def monitor_patch_result(self, success: bool, error_msg: str) -> bool:
|
|
308
|
+
"""监控补丁应用结果
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
success: 是否成功
|
|
312
|
+
error_msg: 错误信息
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
bool: 是否继续尝试
|
|
316
|
+
"""
|
|
317
|
+
if success:
|
|
318
|
+
PrettyOutput.print("补丁应用成功", OutputType.SUCCESS)
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
PrettyOutput.print(f"补丁应用失败: {error_msg}", OutputType.ERROR)
|
|
322
|
+
|
|
323
|
+
# 询问是否继续尝试
|
|
324
|
+
retry = input("\n是否重新尝试?(y/n) [y]: ").lower() or "y"
|
|
325
|
+
return retry == "y"
|
|
326
|
+
|
|
327
|
+
def handle_patch_application(self, related_files: List[Dict], feature: str) -> bool:
|
|
328
|
+
"""处理补丁应用流程
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
related_files: 相关文件列表
|
|
332
|
+
feature: 功能描述
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
bool: 是否成功应用补丁
|
|
336
|
+
"""
|
|
337
|
+
max_attempts = 3
|
|
338
|
+
attempt = 0
|
|
339
|
+
|
|
340
|
+
while attempt < max_attempts:
|
|
341
|
+
attempt += 1
|
|
342
|
+
|
|
343
|
+
while True: # 在当前尝试中循环,直到成功或用户放弃
|
|
344
|
+
# 1. 生成补丁
|
|
345
|
+
patches = self.make_patch(related_files, feature)
|
|
346
|
+
if not patches:
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
# 2. 显示补丁内容
|
|
350
|
+
PrettyOutput.print("\n将要应用以下补丁:", OutputType.INFO)
|
|
351
|
+
for fmt, file_path, patch_content in patches:
|
|
352
|
+
PrettyOutput.print(f"\n文件: {file_path}", OutputType.INFO)
|
|
353
|
+
PrettyOutput.print(f"格式: {fmt}", OutputType.INFO)
|
|
354
|
+
PrettyOutput.print("补丁内容:", OutputType.INFO)
|
|
355
|
+
print(patch_content)
|
|
356
|
+
|
|
357
|
+
# 3. 应用补丁
|
|
358
|
+
success, error_msg = self.apply_patch(related_files, patches)
|
|
359
|
+
if not success:
|
|
360
|
+
# 4. 如果应用失败,询问是否重试
|
|
361
|
+
should_retry = self.monitor_patch_result(success, error_msg)
|
|
362
|
+
if not should_retry:
|
|
363
|
+
break # 退出内层循环,尝试下一次完整的迭代
|
|
364
|
+
|
|
365
|
+
# 5. 处理失败反馈
|
|
366
|
+
patches = self.handle_patch_feedback(error_msg, feature)
|
|
367
|
+
if not patches:
|
|
368
|
+
return False
|
|
369
|
+
continue # 继续当前迭代
|
|
370
|
+
|
|
371
|
+
# 6. 应用成功,让用户确认修改
|
|
372
|
+
PrettyOutput.print("\n补丁已应用,请检查修改效果。", OutputType.SUCCESS)
|
|
373
|
+
confirm = input("\n是否保留这些修改?(y/n) [y]: ").lower() or "y"
|
|
374
|
+
if confirm != "y":
|
|
375
|
+
PrettyOutput.print("用户取消修改,正在回退", OutputType.WARNING)
|
|
376
|
+
os.system("git reset --hard") # 回退所有修改
|
|
377
|
+
|
|
378
|
+
# 询问是否要在当前迭代中重试
|
|
379
|
+
retry = input("\n是否要重新生成补丁?(y/n) [y]: ").lower() or "y"
|
|
380
|
+
if retry != "y":
|
|
381
|
+
break # 退出内层循环,尝试下一次完整的迭代
|
|
382
|
+
continue # 继续当前迭代
|
|
383
|
+
|
|
384
|
+
return True # 用户确认修改,返回成功
|
|
385
|
+
|
|
386
|
+
# 如果内层循环正常退出(非return),继续外层循环
|
|
387
|
+
continue
|
|
388
|
+
|
|
389
|
+
PrettyOutput.print(f"达到最大重试次数 ({max_attempts})", OutputType.WARNING)
|
|
390
|
+
return False
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
jarvis/__init__.py,sha256=
|
|
1
|
+
jarvis/__init__.py,sha256=_3MqMWMf232eVTukktJrSn128MkK9jyds0HMhYHti94,50
|
|
2
2
|
jarvis/agent.py,sha256=_qh4mSojAgClOEz5pTiRIfRJU-5_3QGzBAU09roCjtk,19095
|
|
3
3
|
jarvis/main.py,sha256=72od8757A3bhe0ncE38S7u-YZsAh0y50w9ozMhgqIU8,5423
|
|
4
4
|
jarvis/utils.py,sha256=Y5zig7AgIzdWHF31qHaMUziezythfjVKjxFRtMzd1m4,10357
|
|
5
5
|
jarvis/jarvis_codebase/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
jarvis/jarvis_codebase/main.py,sha256=hYdDwREfFW5KPbIbLZ75MFCth6VqrwxSeNfXiQavdzE,26473
|
|
7
7
|
jarvis/jarvis_coder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
jarvis/jarvis_coder/
|
|
8
|
+
jarvis/jarvis_coder/git_utils.py,sha256=tJ25kIzglGaPBqu42rZZSsXk0tpOFTiaG8q-bq4CSF0,2343
|
|
9
|
+
jarvis/jarvis_coder/main.py,sha256=5uxexKziA5kMZLNkGPEzz8hKQvarKVflNqRcXZVWnT8,13406
|
|
10
|
+
jarvis/jarvis_coder/model_utils.py,sha256=XXg5ZPlgRsq9K6iJb4vPoZqSiAJbAUIZffmgaLFnLCw,1104
|
|
11
|
+
jarvis/jarvis_coder/patch_handler.py,sha256=U8I19Aq2U3LAU3FE94C3tlFQNtGYA_luwnBU7OnX7pw,15253
|
|
9
12
|
jarvis/jarvis_rag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
13
|
jarvis/jarvis_rag/main.py,sha256=a8TtPVCh5Xd6W1AaRFGeXvU_1hEnHQdoMElxnMuq0ew,24773
|
|
11
14
|
jarvis/jarvis_smart_shell/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -32,9 +35,9 @@ jarvis/tools/search.py,sha256=c9dXtyICdl8Lm8shNPNyIx9k67uY0rMF8xnIKu2RsnE,8787
|
|
|
32
35
|
jarvis/tools/shell.py,sha256=UPKshPyOaUwTngresUw-ot1jHjQIb4wCY5nkJqa38lU,2520
|
|
33
36
|
jarvis/tools/sub_agent.py,sha256=rEtAmSVY2ZjFOZEKr5m5wpACOQIiM9Zr_3dT92FhXYU,2621
|
|
34
37
|
jarvis/tools/webpage.py,sha256=d3w3Jcjcu1ESciezTkz3n3Zf-rp_l91PrVoDEZnckOo,2391
|
|
35
|
-
jarvis_ai_assistant-0.1.
|
|
36
|
-
jarvis_ai_assistant-0.1.
|
|
37
|
-
jarvis_ai_assistant-0.1.
|
|
38
|
-
jarvis_ai_assistant-0.1.
|
|
39
|
-
jarvis_ai_assistant-0.1.
|
|
40
|
-
jarvis_ai_assistant-0.1.
|
|
38
|
+
jarvis_ai_assistant-0.1.89.dist-info/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
|
|
39
|
+
jarvis_ai_assistant-0.1.89.dist-info/METADATA,sha256=aG0WNMCBnK4O5fPKrNiq7Nj3wlsU5mx2xy1tQUSlmAU,12589
|
|
40
|
+
jarvis_ai_assistant-0.1.89.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
41
|
+
jarvis_ai_assistant-0.1.89.dist-info/entry_points.txt,sha256=sdmIO86MrIUepJTGyHs0i_Ho9VGf1q9YRP4RgQvGWcI,280
|
|
42
|
+
jarvis_ai_assistant-0.1.89.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
|
|
43
|
+
jarvis_ai_assistant-0.1.89.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{jarvis_ai_assistant-0.1.88.dist-info → jarvis_ai_assistant-0.1.89.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|