jarvis-ai-assistant 0.1.53__py3-none-any.whl → 0.1.56__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/tools/code_edit.py DELETED
@@ -1,729 +0,0 @@
1
- import hashlib
2
- import os
3
- import re
4
- import sqlite3
5
- import time
6
- from typing import Dict, Any, List, Optional, Tuple
7
-
8
- import yaml
9
- from jarvis.models.base import BasePlatform
10
- from jarvis.utils import OutputType, PrettyOutput, get_multiline_input, load_env_from_file
11
- from jarvis.models.registry import PlatformRegistry
12
-
13
-
14
- class CodeEditTool:
15
- """代码修改工具"""
16
-
17
- name = "code_edit"
18
- description = "根据需求描述修改代码文件"
19
- parameters = {
20
- "type": "object",
21
- "properties": {
22
- "feature": {
23
- "type": "string",
24
- "description": "要实现的功能描述"
25
- },
26
- "root_dir": {
27
- "type": "string",
28
- "description": "代码库根目录"
29
- },
30
- "language": {
31
- "type": "string",
32
- "description": "编程语言"
33
- }
34
- },
35
- "required": ["feature", "root_dir", "language"]
36
- }
37
-
38
- def __init__(self):
39
- """初始化代码修改工具"""
40
- self.main_model = self._new_model()
41
- self.language_extensions = {
42
- "c": {".c", ".h"},
43
- "cpp": {".cpp", ".hpp", ".h"},
44
- "python": {".py", ".pyw"},
45
- "java": {".java"},
46
- "go": {".go"},
47
- "rust": {".rs"},
48
- "javascript": {".js"},
49
- "typescript": {".ts"},
50
- "php": {".php"},
51
- "ruby": {".rb"},
52
- "swift": {".swift"},
53
- "kotlin": {".kt"},
54
- "scala": {".scala"},
55
- "haskell": {".hs"},
56
- "erlang": {".erl"},
57
- "elixir": {".ex"},
58
- }
59
- self.db_path = ""
60
- self.feature = ""
61
- self.root_dir = ""
62
- self.language = ""
63
- self.current_dir = ""
64
-
65
- def _new_model(self):
66
- """获取大模型"""
67
- platform_name = os.environ.get(
68
- "JARVIS_CODEGEN_PLATFORM", PlatformRegistry.global_platform_name)
69
- model_name = os.environ.get("JARVIS_CODEGEN_MODEL")
70
- model = PlatformRegistry().get_global_platform_registry().create_platform(platform_name)
71
- if model_name:
72
- model.set_model_name(model_name)
73
- return model
74
-
75
- def _has_uncommitted_files(self, root_dir: str) -> bool:
76
- """判断代码库是否有未提交的文件"""
77
- os.chdir(root_dir)
78
- return os.system(f"git status | grep 'Changes not staged for commit'")
79
-
80
- def _call_model_with_retry(self, model: BasePlatform, prompt: str, max_retries: int = 3, initial_delay: float = 1.0) -> Tuple[bool, str]:
81
- """调用模型并支持重试
82
-
83
- Args:
84
- prompt: 提示词
85
- max_retries: 最大重试次数
86
- initial_delay: 初始延迟时间(秒)
87
-
88
- Returns:
89
- Tuple[bool, str]: (是否成功, 响应内容)
90
- """
91
- delay = initial_delay
92
- for attempt in range(max_retries):
93
- try:
94
- response = model.chat(prompt)
95
- return True, response
96
- except Exception as e:
97
- if attempt == max_retries - 1: # 最后一次尝试
98
- PrettyOutput.print(f"调用模型失败: {str(e)}", OutputType.ERROR)
99
- return False, str(e)
100
-
101
- PrettyOutput.print(f"调用模型失败,{delay}秒后重试: {str(e)}", OutputType.WARNING)
102
- time.sleep(delay)
103
- delay *= 2 # 指数退避
104
-
105
- def _get_key_info(self, file_path: str, content: str) -> Optional[Dict[str, Any]]:
106
- """获取文件的关键信息
107
-
108
- Args:
109
- file_path: 文件路径
110
- content: 文件内容
111
-
112
- Returns:
113
- Optional[Dict[str, Any]]: 文件信息,包含文件描述
114
- """
115
- model = self._new_model() # 创建新的模型实例
116
-
117
- prompt = f"""你是一个资深程序员,请根据文件内容,生成文件的关键信息,要求如下,除了代码,不要输出任何内容:
118
-
119
- 1. 文件路径: {file_path}
120
- 2. 文件内容:(<CONTENT_START>和<CONTENT_END>之间的部分)
121
- <CONTENT_START>
122
- {content}
123
- <CONTENT_END>
124
- 3. 关键信息: 请生成文件的功能描述,使用标准的yaml格式描述,仅输出以下格式内容,如果目标文件不是代码文件,输出(无)
125
- <FILE_INFO_START>
126
- file_description: 这个文件的主要功能和作用描述
127
- <FILE_INFO_END>
128
- """
129
- try:
130
- response = model.chat(prompt)
131
- model.delete_chat() # 删除会话历史
132
-
133
- response = response.replace("<FILE_INFO_START>", "").replace("<FILE_INFO_END>", "")
134
- return yaml.safe_load(response)
135
- except Exception as e:
136
- PrettyOutput.print(f"解析文件信息失败: {str(e)}", OutputType.ERROR)
137
- return None
138
- finally:
139
- # 确保清理模型资源
140
- try:
141
- model.delete_chat()
142
- except:
143
- pass
144
-
145
- def _get_file_extensions(self, language: str) -> List[str]:
146
- """获取文件扩展名"""
147
- return self.language_extensions.get(language, [])
148
-
149
- def _get_file_md5(self, file_path: str) -> str:
150
- """获取文件MD5"""
151
- return hashlib.md5(open(file_path, "rb").read()).hexdigest()
152
-
153
- def _create_index_db(self):
154
- """创建索引数据库"""
155
- index_db_path = os.path.join(self.root_dir, ".index.db")
156
- if not os.path.exists(index_db_path):
157
- PrettyOutput.print("Index database does not exist, creating...", OutputType.INFO)
158
- index_db = sqlite3.connect(index_db_path)
159
- index_db.execute(
160
- "CREATE TABLE files (file_path TEXT PRIMARY KEY, file_md5 TEXT, file_description TEXT)")
161
- index_db.commit()
162
- index_db.close()
163
- PrettyOutput.print("Index database created", OutputType.SUCCESS)
164
- # 将.index.db文件添加到gitignore
165
- with open(os.path.join(self.root_dir, ".gitignore"), "a") as f:
166
- f.write("\n.index.db\n")
167
- PrettyOutput.print("Index database added to gitignore", OutputType.SUCCESS)
168
- # commit
169
- os.chdir(self.root_dir)
170
- os.system(f"git add .gitignore -f")
171
- os.system(f"git commit -m 'add index database'")
172
- os.chdir(self.current_dir)
173
-
174
- def _find_file_by_md5(self, index_db_path: str, file_md5: str) -> Optional[str]:
175
- """根据文件MD5查找文件路径"""
176
- index_db = sqlite3.connect(index_db_path)
177
- cursor = index_db.cursor()
178
- cursor.execute(
179
- "SELECT file_path FROM files WHERE file_md5 = ?", (file_md5,))
180
- result = cursor.fetchone()
181
- index_db.close()
182
- return result[0] if result else None
183
-
184
- def _update_file_path(self, index_db_path: str, file_path: str, file_md5: str):
185
- """更新文件路径"""
186
- index_db = sqlite3.connect(index_db_path)
187
- cursor = index_db.cursor()
188
- cursor.execute(
189
- "UPDATE files SET file_path = ? WHERE file_md5 = ?", (file_path, file_md5))
190
- index_db.commit()
191
- index_db.close()
192
-
193
- def _insert_info(self, index_db_path: str, file_path: str, file_md5: str, file_description: str):
194
- """插入文件信息"""
195
- index_db = sqlite3.connect(index_db_path)
196
- cursor = index_db.cursor()
197
- cursor.execute("DELETE FROM files WHERE file_path = ?", (file_path,))
198
- cursor.execute("INSERT INTO files (file_path, file_md5, file_description) VALUES (?, ?, ?)",
199
- (file_path, file_md5, file_description))
200
- index_db.commit()
201
- index_db.close()
202
-
203
- def _index_project(self, language: str):
204
- """建立代码库索引"""
205
- # 1. 创建索引数据库,位于root_dir/.index.db
206
- index_db_path = os.path.join(self.root_dir, ".index.db")
207
- self.db_path = index_db_path
208
- if not os.path.exists(index_db_path):
209
- self._create_index_db()
210
-
211
- # 2. 遍历文件
212
- for root, dirs, files in os.walk(self.root_dir):
213
- for file in files:
214
- if os.path.splitext(file)[1] in self._get_file_extensions(language):
215
- # 计算文件MD5
216
- file_path = os.path.join(root, file)
217
- file_md5 = self._get_file_md5(file_path)
218
-
219
- # 查找文件
220
- file_path_in_db = self._find_file_by_md5(
221
- self.db_path, file_md5)
222
- if file_path_in_db:
223
- PrettyOutput.print(
224
- f"File {file_path} is duplicate, skip", OutputType.INFO)
225
- if file_path_in_db != file_path:
226
- self._update_file_path(
227
- self.db_path, file_path, file_md5)
228
- PrettyOutput.print(
229
- f"File {file_path} is duplicate, update path to {file_path}", OutputType.INFO)
230
- continue
231
-
232
- with open(file_path, "r", encoding="utf-8") as f:
233
- file_content = f.read()
234
- key_info = self._get_key_info(file_path, file_content)
235
- if not key_info:
236
- PrettyOutput.print(
237
- f"File {file_path} index failed", OutputType.INFO)
238
- continue
239
- if "file_description" in key_info:
240
- self._insert_info(
241
- self.db_path, file_path, file_md5, key_info["file_description"])
242
- PrettyOutput.print(
243
- f"File {file_path} is indexed", OutputType.INFO)
244
- else:
245
- PrettyOutput.print(
246
- f"File {file_path} is not a code file, skip", OutputType.INFO)
247
- PrettyOutput.print("Index project finished", OutputType.INFO)
248
-
249
- def _find_related_files(self, feature: str) -> List[Dict]:
250
- """根据需求描述,查找相关文件"""
251
- score = [[], [], [], [], [], [], [], [], [], []]
252
- step = 5
253
- offset = 0
254
-
255
- while True:
256
- try:
257
- index_db = sqlite3.connect(self.db_path)
258
- cursor = index_db.cursor()
259
- cursor.execute(f"SELECT file_path, file_description FROM files LIMIT {step} OFFSET {offset}")
260
- result = cursor.fetchall()
261
- index_db.close()
262
-
263
- if not result:
264
- break
265
-
266
- offset += len(result)
267
- prompt = "你是资深程序员,请根据需求描述,分析文件的相关性,文件列表如下:\n"
268
- prompt += "<FILE_LIST_START>\n"
269
- for i, file_path in enumerate(result):
270
- prompt += f"""{i}. {file_path[0]} : {file_path[1]}\n"""
271
- prompt += f"""需求描述: {feature}\n"""
272
- prompt += "<FILE_LIST_END>\n"
273
- prompt += "请根据需求描述和文件描述,分析文件的相关性,输出每个编号的相关性[0~9],仅输出以下格式内容(key为文件编号,value为相关性)\n"
274
- prompt += "<FILE_RELATION_START>\n"
275
- prompt += '''"0": 5'''
276
- prompt += '''"1": 3'''
277
- prompt += "<FILE_RELATION_END>\n"
278
-
279
- success, response = self._call_model_with_retry(self._new_model(), prompt)
280
- if not success:
281
- continue
282
-
283
- try:
284
- response = response.replace("<FILE_RELATION_START>", "").replace("<FILE_RELATION_END>", "")
285
- file_relation = yaml.safe_load(response)
286
- if not file_relation:
287
- PrettyOutput.print("响应格式错误", OutputType.WARNING)
288
- continue
289
-
290
- for file_id, relation in file_relation.items():
291
- id = int(file_id)
292
- relation = max(0, min(9, relation)) # 确保范围在0-9之间
293
- score[relation].append({
294
- "file_path": result[id][0],
295
- "file_description": result[id][1]
296
- })
297
-
298
- except Exception as e:
299
- PrettyOutput.print(f"处理文件关系失败: {str(e)}", OutputType.ERROR)
300
- continue
301
-
302
- except sqlite3.Error as e:
303
- PrettyOutput.print(f"数据库操作失败: {str(e)}", OutputType.ERROR)
304
- break
305
- except Exception as e:
306
- PrettyOutput.print(f"查找相关文件失败: {str(e)}", OutputType.ERROR)
307
- break
308
-
309
- files = []
310
- score.reverse()
311
- for i in score:
312
- files.extend(i)
313
- if len(files) >= 10: # 先获取最多10个相关文件
314
- break
315
-
316
- # 如果找到超过3个相关文件,再次筛选
317
- if len(files) > 3:
318
- prompt = "你是资深程序员,请从以下文件中挑选出与需求最相关的3个文件:\n"
319
- prompt += "<FILE_LIST_START>\n"
320
- for i, file in enumerate(files):
321
- prompt += f"""{i}. {file["file_path"]} : {file["file_description"]}\n"""
322
- prompt += f"""需求描述: {feature}\n"""
323
- prompt += "<FILE_LIST_END>\n"
324
- prompt += "请输出最相关的3个文件的编号,用逗号分隔,仅输出以下格式内容\n"
325
- prompt += "<TOP3_START>\n"
326
- prompt += "0,2,5"
327
- prompt += "<TOP3_END>\n"
328
-
329
- success, response = self._call_model_with_retry(self._new_model(), prompt)
330
- if success:
331
- try:
332
- response = response.replace("<TOP3_START>", "").replace("<TOP3_END>", "")
333
- top3_ids = [int(id.strip()) for id in response.split(",")]
334
- files = [files[i] for i in top3_ids if i < len(files)]
335
- except Exception as e:
336
- PrettyOutput.print(f"解析TOP3文件失败: {str(e)}", OutputType.ERROR)
337
- files = files[:3] # 如果解析失败,取前3个文件
338
- else:
339
- files = files[:3] # 如果模型调用失败,取前3个文件
340
- else:
341
- files = files[:3] # 如果文件数不足3个,取所有文件
342
-
343
- return files
344
-
345
- def _remake_patch(self, prompt: str) -> List[str]:
346
- success, response = self._call_model_with_retry(self.main_model, prompt, max_retries=5) # 增加重试次数
347
- if not success:
348
- return []
349
-
350
- try:
351
- patches = re.findall(r">>>>>>.*?<<<<<<", response, re.DOTALL)
352
- return patches
353
- except Exception as e:
354
- PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.ERROR)
355
- return []
356
-
357
- def _make_patch(self, related_files: List[Dict]) -> List[str]:
358
- """生成修改方案"""
359
- prompt = "你是一个资深程序员,请根据需求描述,修改文件内容,文件列表如下:\n"
360
- prompt += "<FILE_RELATION_START>\n"
361
- for i, file in enumerate(related_files):
362
- prompt += f"""{i}. {file["file_path"]} : {file["file_description"]}\n"""
363
- prompt += f"""文件内容: \n"""
364
- prompt += f"<FILE_CONTENT_START>\n"
365
- prompt += f'{file["file_content"]}\n'
366
- prompt += f"<FILE_CONTENT_END>\n"
367
- prompt += f"<FILE_RELATION_END>\n"
368
- prompt += f"请根据需求描述,修改文件。\n"
369
- prompt += f"需求描述: {self.feature}\n"
370
- prompt += f"请输出以下格式内容(多段patch),注意缩进也是代码的一部分,不要输出任何其他内容\n"
371
- prompt += f">>>>>> [文件路径1]\n"
372
- prompt += f"[原文件内容]\n"
373
- prompt += f"==========\n"
374
- prompt += f"[修改后的文件内容]\n"
375
- prompt += f"<<<<<<\n"
376
- prompt += f"如果文件不存在,请创建新文件,不要包含原始内容,如下:\n"
377
- prompt += f">>>>>> [文件路径1]\n"
378
- prompt += f"==========\n"
379
- prompt += f"[新文件内容]\n"
380
- prompt += f"<<<<<<\n"
381
- prompt += f"生成最小化的patch,原文件内容与修改后文件内容不要有大量重复内容。"
382
- prompt += f"如果一个文件有多处需要修改,请生成多个patch,不要生成一个patch包含多处修改。"
383
-
384
- success, response = self._call_model_with_retry(self.main_model, prompt, max_retries=5) # 增加重试次数
385
- if not success:
386
- return []
387
-
388
- try:
389
- patches = re.findall(r">>>>>>.*?<<<<<<", response, re.DOTALL)
390
- return patches
391
- except Exception as e:
392
- PrettyOutput.print(f"解析patch失败: {str(e)}", OutputType.ERROR)
393
- return []
394
-
395
- def _apply_patch(self, related_files: List[Dict], patches: List[str]) -> Tuple[bool, str]:
396
- """应用补丁
397
-
398
- Args:
399
- related_files: 相关文件列表
400
- patches: 补丁列表
401
-
402
- Returns:
403
- Tuple[bool, str]: (是否成功, 错误信息)
404
- """
405
- # 创建文件内容映射
406
- file_map = {file["file_path"]: file["file_content"] for file in related_files}
407
- temp_map = file_map.copy() # 创建临时映射用于尝试应用
408
-
409
- error_info = []
410
-
411
- modified_files = set() # 记录修改的文件
412
-
413
- # 尝试应用所有补丁
414
- for i, patch in enumerate(patches):
415
- PrettyOutput.print(f"正在应用补丁 {i+1}/{len(patches)}", OutputType.INFO)
416
- patch_lines = patch.split("\n")
417
- file_name = ""
418
- old_code = []
419
- new_code = []
420
- old_code_flag = False
421
- new_code_flag = False
422
-
423
- # 解析补丁内容
424
- for line in patch_lines:
425
- if line.startswith(">>>>>>"):
426
- old_code_flag = True
427
- file_name = line.split(" ")[1]
428
- elif line.startswith("=========="):
429
- old_code_flag = False
430
- new_code_flag = True
431
- elif line.startswith("<<<<<<"):
432
- new_code_flag = False
433
- elif old_code_flag:
434
- old_code.append(line)
435
- elif new_code_flag:
436
- new_code.append(line)
437
-
438
- # 处理新文件的情况
439
- if file_name not in temp_map:
440
- PrettyOutput.print(f"创建新文件: {file_name}", OutputType.WARNING)
441
- if old_code: # 如果是新文件但有原始内容,这是错误的
442
- error_info.append(f"文件 {file_name} 不存在,但补丁包含原始内容")
443
- return False, "\n".join(error_info)
444
-
445
- temp_map[file_name] = "\n".join(new_code)
446
- modified_files.add(file_name) # 记录新文件
447
- continue
448
-
449
- # 应用补丁到现有文件
450
- old_content = "\n".join(old_code)
451
- new_content = "\n".join(new_code)
452
-
453
- if old_content not in temp_map[file_name]:
454
- error_info.append(
455
- f"补丁应用失败: {file_name}\n"
456
- f"原因: 未找到要替换的代码\n"
457
- f"期望找到的代码:\n{old_content}\n"
458
- f"实际文件内容:\n{temp_map[file_name][:200]}..." # 只显示前200个字符
459
- )
460
- return False, "\n".join(error_info)
461
-
462
- # 应用更改到临时映射
463
- temp_map[file_name] = temp_map[file_name].replace(old_content, new_content)
464
- modified_files.add(file_name) # 记录修改的文件
465
-
466
- # 所有补丁都应用成功,更新实际文件
467
- for file_path in modified_files:
468
- try:
469
- dir = os.path.dirname(file_path)
470
- if dir and not os.path.exists(dir):
471
- os.makedirs(dir, exist_ok=True)
472
- PrettyOutput.print(f"更新文件: {file_path}", OutputType.INFO)
473
- with open(file_path, "w", encoding="utf-8") as f:
474
- f.write(temp_map[file_path])
475
- PrettyOutput.print(f"成功更新文件: {file_path}", OutputType.SUCCESS)
476
- except Exception as e:
477
- error_info.append(f"写入文件失败 {file_path}: {str(e)}")
478
- return False, "\n".join(error_info)
479
-
480
- return True, ""
481
-
482
- def _save_edit_record(self, feature: str, patches: List[str]) -> None:
483
- """保存代码修改记录
484
-
485
- Args:
486
- feature: 需求描述
487
- patches: 补丁列表
488
- """
489
- # 创建记录目录
490
- record_dir = os.path.join(self.root_dir, ".jarvis_code_edit")
491
- os.makedirs(record_dir, exist_ok=True)
492
-
493
- # 添加到 .gitignore
494
- gitignore_path = os.path.join(self.root_dir, ".gitignore")
495
- if os.path.exists(gitignore_path):
496
- with open(gitignore_path, "r") as f:
497
- if ".jarvis_code_edit" not in f.read():
498
- with open(gitignore_path, "a") as f:
499
- f.write("\n.jarvis_code_edit/\n")
500
- else:
501
- with open(gitignore_path, "w") as f:
502
- f.write(".jarvis_code_edit/\n")
503
-
504
- # 获取下一个序号
505
- existing_records = [f for f in os.listdir(record_dir) if f.endswith('.yaml')]
506
- next_num = 1
507
- if existing_records:
508
- last_num = max(int(f[:4]) for f in existing_records)
509
- next_num = last_num + 1
510
-
511
- # 创建记录文件
512
- record = {
513
- "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
514
- "feature": feature,
515
- "patches": patches
516
- }
517
-
518
- record_path = os.path.join(record_dir, f"{next_num:04d}.yaml")
519
- with open(record_path, "w", encoding="utf-8") as f:
520
- yaml.safe_dump(record, f, allow_unicode=True)
521
-
522
- PrettyOutput.print(f"已保存修改记录: {record_path}", OutputType.SUCCESS)
523
-
524
- def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
525
- """执行代码修改
526
-
527
- Args:
528
- args: 包含操作参数的字典
529
- - feature: 要实现的功能描述
530
- - root_dir: 代码库根目录
531
- - language: 编程语言
532
-
533
- Returns:
534
- Dict[str, Any]: 包含执行结果的字典
535
- - success: 是否成功
536
- - stdout: 标准输出信息
537
- - stderr: 错误信息
538
- - error: 错误对象(如果有)
539
- """
540
- try:
541
- self.feature = args["feature"]
542
- self.root_dir = args["root_dir"]
543
- self.language = args["language"]
544
- self.current_dir = os.getcwd()
545
-
546
- # 0. 判断语言是否支持
547
- if not self._get_file_extensions(self.language):
548
- return {
549
- "success": False,
550
- "stdout": "",
551
- "stderr": "不支持的编程语言",
552
- "error": ValueError("不支持的编程语言")
553
- }
554
-
555
- # 1. 判断代码库路径是否存在,如果不存在,创建
556
- if not os.path.exists(self.root_dir):
557
- PrettyOutput.print(
558
- "Root directory does not exist, creating...", OutputType.INFO)
559
- os.makedirs(self.root_dir)
560
-
561
- # 2. 判断代码库是否是git仓库,如果不是,初始化git仓库
562
- if not os.path.exists(os.path.join(self.root_dir, ".git")):
563
- PrettyOutput.print(
564
- "Git repository does not exist, initializing...", OutputType.INFO)
565
- os.chdir(self.root_dir)
566
- os.system(f"git init")
567
- # 2.1 添加所有的文件
568
- os.system(f"git add .")
569
- # 2.2 提交
570
- os.system(f"git commit -m 'Initial commit'")
571
- os.chdir(self.current_dir)
572
-
573
- # 3. 查看代码库是否有未提交的文件,如果有,提交一次
574
- if self._has_uncommitted_files(self.root_dir):
575
- os.chdir(self.root_dir)
576
- os.system(f"git add .")
577
- os.system(f"git commit -m 'commit before code edit'")
578
- os.chdir(self.current_dir)
579
-
580
- # 4. 开始建立代码库索引
581
- os.chdir(self.root_dir)
582
- self._index_project(self.language)
583
- os.chdir(self.current_dir)
584
-
585
- # 5. 根据索引和需求,查找相关文件
586
- related_files = self._find_related_files(self.feature)
587
- for file in related_files:
588
- PrettyOutput.print(f"Related file: {file['file_path']}", OutputType.INFO)
589
- for file in related_files:
590
- with open(file["file_path"], "r", encoding="utf-8") as f:
591
- file_content = f.read()
592
- file["file_content"] = file_content
593
- patches = self._make_patch(related_files)
594
- while True:
595
- # 生成修改方案
596
- PrettyOutput.print(f"生成{len(patches)}个补丁", OutputType.INFO)
597
-
598
- if not patches:
599
- self._save_edit_record(self.feature, patches)
600
- return {
601
- "success": False,
602
- "stdout": "",
603
- "stderr": "未生成补丁",
604
- "error": ValueError("未生成补丁")
605
- }
606
-
607
- # 尝试应用补丁
608
- success, error_info = self._apply_patch(related_files, patches)
609
-
610
- if success:
611
- # 用户确认修改
612
- user_confirm = input("是否确认修改?(y/n)")
613
- if user_confirm.lower() == "y":
614
- PrettyOutput.print("修改确认成功,提交修改", OutputType.INFO)
615
- os.chdir(self.root_dir)
616
- os.system(f"git add .")
617
- os.system(f"git commit -m '{self.feature}'")
618
- os.chdir(self.current_dir)
619
- # 保存修改记录
620
- self._save_edit_record(self.feature, patches)
621
- # 重新建立代码库索引
622
- self._index_project(self.language)
623
-
624
- return {
625
- "success": True,
626
- "stdout": f"已完成功能开发{self.feature}",
627
- "stderr": "",
628
- "error": None
629
- }
630
- else:
631
- PrettyOutput.print("修改已取消,回退更改", OutputType.INFO)
632
- os.chdir(self.root_dir)
633
- os.system(f"git reset --hard") # 回退已修改的文件
634
- os.system(f"git clean -df") # 删除新创建的文件和目录
635
- os.chdir(self.current_dir)
636
- return {
637
- "success": False,
638
- "stdout": "",
639
- "stderr": "修改被用户取消,文件未发生任何变化",
640
- "error": UserWarning("用户取消修改")
641
- }
642
- else:
643
- # 补丁应用失败,让模型重新生成
644
- PrettyOutput.print(f"补丁应用失败,请求重新生成: {error_info}", OutputType.WARNING)
645
- retry_prompt = f"""补丁应用失败,请根据以下错误信息重新生成补丁:
646
-
647
- 错误信息:
648
- {error_info}
649
-
650
- 请确保:
651
- 1. 准确定位要修改的代码位置
652
- 2. 正确处理代码缩进
653
- 3. 考虑代码上下文
654
- 4. 对新文件不要包含原始内容
655
- """
656
- patches = self._remake_patch(retry_prompt)
657
- continue
658
-
659
- except Exception as e:
660
- return {
661
- "success": False,
662
- "stdout": "",
663
- "stderr": f"执行失败: {str(e)}",
664
- "error": e
665
- }
666
-
667
-
668
- def main():
669
- """命令行入口"""
670
- import argparse
671
-
672
- load_env_from_file()
673
-
674
- parser = argparse.ArgumentParser(description='代码修改工具')
675
- parser.add_argument('-p', '--platform', help='AI平台名称', default=os.environ.get('JARVIS_PLATFORM'))
676
- parser.add_argument('-m', '--model', help='模型名称', default=os.environ.get('JARVIS_CODEGEN_MODEL'))
677
- parser.add_argument('-d', '--dir', help='项目根目录', required=True)
678
- parser.add_argument('-l', '--language', help='编程语言', required=True)
679
- args = parser.parse_args()
680
-
681
- # 设置平台
682
- if not args.platform:
683
- print("错误: 未指定AI平台,请使用 -p 参数或设置 JARVIS_PLATFORM 环境变量")
684
- return 1
685
-
686
- PlatformRegistry.get_global_platform_registry().set_global_platform_name(args.platform)
687
-
688
- # 设置模型
689
- if args.model:
690
- os.environ['JARVIS_CODEGEN_MODEL'] = args.model
691
-
692
- tool = CodeEditTool()
693
-
694
- # 循环处理需求
695
- while True:
696
- try:
697
- # 获取需求
698
- feature = get_multiline_input("请输入开发需求 (输入空行退出):")
699
-
700
- if not feature or feature == "__interrupt__":
701
- break
702
-
703
- # 执行修改
704
- result = tool.execute({
705
- "feature": feature,
706
- "root_dir": args.dir,
707
- "language": args.language
708
- })
709
-
710
- # 显示结果
711
- if result["success"]:
712
- PrettyOutput.print(result["stdout"], OutputType.SUCCESS)
713
- else:
714
- if result["stderr"]:
715
- PrettyOutput.print(result["stderr"], OutputType.ERROR)
716
- if result["error"]:
717
- PrettyOutput.print(f"错误类型: {type(result['error']).__name__}", OutputType.ERROR)
718
-
719
- except KeyboardInterrupt:
720
- print("\n用户中断执行")
721
- break
722
- except Exception as e:
723
- PrettyOutput.print(f"执行出错: {str(e)}", OutputType.ERROR)
724
- continue
725
-
726
- return 0
727
-
728
- if __name__ == "__main__":
729
- exit(main())