jarvis-ai-assistant 0.7.16__py3-none-any.whl → 1.0.2__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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +567 -222
- jarvis/jarvis_agent/agent_manager.py +19 -12
- jarvis/jarvis_agent/builtin_input_handler.py +79 -11
- jarvis/jarvis_agent/config_editor.py +7 -2
- jarvis/jarvis_agent/event_bus.py +24 -13
- jarvis/jarvis_agent/events.py +19 -1
- jarvis/jarvis_agent/file_context_handler.py +67 -64
- jarvis/jarvis_agent/file_methodology_manager.py +38 -24
- jarvis/jarvis_agent/jarvis.py +186 -114
- jarvis/jarvis_agent/language_extractors/__init__.py +8 -1
- jarvis/jarvis_agent/language_extractors/c_extractor.py +7 -4
- jarvis/jarvis_agent/language_extractors/cpp_extractor.py +9 -4
- jarvis/jarvis_agent/language_extractors/go_extractor.py +7 -4
- jarvis/jarvis_agent/language_extractors/java_extractor.py +27 -20
- jarvis/jarvis_agent/language_extractors/javascript_extractor.py +22 -17
- jarvis/jarvis_agent/language_extractors/python_extractor.py +7 -4
- jarvis/jarvis_agent/language_extractors/rust_extractor.py +7 -4
- jarvis/jarvis_agent/language_extractors/typescript_extractor.py +22 -17
- jarvis/jarvis_agent/language_support_info.py +250 -219
- jarvis/jarvis_agent/main.py +19 -23
- jarvis/jarvis_agent/memory_manager.py +9 -6
- jarvis/jarvis_agent/methodology_share_manager.py +21 -15
- jarvis/jarvis_agent/output_handler.py +4 -2
- jarvis/jarvis_agent/prompt_builder.py +7 -6
- jarvis/jarvis_agent/prompt_manager.py +113 -8
- jarvis/jarvis_agent/prompts.py +317 -85
- jarvis/jarvis_agent/protocols.py +5 -2
- jarvis/jarvis_agent/run_loop.py +192 -32
- jarvis/jarvis_agent/session_manager.py +7 -3
- jarvis/jarvis_agent/share_manager.py +23 -13
- jarvis/jarvis_agent/shell_input_handler.py +12 -8
- jarvis/jarvis_agent/stdio_redirect.py +25 -26
- jarvis/jarvis_agent/task_analyzer.py +29 -23
- jarvis/jarvis_agent/task_list.py +869 -0
- jarvis/jarvis_agent/task_manager.py +26 -23
- jarvis/jarvis_agent/tool_executor.py +6 -5
- jarvis/jarvis_agent/tool_share_manager.py +24 -14
- jarvis/jarvis_agent/user_interaction.py +3 -3
- jarvis/jarvis_agent/utils.py +9 -1
- jarvis/jarvis_agent/web_bridge.py +37 -17
- jarvis/jarvis_agent/web_output_sink.py +5 -2
- jarvis/jarvis_agent/web_server.py +165 -36
- jarvis/jarvis_c2rust/__init__.py +1 -1
- jarvis/jarvis_c2rust/cli.py +260 -141
- jarvis/jarvis_c2rust/collector.py +37 -18
- jarvis/jarvis_c2rust/constants.py +60 -0
- jarvis/jarvis_c2rust/library_replacer.py +242 -1010
- jarvis/jarvis_c2rust/library_replacer_checkpoint.py +133 -0
- jarvis/jarvis_c2rust/library_replacer_llm.py +287 -0
- jarvis/jarvis_c2rust/library_replacer_loader.py +191 -0
- jarvis/jarvis_c2rust/library_replacer_output.py +134 -0
- jarvis/jarvis_c2rust/library_replacer_prompts.py +124 -0
- jarvis/jarvis_c2rust/library_replacer_utils.py +188 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +98 -1044
- jarvis/jarvis_c2rust/llm_module_agent_apply.py +170 -0
- jarvis/jarvis_c2rust/llm_module_agent_executor.py +288 -0
- jarvis/jarvis_c2rust/llm_module_agent_loader.py +170 -0
- jarvis/jarvis_c2rust/llm_module_agent_prompts.py +268 -0
- jarvis/jarvis_c2rust/llm_module_agent_types.py +57 -0
- jarvis/jarvis_c2rust/llm_module_agent_utils.py +150 -0
- jarvis/jarvis_c2rust/llm_module_agent_validator.py +119 -0
- jarvis/jarvis_c2rust/loaders.py +28 -10
- jarvis/jarvis_c2rust/models.py +5 -2
- jarvis/jarvis_c2rust/optimizer.py +192 -1974
- jarvis/jarvis_c2rust/optimizer_build_fix.py +286 -0
- jarvis/jarvis_c2rust/optimizer_clippy.py +766 -0
- jarvis/jarvis_c2rust/optimizer_config.py +49 -0
- jarvis/jarvis_c2rust/optimizer_docs.py +183 -0
- jarvis/jarvis_c2rust/optimizer_options.py +48 -0
- jarvis/jarvis_c2rust/optimizer_progress.py +469 -0
- jarvis/jarvis_c2rust/optimizer_report.py +52 -0
- jarvis/jarvis_c2rust/optimizer_unsafe.py +309 -0
- jarvis/jarvis_c2rust/optimizer_utils.py +469 -0
- jarvis/jarvis_c2rust/optimizer_visibility.py +185 -0
- jarvis/jarvis_c2rust/scanner.py +229 -166
- jarvis/jarvis_c2rust/transpiler.py +531 -2732
- jarvis/jarvis_c2rust/transpiler_agents.py +503 -0
- jarvis/jarvis_c2rust/transpiler_build.py +1294 -0
- jarvis/jarvis_c2rust/transpiler_codegen.py +204 -0
- jarvis/jarvis_c2rust/transpiler_compile.py +146 -0
- jarvis/jarvis_c2rust/transpiler_config.py +178 -0
- jarvis/jarvis_c2rust/transpiler_context.py +122 -0
- jarvis/jarvis_c2rust/transpiler_executor.py +516 -0
- jarvis/jarvis_c2rust/transpiler_generation.py +278 -0
- jarvis/jarvis_c2rust/transpiler_git.py +163 -0
- jarvis/jarvis_c2rust/transpiler_mod_utils.py +225 -0
- jarvis/jarvis_c2rust/transpiler_modules.py +336 -0
- jarvis/jarvis_c2rust/transpiler_planning.py +394 -0
- jarvis/jarvis_c2rust/transpiler_review.py +1196 -0
- jarvis/jarvis_c2rust/transpiler_symbols.py +176 -0
- jarvis/jarvis_c2rust/utils.py +269 -79
- jarvis/jarvis_code_agent/after_change.py +233 -0
- jarvis/jarvis_code_agent/build_validation_config.py +37 -30
- jarvis/jarvis_code_agent/builtin_rules.py +68 -0
- jarvis/jarvis_code_agent/code_agent.py +976 -1517
- jarvis/jarvis_code_agent/code_agent_build.py +227 -0
- jarvis/jarvis_code_agent/code_agent_diff.py +246 -0
- jarvis/jarvis_code_agent/code_agent_git.py +525 -0
- jarvis/jarvis_code_agent/code_agent_impact.py +177 -0
- jarvis/jarvis_code_agent/code_agent_lint.py +283 -0
- jarvis/jarvis_code_agent/code_agent_llm.py +159 -0
- jarvis/jarvis_code_agent/code_agent_postprocess.py +105 -0
- jarvis/jarvis_code_agent/code_agent_prompts.py +46 -0
- jarvis/jarvis_code_agent/code_agent_rules.py +305 -0
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +52 -48
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +12 -10
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +12 -11
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +16 -12
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +26 -17
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +558 -104
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +27 -16
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +22 -18
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +21 -16
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +20 -16
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +27 -16
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +47 -23
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +71 -37
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +162 -35
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +111 -57
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +18 -12
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +185 -183
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +2 -1
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +24 -15
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +227 -141
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +321 -247
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +37 -29
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +21 -13
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +15 -9
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +75 -45
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +87 -52
- jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +84 -51
- jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +94 -64
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +109 -71
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +97 -63
- jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +103 -69
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +271 -268
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +76 -64
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +92 -19
- jarvis/jarvis_code_agent/diff_visualizer.py +998 -0
- jarvis/jarvis_code_agent/lint.py +223 -524
- jarvis/jarvis_code_agent/rule_share_manager.py +158 -0
- jarvis/jarvis_code_agent/rules/clean_code.md +144 -0
- jarvis/jarvis_code_agent/rules/code_review.md +115 -0
- jarvis/jarvis_code_agent/rules/documentation.md +165 -0
- jarvis/jarvis_code_agent/rules/generate_rules.md +52 -0
- jarvis/jarvis_code_agent/rules/performance.md +158 -0
- jarvis/jarvis_code_agent/rules/refactoring.md +139 -0
- jarvis/jarvis_code_agent/rules/security.md +160 -0
- jarvis/jarvis_code_agent/rules/tdd.md +78 -0
- jarvis/jarvis_code_agent/test_rules/cpp_test.md +118 -0
- jarvis/jarvis_code_agent/test_rules/go_test.md +98 -0
- jarvis/jarvis_code_agent/test_rules/java_test.md +99 -0
- jarvis/jarvis_code_agent/test_rules/javascript_test.md +113 -0
- jarvis/jarvis_code_agent/test_rules/php_test.md +117 -0
- jarvis/jarvis_code_agent/test_rules/python_test.md +91 -0
- jarvis/jarvis_code_agent/test_rules/ruby_test.md +102 -0
- jarvis/jarvis_code_agent/test_rules/rust_test.md +86 -0
- jarvis/jarvis_code_agent/utils.py +36 -26
- jarvis/jarvis_code_analysis/checklists/loader.py +21 -21
- jarvis/jarvis_code_analysis/code_review.py +64 -33
- jarvis/jarvis_data/config_schema.json +285 -192
- jarvis/jarvis_git_squash/main.py +8 -6
- jarvis/jarvis_git_utils/git_commiter.py +53 -76
- jarvis/jarvis_mcp/__init__.py +5 -2
- jarvis/jarvis_mcp/sse_mcp_client.py +40 -30
- jarvis/jarvis_mcp/stdio_mcp_client.py +27 -19
- jarvis/jarvis_mcp/streamable_mcp_client.py +35 -26
- jarvis/jarvis_memory_organizer/memory_organizer.py +78 -55
- jarvis/jarvis_methodology/main.py +48 -39
- jarvis/jarvis_multi_agent/__init__.py +56 -23
- jarvis/jarvis_multi_agent/main.py +15 -18
- jarvis/jarvis_platform/base.py +179 -111
- jarvis/jarvis_platform/human.py +27 -16
- jarvis/jarvis_platform/kimi.py +52 -45
- jarvis/jarvis_platform/openai.py +101 -40
- jarvis/jarvis_platform/registry.py +51 -33
- jarvis/jarvis_platform/tongyi.py +68 -38
- jarvis/jarvis_platform/yuanbao.py +59 -43
- jarvis/jarvis_platform_manager/main.py +68 -76
- jarvis/jarvis_platform_manager/service.py +24 -14
- jarvis/jarvis_rag/README_CONFIG.md +314 -0
- jarvis/jarvis_rag/README_DYNAMIC_LOADING.md +311 -0
- jarvis/jarvis_rag/README_ONLINE_MODELS.md +230 -0
- jarvis/jarvis_rag/__init__.py +57 -4
- jarvis/jarvis_rag/cache.py +3 -1
- jarvis/jarvis_rag/cli.py +48 -68
- jarvis/jarvis_rag/embedding_interface.py +39 -0
- jarvis/jarvis_rag/embedding_manager.py +7 -230
- jarvis/jarvis_rag/embeddings/__init__.py +41 -0
- jarvis/jarvis_rag/embeddings/base.py +114 -0
- jarvis/jarvis_rag/embeddings/cohere.py +66 -0
- jarvis/jarvis_rag/embeddings/edgefn.py +117 -0
- jarvis/jarvis_rag/embeddings/local.py +260 -0
- jarvis/jarvis_rag/embeddings/openai.py +62 -0
- jarvis/jarvis_rag/embeddings/registry.py +293 -0
- jarvis/jarvis_rag/llm_interface.py +8 -6
- jarvis/jarvis_rag/query_rewriter.py +8 -9
- jarvis/jarvis_rag/rag_pipeline.py +61 -52
- jarvis/jarvis_rag/reranker.py +7 -75
- jarvis/jarvis_rag/reranker_interface.py +32 -0
- jarvis/jarvis_rag/rerankers/__init__.py +41 -0
- jarvis/jarvis_rag/rerankers/base.py +109 -0
- jarvis/jarvis_rag/rerankers/cohere.py +67 -0
- jarvis/jarvis_rag/rerankers/edgefn.py +140 -0
- jarvis/jarvis_rag/rerankers/jina.py +79 -0
- jarvis/jarvis_rag/rerankers/local.py +89 -0
- jarvis/jarvis_rag/rerankers/registry.py +293 -0
- jarvis/jarvis_rag/retriever.py +58 -43
- jarvis/jarvis_sec/__init__.py +66 -141
- jarvis/jarvis_sec/agents.py +21 -17
- jarvis/jarvis_sec/analysis.py +80 -33
- jarvis/jarvis_sec/checkers/__init__.py +7 -13
- jarvis/jarvis_sec/checkers/c_checker.py +356 -164
- jarvis/jarvis_sec/checkers/rust_checker.py +47 -29
- jarvis/jarvis_sec/cli.py +43 -21
- jarvis/jarvis_sec/clustering.py +430 -272
- jarvis/jarvis_sec/file_manager.py +99 -55
- jarvis/jarvis_sec/parsers.py +9 -6
- jarvis/jarvis_sec/prompts.py +4 -3
- jarvis/jarvis_sec/report.py +44 -22
- jarvis/jarvis_sec/review.py +180 -107
- jarvis/jarvis_sec/status.py +50 -41
- jarvis/jarvis_sec/types.py +3 -0
- jarvis/jarvis_sec/utils.py +160 -83
- jarvis/jarvis_sec/verification.py +411 -181
- jarvis/jarvis_sec/workflow.py +132 -21
- jarvis/jarvis_smart_shell/main.py +28 -41
- jarvis/jarvis_stats/cli.py +14 -12
- jarvis/jarvis_stats/stats.py +28 -19
- jarvis/jarvis_stats/storage.py +14 -8
- jarvis/jarvis_stats/visualizer.py +12 -7
- jarvis/jarvis_tools/base.py +5 -2
- jarvis/jarvis_tools/clear_memory.py +13 -9
- jarvis/jarvis_tools/cli/main.py +23 -18
- jarvis/jarvis_tools/edit_file.py +572 -873
- jarvis/jarvis_tools/execute_script.py +10 -7
- jarvis/jarvis_tools/file_analyzer.py +7 -8
- jarvis/jarvis_tools/meta_agent.py +287 -0
- jarvis/jarvis_tools/methodology.py +5 -3
- jarvis/jarvis_tools/read_code.py +305 -1438
- jarvis/jarvis_tools/read_symbols.py +50 -17
- jarvis/jarvis_tools/read_webpage.py +19 -18
- jarvis/jarvis_tools/registry.py +435 -156
- jarvis/jarvis_tools/retrieve_memory.py +16 -11
- jarvis/jarvis_tools/save_memory.py +8 -6
- jarvis/jarvis_tools/search_web.py +31 -31
- jarvis/jarvis_tools/sub_agent.py +32 -28
- jarvis/jarvis_tools/sub_code_agent.py +44 -60
- jarvis/jarvis_tools/task_list_manager.py +1811 -0
- jarvis/jarvis_tools/virtual_tty.py +29 -19
- jarvis/jarvis_utils/__init__.py +4 -0
- jarvis/jarvis_utils/builtin_replace_map.py +2 -1
- jarvis/jarvis_utils/clipboard.py +9 -8
- jarvis/jarvis_utils/collections.py +331 -0
- jarvis/jarvis_utils/config.py +699 -194
- jarvis/jarvis_utils/dialogue_recorder.py +294 -0
- jarvis/jarvis_utils/embedding.py +6 -3
- jarvis/jarvis_utils/file_processors.py +7 -1
- jarvis/jarvis_utils/fzf.py +9 -3
- jarvis/jarvis_utils/git_utils.py +71 -42
- jarvis/jarvis_utils/globals.py +116 -32
- jarvis/jarvis_utils/http.py +6 -2
- jarvis/jarvis_utils/input.py +318 -83
- jarvis/jarvis_utils/jsonnet_compat.py +119 -104
- jarvis/jarvis_utils/methodology.py +37 -28
- jarvis/jarvis_utils/output.py +201 -44
- jarvis/jarvis_utils/utils.py +986 -628
- {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/METADATA +49 -33
- jarvis_ai_assistant-1.0.2.dist-info/RECORD +304 -0
- jarvis/jarvis_code_agent/code_analyzer/structured_code.py +0 -556
- jarvis/jarvis_tools/generate_new_tool.py +0 -205
- jarvis/jarvis_tools/lsp_client.py +0 -1552
- jarvis/jarvis_tools/rewrite_file.py +0 -105
- jarvis_ai_assistant-0.7.16.dist-info/RECORD +0 -218
- {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/top_level.txt +0 -0
jarvis/jarvis_tools/edit_file.py
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
"""普通文件编辑工具(基于 search/replace 的非结构化编辑)"""
|
|
2
|
+
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
|
-
import time
|
|
5
|
-
from enum import Enum
|
|
6
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
-
|
|
8
5
|
|
|
6
|
+
from jarvis.jarvis_utils.output import PrettyOutput
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
PARAMETER_MISSING = "parameter_missing" # 参数缺失
|
|
17
|
-
UNSUPPORTED_ACTION = "unsupported_action" # 不支持的操作
|
|
18
|
-
OTHER = "other" # 其他错误
|
|
8
|
+
# -*- coding: utf-8 -*-
|
|
9
|
+
from typing import Any
|
|
10
|
+
from typing import Dict
|
|
11
|
+
from typing import List
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from typing import Tuple
|
|
19
14
|
|
|
20
15
|
|
|
21
|
-
class
|
|
22
|
-
"""
|
|
16
|
+
class EditFileNormalTool:
|
|
17
|
+
"""普通文件编辑工具,完全基于 search/replace 进行文件编辑"""
|
|
23
18
|
|
|
24
19
|
name = "edit_file"
|
|
25
|
-
description =
|
|
20
|
+
description = (
|
|
21
|
+
"使用 search/replace 对文件进行普通文本编辑(不依赖块id),支持同时修改多个文件。\n\n"
|
|
22
|
+
"💡 使用方式:\n"
|
|
23
|
+
"1. 直接指定要编辑的文件路径\n"
|
|
24
|
+
"2. 为每个文件提供一组 search/replace 操作\n"
|
|
25
|
+
"3. 使用精确匹配查找 search 文本,找到匹配后替换为新文本\n\n"
|
|
26
|
+
"🚀 特殊功能:\n"
|
|
27
|
+
'- 当 search 为空字符串 "" 时,表示直接重写整个文件,replace 的内容将作为文件的完整新内容\n'
|
|
28
|
+
"- 如果存在多个diffs且第一个diff的search为空字符串,将只应用第一个diff(重写整个文件),跳过后续所有diffs\n\n"
|
|
29
|
+
"⚠️ 提示:\n"
|
|
30
|
+
"- search 使用精确字符串匹配,不支持正则表达式\n"
|
|
31
|
+
"- **重要:search 必须提供足够的上下文来唯一定位目标位置**,避免匹配到错误的位置。建议包含:\n"
|
|
32
|
+
" * 目标代码的前后几行上下文(至少包含目标代码所在函数的签名或关键标识)\n"
|
|
33
|
+
" * 目标代码附近的唯一标识符(如函数名、变量名、注释等)\n"
|
|
34
|
+
" * 避免使用过短的 search 文本(如单个单词、短字符串),除非能确保唯一性\n"
|
|
35
|
+
"- 如果某个 search 在文件中找不到精确匹配(search非空时),将导致该文件的编辑失败,文件内容会回滚到原始状态\n"
|
|
36
|
+
"- 建议在 search 中包含足够的上下文,确保能唯一匹配到目标位置,避免误匹配"
|
|
37
|
+
)
|
|
26
38
|
|
|
27
39
|
parameters = {
|
|
28
40
|
"type": "object",
|
|
@@ -41,50 +53,36 @@ class EditFileTool:
|
|
|
41
53
|
"items": {
|
|
42
54
|
"type": "object",
|
|
43
55
|
"properties": {
|
|
44
|
-
"block_id": {
|
|
45
|
-
"type": "string",
|
|
46
|
-
"description": "要操作的块id(从read_code工具获取的结构化块id)",
|
|
47
|
-
},
|
|
48
|
-
"action": {
|
|
49
|
-
"type": "string",
|
|
50
|
-
"enum": ["delete", "insert_before", "insert_after", "replace", "edit"],
|
|
51
|
-
"description": "操作类型:delete(删除块)、insert_before(在块前插入)、insert_after(在块后插入)、replace(替换块)、edit(在块内进行search/replace)",
|
|
52
|
-
},
|
|
53
|
-
"content": {
|
|
54
|
-
"type": "string",
|
|
55
|
-
"description": "新内容(对于insert_before、insert_after、replace操作必需,delete和edit操作不需要)",
|
|
56
|
-
},
|
|
57
56
|
"search": {
|
|
58
57
|
"type": "string",
|
|
59
|
-
"description": "
|
|
58
|
+
"description": '要搜索的原始文本(不支持正则表达式)。当为空字符串""时,表示直接重写整个文件,replace的内容将作为文件的完整新内容。非空时,**重要:必须提供足够的上下文来唯一定位目标位置**,建议包含目标代码的前后几行上下文、函数签名或唯一标识符,避免匹配到错误的位置。',
|
|
60
59
|
},
|
|
61
60
|
"replace": {
|
|
62
61
|
"type": "string",
|
|
63
|
-
"description": "
|
|
62
|
+
"description": "替换后的文本(可以为空字符串)",
|
|
64
63
|
},
|
|
65
64
|
},
|
|
66
|
-
"required": ["
|
|
65
|
+
"required": ["search", "replace"],
|
|
67
66
|
},
|
|
68
|
-
"description": "
|
|
67
|
+
"description": "普通文本替换操作列表,按顺序依次应用到文件内容",
|
|
69
68
|
},
|
|
70
69
|
},
|
|
71
70
|
"required": ["file_path", "diffs"],
|
|
72
71
|
},
|
|
73
|
-
"description": "
|
|
72
|
+
"description": "要修改的文件列表,每个文件包含文件路径和对应的 search/replace 操作列表",
|
|
74
73
|
},
|
|
75
74
|
},
|
|
76
75
|
"required": ["files"],
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
def __init__(self):
|
|
80
|
-
"""
|
|
79
|
+
"""初始化普通文件编辑工具"""
|
|
81
80
|
pass
|
|
82
81
|
|
|
83
|
-
|
|
84
82
|
@staticmethod
|
|
85
83
|
def _validate_basic_args(args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
86
84
|
"""验证基本参数
|
|
87
|
-
|
|
85
|
+
|
|
88
86
|
Returns:
|
|
89
87
|
如果验证失败,返回错误响应;否则返回None
|
|
90
88
|
"""
|
|
@@ -103,305 +101,449 @@ class EditFileTool:
|
|
|
103
101
|
"stdout": "",
|
|
104
102
|
"stderr": "files参数必须是数组类型",
|
|
105
103
|
}
|
|
106
|
-
|
|
104
|
+
|
|
107
105
|
if len(files) == 0:
|
|
108
106
|
return {
|
|
109
107
|
"success": False,
|
|
110
108
|
"stdout": "",
|
|
111
109
|
"stderr": "files数组不能为空",
|
|
112
110
|
}
|
|
113
|
-
|
|
111
|
+
|
|
114
112
|
# 验证每个文件项
|
|
115
113
|
for idx, file_item in enumerate(files):
|
|
116
114
|
if not isinstance(file_item, dict):
|
|
117
115
|
return {
|
|
118
116
|
"success": False,
|
|
119
117
|
"stdout": "",
|
|
120
|
-
"stderr": f"files数组第 {idx+1} 项必须是字典类型",
|
|
118
|
+
"stderr": f"files数组第 {idx + 1} 项必须是字典类型",
|
|
121
119
|
}
|
|
122
|
-
|
|
120
|
+
|
|
123
121
|
file_path = file_item.get("file_path")
|
|
124
122
|
diffs = file_item.get("diffs", [])
|
|
125
|
-
|
|
123
|
+
|
|
126
124
|
if not file_path:
|
|
127
125
|
return {
|
|
128
126
|
"success": False,
|
|
129
127
|
"stdout": "",
|
|
130
|
-
"stderr": f"files数组第 {idx+1} 项缺少必需参数:file_path",
|
|
128
|
+
"stderr": f"files数组第 {idx + 1} 项缺少必需参数:file_path",
|
|
131
129
|
}
|
|
132
|
-
|
|
130
|
+
|
|
133
131
|
if not diffs:
|
|
134
132
|
return {
|
|
135
133
|
"success": False,
|
|
136
134
|
"stdout": "",
|
|
137
|
-
"stderr": f"files数组第 {idx+1} 项缺少必需参数:diffs",
|
|
135
|
+
"stderr": f"files数组第 {idx + 1} 项缺少必需参数:diffs",
|
|
138
136
|
}
|
|
139
|
-
|
|
137
|
+
|
|
140
138
|
if not isinstance(diffs, list):
|
|
141
139
|
return {
|
|
142
140
|
"success": False,
|
|
143
141
|
"stdout": "",
|
|
144
|
-
"stderr": f"files数组第 {idx+1} 项的diffs参数必须是数组类型",
|
|
142
|
+
"stderr": f"files数组第 {idx + 1} 项的diffs参数必须是数组类型",
|
|
145
143
|
}
|
|
146
|
-
|
|
144
|
+
|
|
147
145
|
return None
|
|
148
146
|
|
|
149
147
|
@staticmethod
|
|
150
|
-
def
|
|
151
|
-
"""
|
|
152
|
-
|
|
148
|
+
def _read_file_with_backup(file_path: str) -> Tuple[str, Optional[str]]:
|
|
149
|
+
"""读取文件并创建备份
|
|
150
|
+
|
|
153
151
|
Args:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
file_path: 文件路径
|
|
153
|
+
|
|
157
154
|
Returns:
|
|
158
|
-
|
|
155
|
+
(文件内容, 备份文件路径或None)
|
|
159
156
|
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
157
|
+
abs_path = os.path.abspath(file_path)
|
|
158
|
+
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
|
159
|
+
|
|
160
|
+
file_content = ""
|
|
161
|
+
backup_path = None
|
|
162
|
+
if os.path.exists(abs_path):
|
|
163
|
+
with open(abs_path, "r", encoding="utf-8") as f:
|
|
164
|
+
file_content = f.read()
|
|
165
|
+
# 创建备份文件
|
|
166
|
+
backup_path = abs_path + ".bak"
|
|
167
|
+
try:
|
|
168
|
+
shutil.copy2(abs_path, backup_path)
|
|
169
|
+
except Exception:
|
|
170
|
+
# 备份失败不影响主流程
|
|
171
|
+
backup_path = None
|
|
172
|
+
|
|
173
|
+
return file_content, backup_path
|
|
169
174
|
|
|
170
175
|
@staticmethod
|
|
171
|
-
def
|
|
172
|
-
|
|
173
|
-
|
|
176
|
+
def _write_file_with_rollback(
|
|
177
|
+
abs_path: str, content: str, backup_path: Optional[str]
|
|
178
|
+
) -> Tuple[bool, Optional[str]]:
|
|
179
|
+
"""写入文件,失败时回滚
|
|
180
|
+
|
|
174
181
|
Args:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
abs_path: 文件绝对路径
|
|
183
|
+
content: 要写入的内容
|
|
184
|
+
backup_path: 备份文件路径或None
|
|
185
|
+
|
|
178
186
|
Returns:
|
|
179
|
-
|
|
187
|
+
(是否成功, 错误信息或None)
|
|
180
188
|
"""
|
|
181
|
-
if not cache_info:
|
|
182
|
-
return False
|
|
183
|
-
|
|
184
189
|
try:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
190
|
+
with open(abs_path, "w", encoding="utf-8") as f:
|
|
191
|
+
f.write(content)
|
|
192
|
+
return (True, None)
|
|
193
|
+
except Exception as write_error:
|
|
194
|
+
# 写入失败,尝试回滚
|
|
195
|
+
if backup_path and os.path.exists(backup_path):
|
|
196
|
+
try:
|
|
197
|
+
shutil.copy2(backup_path, abs_path)
|
|
198
|
+
os.remove(backup_path)
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
error_msg = f"文件写入失败: {str(write_error)}"
|
|
202
|
+
PrettyOutput.auto_print(f"❌ {error_msg}")
|
|
203
|
+
return (False, error_msg)
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _validate_normal_diff(
|
|
207
|
+
diff: Dict[str, Any], idx: int
|
|
208
|
+
) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
|
|
209
|
+
"""验证并转换 normal 类型的 diff
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
(错误响应或None, 规范化后的diff或None)
|
|
213
|
+
"""
|
|
214
|
+
search = diff.get("search")
|
|
215
|
+
replace = diff.get("replace")
|
|
216
|
+
|
|
217
|
+
if search is None:
|
|
218
|
+
return (
|
|
219
|
+
{
|
|
220
|
+
"success": False,
|
|
221
|
+
"stdout": "",
|
|
222
|
+
"stderr": f"第 {idx} 个diff缺少search参数",
|
|
223
|
+
},
|
|
224
|
+
None,
|
|
225
|
+
)
|
|
226
|
+
if not isinstance(search, str):
|
|
227
|
+
return (
|
|
228
|
+
{
|
|
229
|
+
"success": False,
|
|
230
|
+
"stdout": "",
|
|
231
|
+
"stderr": f"第 {idx} 个diff的search参数必须是字符串",
|
|
232
|
+
},
|
|
233
|
+
None,
|
|
234
|
+
)
|
|
235
|
+
# 允许空字符串作为search参数,表示直接重写整个文件
|
|
236
|
+
|
|
237
|
+
if replace is None:
|
|
238
|
+
return (
|
|
239
|
+
{
|
|
240
|
+
"success": False,
|
|
241
|
+
"stdout": "",
|
|
242
|
+
"stderr": f"第 {idx} 个diff缺少replace参数",
|
|
243
|
+
},
|
|
244
|
+
None,
|
|
245
|
+
)
|
|
246
|
+
if not isinstance(replace, str):
|
|
247
|
+
return (
|
|
248
|
+
{
|
|
249
|
+
"success": False,
|
|
250
|
+
"stdout": "",
|
|
251
|
+
"stderr": f"第 {idx} 个diff的replace参数必须是字符串",
|
|
252
|
+
},
|
|
253
|
+
None,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
None,
|
|
258
|
+
{
|
|
259
|
+
"search": search,
|
|
260
|
+
"replace": replace,
|
|
261
|
+
},
|
|
262
|
+
)
|
|
203
263
|
|
|
204
264
|
@staticmethod
|
|
205
|
-
def
|
|
206
|
-
"""
|
|
207
|
-
|
|
265
|
+
def _count_matches(content: str, search_text: str) -> int:
|
|
266
|
+
"""统计文本在内容中的匹配次数
|
|
267
|
+
|
|
208
268
|
Args:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
269
|
+
content: 文件内容
|
|
270
|
+
search_text: 要搜索的文本
|
|
271
|
+
|
|
212
272
|
Returns:
|
|
213
|
-
|
|
273
|
+
匹配次数
|
|
214
274
|
"""
|
|
215
|
-
if not
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
# 直接从 blocks 字典中查找
|
|
219
|
-
blocks = cache_info.get("blocks", {})
|
|
220
|
-
block = blocks.get(block_id)
|
|
221
|
-
if block:
|
|
222
|
-
return {
|
|
223
|
-
"content": block.get("content", ""),
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return None
|
|
275
|
+
if not search_text:
|
|
276
|
+
return 0
|
|
277
|
+
return content.count(search_text)
|
|
227
278
|
|
|
228
279
|
@staticmethod
|
|
229
|
-
def
|
|
230
|
-
|
|
231
|
-
|
|
280
|
+
def _generate_diff_preview(
|
|
281
|
+
original_content: str,
|
|
282
|
+
modified_content: str,
|
|
283
|
+
file_path: str,
|
|
284
|
+
match_count: int,
|
|
285
|
+
search_text: str,
|
|
286
|
+
replace_text: str,
|
|
287
|
+
agent: Optional[Any] = None,
|
|
288
|
+
token_ratio: float = 0.3,
|
|
289
|
+
) -> str:
|
|
290
|
+
"""生成修改后的预览diff
|
|
291
|
+
|
|
232
292
|
Args:
|
|
233
|
-
|
|
234
|
-
|
|
293
|
+
original_content: 原始文件内容
|
|
294
|
+
modified_content: 修改后的文件内容
|
|
295
|
+
file_path: 文件路径
|
|
296
|
+
match_count: 匹配次数
|
|
297
|
+
search_text: 搜索文本
|
|
298
|
+
replace_text: 替换文本
|
|
299
|
+
agent: 可选的 agent 实例,用于获取剩余 token 数量
|
|
300
|
+
token_ratio: token 使用比例(默认 0.3,即 30%)
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
预览diff字符串
|
|
235
304
|
"""
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
305
|
+
import difflib
|
|
306
|
+
|
|
307
|
+
# 生成统一的diff格式
|
|
308
|
+
original_lines = original_content.splitlines(keepends=True)
|
|
309
|
+
modified_lines = modified_content.splitlines(keepends=True)
|
|
310
|
+
|
|
311
|
+
# 使用difflib生成统一的diff
|
|
312
|
+
diff = list(
|
|
313
|
+
difflib.unified_diff(
|
|
314
|
+
original_lines,
|
|
315
|
+
modified_lines,
|
|
316
|
+
fromfile=f"a/{file_path}",
|
|
317
|
+
tofile=f"b/{file_path}",
|
|
318
|
+
lineterm="",
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
diff_preview = "".join(diff)
|
|
323
|
+
|
|
324
|
+
# 根据剩余token计算最大字符数
|
|
325
|
+
max_diff_chars = None
|
|
326
|
+
|
|
327
|
+
# 优先尝试使用 agent 获取剩余 token(更准确,包含对话历史)
|
|
328
|
+
if agent:
|
|
247
329
|
try:
|
|
248
|
-
|
|
249
|
-
|
|
330
|
+
remaining_tokens = agent.get_remaining_token_count()
|
|
331
|
+
if remaining_tokens > 0:
|
|
332
|
+
# 使用剩余 token 的指定比例作为字符限制(1 token ≈ 4字符)
|
|
333
|
+
max_diff_chars = int(remaining_tokens * token_ratio * 4)
|
|
334
|
+
if max_diff_chars <= 0:
|
|
335
|
+
max_diff_chars = None
|
|
250
336
|
except Exception:
|
|
251
337
|
pass
|
|
252
|
-
|
|
338
|
+
|
|
339
|
+
# 回退方案:使用输入窗口的指定比例转换为字符数
|
|
340
|
+
if max_diff_chars is None:
|
|
341
|
+
try:
|
|
342
|
+
from jarvis.jarvis_utils.config import get_max_input_token_count
|
|
343
|
+
|
|
344
|
+
max_input_tokens = get_max_input_token_count()
|
|
345
|
+
max_diff_chars = int(max_input_tokens * token_ratio * 4)
|
|
346
|
+
except Exception:
|
|
347
|
+
# 如果获取失败,使用默认值(约 10000 字符)
|
|
348
|
+
max_diff_chars = 10000
|
|
349
|
+
|
|
350
|
+
# 限制diff长度
|
|
351
|
+
if len(diff_preview) > max_diff_chars:
|
|
352
|
+
diff_preview = (
|
|
353
|
+
diff_preview[:max_diff_chars] + "\n... (diff 内容过长,已截断)"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return diff_preview
|
|
253
357
|
|
|
254
358
|
@staticmethod
|
|
255
|
-
def
|
|
256
|
-
|
|
257
|
-
|
|
359
|
+
def _confirm_multiple_matches(
|
|
360
|
+
agent: Any,
|
|
361
|
+
file_path: str,
|
|
362
|
+
original_content: str,
|
|
363
|
+
modified_content: str,
|
|
364
|
+
match_count: int,
|
|
365
|
+
search_text: str,
|
|
366
|
+
replace_text: str,
|
|
367
|
+
) -> bool:
|
|
368
|
+
"""使用 agent 确认多个匹配是否应该继续
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
agent: Agent 实例
|
|
372
|
+
file_path: 文件路径
|
|
373
|
+
original_content: 原始文件内容
|
|
374
|
+
modified_content: 修改后的文件内容
|
|
375
|
+
match_count: 匹配次数
|
|
376
|
+
search_text: 搜索文本
|
|
377
|
+
replace_text: 替换文本
|
|
378
|
+
|
|
258
379
|
Returns:
|
|
259
|
-
|
|
380
|
+
True 表示确认继续,False 表示取消
|
|
260
381
|
"""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
content = diff.get("content")
|
|
264
|
-
|
|
265
|
-
if block_id is None:
|
|
266
|
-
return ({
|
|
267
|
-
"success": False,
|
|
268
|
-
"stdout": "",
|
|
269
|
-
"stderr": f"第 {idx+1} 个diff缺少block_id参数",
|
|
270
|
-
}, None)
|
|
271
|
-
if not isinstance(block_id, str):
|
|
272
|
-
return ({
|
|
273
|
-
"success": False,
|
|
274
|
-
"stdout": "",
|
|
275
|
-
"stderr": f"第 {idx+1} 个diff的block_id参数必须是字符串",
|
|
276
|
-
}, None)
|
|
277
|
-
if not block_id.strip():
|
|
278
|
-
return ({
|
|
279
|
-
"success": False,
|
|
280
|
-
"stdout": "",
|
|
281
|
-
"stderr": f"第 {idx+1} 个diff的block_id参数不能为空",
|
|
282
|
-
}, None)
|
|
283
|
-
|
|
284
|
-
if action is None:
|
|
285
|
-
return ({
|
|
286
|
-
"success": False,
|
|
287
|
-
"stdout": "",
|
|
288
|
-
"stderr": f"第 {idx+1} 个diff缺少action参数",
|
|
289
|
-
}, None)
|
|
290
|
-
if not isinstance(action, str):
|
|
291
|
-
return ({
|
|
292
|
-
"success": False,
|
|
293
|
-
"stdout": "",
|
|
294
|
-
"stderr": f"第 {idx+1} 个diff的action参数必须是字符串",
|
|
295
|
-
}, None)
|
|
296
|
-
if action not in ["delete", "insert_before", "insert_after", "replace", "edit"]:
|
|
297
|
-
return ({
|
|
298
|
-
"success": False,
|
|
299
|
-
"stdout": "",
|
|
300
|
-
"stderr": f"第 {idx+1} 个diff的action参数必须是 delete、insert_before、insert_after、replace 或 edit 之一",
|
|
301
|
-
}, None)
|
|
302
|
-
|
|
303
|
-
# 对于edit操作,需要search和replace参数
|
|
304
|
-
if action == "edit":
|
|
305
|
-
search = diff.get("search")
|
|
306
|
-
replace = diff.get("replace")
|
|
307
|
-
if search is None:
|
|
308
|
-
return ({
|
|
309
|
-
"success": False,
|
|
310
|
-
"stdout": "",
|
|
311
|
-
"stderr": f"第 {idx+1} 个diff的action为 edit,需要提供search参数",
|
|
312
|
-
}, None)
|
|
313
|
-
if not isinstance(search, str):
|
|
314
|
-
return ({
|
|
315
|
-
"success": False,
|
|
316
|
-
"stdout": "",
|
|
317
|
-
"stderr": f"第 {idx+1} 个diff的search参数必须是字符串",
|
|
318
|
-
}, None)
|
|
319
|
-
if replace is None:
|
|
320
|
-
return ({
|
|
321
|
-
"success": False,
|
|
322
|
-
"stdout": "",
|
|
323
|
-
"stderr": f"第 {idx+1} 个diff的action为 edit,需要提供replace参数",
|
|
324
|
-
}, None)
|
|
325
|
-
if not isinstance(replace, str):
|
|
326
|
-
return ({
|
|
327
|
-
"success": False,
|
|
328
|
-
"stdout": "",
|
|
329
|
-
"stderr": f"第 {idx+1} 个diff的replace参数必须是字符串",
|
|
330
|
-
}, None)
|
|
331
|
-
# 对于非delete和非edit操作,content是必需的
|
|
332
|
-
elif action != "delete":
|
|
333
|
-
if content is None:
|
|
334
|
-
return ({
|
|
335
|
-
"success": False,
|
|
336
|
-
"stdout": "",
|
|
337
|
-
"stderr": f"第 {idx+1} 个diff的action为 {action},需要提供content参数",
|
|
338
|
-
}, None)
|
|
339
|
-
if not isinstance(content, str):
|
|
340
|
-
return ({
|
|
341
|
-
"success": False,
|
|
342
|
-
"stdout": "",
|
|
343
|
-
"stderr": f"第 {idx+1} 个diff的content参数必须是字符串",
|
|
344
|
-
}, None)
|
|
345
|
-
|
|
346
|
-
patch = {
|
|
347
|
-
"STRUCTURED_BLOCK_ID": block_id,
|
|
348
|
-
"STRUCTURED_ACTION": action,
|
|
349
|
-
}
|
|
350
|
-
if content is not None:
|
|
351
|
-
patch["STRUCTURED_CONTENT"] = content
|
|
352
|
-
if action == "edit":
|
|
353
|
-
patch["STRUCTURED_SEARCH"] = diff.get("search")
|
|
354
|
-
patch["STRUCTURED_REPLACE"] = diff.get("replace")
|
|
355
|
-
return (None, patch)
|
|
382
|
+
try:
|
|
383
|
+
from jarvis.jarvis_agent import Agent
|
|
356
384
|
|
|
385
|
+
agent_instance: Agent = agent
|
|
386
|
+
if not agent_instance or not agent_instance.model:
|
|
387
|
+
# 如果没有 agent 或 model,默认不继续
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
# 生成预览diff
|
|
391
|
+
diff_preview = EditFileNormalTool._generate_diff_preview(
|
|
392
|
+
original_content,
|
|
393
|
+
modified_content,
|
|
394
|
+
file_path,
|
|
395
|
+
match_count,
|
|
396
|
+
search_text,
|
|
397
|
+
replace_text,
|
|
398
|
+
agent=agent_instance,
|
|
399
|
+
token_ratio=0.3, # 使用30%的剩余token用于diff预览
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
prompt = f"""检测到文件编辑操作中,search 文本在文件中存在多处匹配,需要您确认是否继续修改:
|
|
403
|
+
|
|
404
|
+
文件路径:{file_path}
|
|
405
|
+
|
|
406
|
+
匹配统计:
|
|
407
|
+
- 匹配数量: {match_count}
|
|
408
|
+
- 搜索文本长度: {len(search_text)} 字符
|
|
409
|
+
- 替换文本长度: {len(replace_text)} 字符
|
|
410
|
+
|
|
411
|
+
修改预览(diff):
|
|
412
|
+
{diff_preview}
|
|
413
|
+
|
|
414
|
+
请仔细分析以上代码变更,判断这些修改是否合理。可能的情况包括:
|
|
415
|
+
1. 这些匹配位置都是您想要修改的,修改是正确的
|
|
416
|
+
2. 这些匹配位置不是您想要的,或者需要更精确的定位
|
|
417
|
+
3. 修改可能影响其他不相关的代码
|
|
418
|
+
|
|
419
|
+
请使用以下协议回答(必须包含且仅包含以下标记之一):
|
|
420
|
+
- 如果认为这些修改是合理的,回答: <!!!YES!!!>
|
|
421
|
+
- 如果认为这些修改不合理或存在风险,回答: <!!!NO!!!>
|
|
422
|
+
|
|
423
|
+
请严格按照协议格式回答,不要添加其他内容。"""
|
|
424
|
+
|
|
425
|
+
PrettyOutput.auto_print("🤖 正在询问大模型确认多处匹配的修改是否合理...")
|
|
426
|
+
response = agent_instance.model.chat_until_success(prompt)
|
|
427
|
+
response_str = str(response or "")
|
|
428
|
+
|
|
429
|
+
# 使用确定的协议标记解析回答
|
|
430
|
+
if "<!!!YES!!!>" in response_str:
|
|
431
|
+
PrettyOutput.auto_print("✅ 大模型确认:修改合理,继续执行")
|
|
432
|
+
return True
|
|
433
|
+
elif "<!!!NO!!!>" in response_str:
|
|
434
|
+
PrettyOutput.auto_print("⚠️ 大模型确认:修改不合理,取消操作")
|
|
435
|
+
return False
|
|
436
|
+
else:
|
|
437
|
+
# 如果无法找到协议标记,默认认为不合理(保守策略)
|
|
438
|
+
PrettyOutput.auto_print(
|
|
439
|
+
f"⚠️ 无法找到协议标记,默认认为不合理。回答内容: {response_str[:200]}"
|
|
440
|
+
)
|
|
441
|
+
return False
|
|
442
|
+
except Exception as e:
|
|
443
|
+
# 确认过程出错,默认不继续
|
|
444
|
+
PrettyOutput.auto_print(f"⚠️ 确认过程出错: {e},默认取消操作")
|
|
445
|
+
return False
|
|
357
446
|
|
|
358
447
|
@staticmethod
|
|
359
|
-
def
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
448
|
+
def _apply_normal_edits_to_content(
|
|
449
|
+
original_content: str,
|
|
450
|
+
diffs: List[Dict[str, Any]],
|
|
451
|
+
agent: Optional[Any] = None,
|
|
452
|
+
file_path: Optional[str] = None,
|
|
453
|
+
start_idx: int = 0,
|
|
454
|
+
) -> Tuple[bool, str, Optional[Dict[str, Any]], Optional[int]]:
|
|
455
|
+
"""对文件内容按顺序应用普通 search/replace 编辑(使用字符串替换)
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
original_content: 原始文件内容(或已部分修改的内容)
|
|
459
|
+
diffs: diff 列表
|
|
460
|
+
agent: 可选的 agent 实例
|
|
461
|
+
file_path: 可选的文件路径
|
|
462
|
+
start_idx: 从哪个 diff 索引开始处理(0-based,用于继续处理剩余 diffs)
|
|
463
|
+
|
|
464
|
+
返回:
|
|
465
|
+
(是否成功, 新内容或错误信息, 确认信息字典或None, 需要确认的diff索引或None)
|
|
466
|
+
确认信息字典包含: match_count, search_text, replace_text, modified_content, current_content
|
|
364
467
|
"""
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
468
|
+
content = original_content
|
|
469
|
+
|
|
470
|
+
for idx, diff in enumerate(diffs[start_idx:], start=start_idx + 1):
|
|
471
|
+
search = diff["search"]
|
|
472
|
+
replace = diff["replace"]
|
|
473
|
+
|
|
474
|
+
# 处理空字符串search的特殊情况
|
|
475
|
+
if search == "":
|
|
476
|
+
# 空字符串表示直接重写整个文件
|
|
477
|
+
content = replace
|
|
478
|
+
# 空search只处理第一个diff,跳过后续所有diffs
|
|
479
|
+
break
|
|
480
|
+
|
|
481
|
+
# 验证 search 文本
|
|
482
|
+
if not isinstance(search, str):
|
|
483
|
+
error_info = f"第 {idx} 个diff失败:search 文本必须是字符串"
|
|
484
|
+
return False, error_info, None, None
|
|
485
|
+
|
|
486
|
+
# 统计匹配次数
|
|
487
|
+
match_count = EditFileNormalTool._count_matches(content, search)
|
|
488
|
+
|
|
489
|
+
if match_count == 0:
|
|
490
|
+
# 找不到匹配
|
|
491
|
+
error_info = f"第 {idx} 个diff失败:未找到精确匹配的文本"
|
|
492
|
+
if search:
|
|
493
|
+
error_info += f"\n搜索文本: {search[:200]}..."
|
|
494
|
+
error_info += (
|
|
495
|
+
"\n💡 提示:如果搜索文本在文件中存在但未找到匹配,可能是因为:"
|
|
496
|
+
)
|
|
497
|
+
error_info += "\n 1. 搜索文本包含不可见字符或格式不匹配(建议检查空格、换行等)"
|
|
498
|
+
error_info += "\n 2. **文件可能已被更新**:如果文件在其他地方被修改了,搜索文本可能已经不存在或已改变"
|
|
499
|
+
if file_path:
|
|
500
|
+
error_info += f"\n 💡 建议:使用 `read_code` 工具重新读取文件 `{file_path}` 查看当前内容,"
|
|
501
|
+
error_info += "\n 确认文件是否已被更新,然后根据实际内容调整 search 文本"
|
|
502
|
+
return False, error_info, None, None
|
|
503
|
+
|
|
504
|
+
if match_count == 1:
|
|
505
|
+
# 唯一匹配,直接替换
|
|
506
|
+
content = content.replace(search, replace, 1)
|
|
507
|
+
else:
|
|
508
|
+
# 多个匹配,需要确认
|
|
509
|
+
# 生成修改后的内容(替换所有匹配)
|
|
510
|
+
modified_content = content.replace(search, replace)
|
|
511
|
+
# 返回确认信息,包含当前内容以便继续处理后续 diffs
|
|
512
|
+
confirm_info = {
|
|
513
|
+
"match_count": match_count,
|
|
514
|
+
"search_text": search,
|
|
515
|
+
"replace_text": replace,
|
|
516
|
+
"modified_content": modified_content,
|
|
517
|
+
"current_content": content, # 保存当前内容,用于继续处理
|
|
518
|
+
"diff_idx": idx, # 保存当前 diff 索引
|
|
519
|
+
}
|
|
520
|
+
error_info = (
|
|
521
|
+
f"第 {idx} 个diff失败:发现 {match_count} 处匹配,需要确认后再修改"
|
|
522
|
+
)
|
|
523
|
+
return False, error_info, confirm_info, idx
|
|
524
|
+
|
|
525
|
+
return True, content, None, None
|
|
384
526
|
|
|
385
527
|
def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
386
|
-
"""
|
|
528
|
+
"""执行普通 search/replace 文件编辑操作(支持同时修改多个文件)"""
|
|
387
529
|
try:
|
|
388
|
-
#
|
|
389
|
-
error_response =
|
|
530
|
+
# 验证基本参数(files 结构)
|
|
531
|
+
error_response = EditFileNormalTool._validate_basic_args(args)
|
|
390
532
|
if error_response:
|
|
391
533
|
return error_response
|
|
392
|
-
|
|
534
|
+
|
|
393
535
|
files = args.get("files", [])
|
|
394
|
-
agent
|
|
536
|
+
# 获取 agent 实例(v1.0 协议中 agent 在 args 中)
|
|
537
|
+
agent = args.get("agent")
|
|
395
538
|
|
|
396
539
|
# 记录 PATCH 操作调用统计
|
|
397
540
|
try:
|
|
398
541
|
from jarvis.jarvis_stats.stats import StatsManager
|
|
399
542
|
|
|
400
|
-
StatsManager.increment("
|
|
543
|
+
StatsManager.increment("patch_normal", group="tool")
|
|
401
544
|
except Exception:
|
|
402
545
|
pass
|
|
403
546
|
|
|
404
|
-
# 处理每个文件
|
|
405
547
|
all_results = []
|
|
406
548
|
overall_success = True
|
|
407
549
|
successful_files = []
|
|
@@ -411,22 +553,164 @@ class EditFileTool:
|
|
|
411
553
|
file_path = file_item.get("file_path")
|
|
412
554
|
diffs = file_item.get("diffs", [])
|
|
413
555
|
|
|
414
|
-
#
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
556
|
+
# 校验并规范化 diffs
|
|
557
|
+
normalized_diffs: List[Dict[str, Any]] = []
|
|
558
|
+
for idx, diff in enumerate(diffs, start=1):
|
|
559
|
+
if not isinstance(diff, dict):
|
|
560
|
+
all_results.append(
|
|
561
|
+
f"❌ {file_path}: 第 {idx} 个diff必须是字典类型"
|
|
562
|
+
)
|
|
563
|
+
failed_files.append(file_path)
|
|
564
|
+
overall_success = False
|
|
565
|
+
normalized_diffs = []
|
|
566
|
+
break
|
|
567
|
+
|
|
568
|
+
error, normalized = EditFileNormalTool._validate_normal_diff(
|
|
569
|
+
diff, idx
|
|
570
|
+
)
|
|
571
|
+
if error:
|
|
572
|
+
all_results.append(
|
|
573
|
+
f"❌ {file_path}: {error.get('stderr', '参数验证失败')}"
|
|
574
|
+
)
|
|
575
|
+
failed_files.append(file_path)
|
|
576
|
+
overall_success = False
|
|
577
|
+
normalized_diffs = []
|
|
578
|
+
break
|
|
579
|
+
|
|
580
|
+
if normalized is not None:
|
|
581
|
+
normalized_diffs.append(normalized)
|
|
582
|
+
|
|
583
|
+
if not normalized_diffs:
|
|
584
|
+
# 该文件的diffs有问题,已记录错误,跳过
|
|
585
|
+
continue
|
|
586
|
+
|
|
587
|
+
# 读取原始内容并创建备份
|
|
588
|
+
(
|
|
589
|
+
original_content,
|
|
590
|
+
backup_path,
|
|
591
|
+
) = EditFileNormalTool._read_file_with_backup(file_path)
|
|
592
|
+
|
|
593
|
+
# 应用所有普通编辑,使用循环处理所有可能的确认情况
|
|
594
|
+
current_content = original_content
|
|
595
|
+
current_start_idx = 0
|
|
596
|
+
success = False
|
|
597
|
+
result_or_error = ""
|
|
598
|
+
max_confirm_iterations = len(normalized_diffs) * 2 # 防止无限循环
|
|
599
|
+
confirm_iteration = 0
|
|
600
|
+
|
|
601
|
+
while confirm_iteration < max_confirm_iterations:
|
|
602
|
+
(
|
|
603
|
+
iter_success,
|
|
604
|
+
iter_result_or_error,
|
|
605
|
+
iter_confirm_info,
|
|
606
|
+
iter_confirm_diff_idx,
|
|
607
|
+
) = EditFileNormalTool._apply_normal_edits_to_content(
|
|
608
|
+
current_content,
|
|
609
|
+
normalized_diffs,
|
|
610
|
+
agent=agent,
|
|
611
|
+
file_path=file_path,
|
|
612
|
+
start_idx=current_start_idx,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if iter_success:
|
|
616
|
+
# 所有 diffs 处理成功
|
|
617
|
+
success = True
|
|
618
|
+
result_or_error = iter_result_or_error
|
|
619
|
+
break
|
|
620
|
+
|
|
621
|
+
# 处理失败,检查是否需要确认
|
|
622
|
+
if (
|
|
623
|
+
iter_confirm_info
|
|
624
|
+
and agent
|
|
625
|
+
and iter_confirm_diff_idx is not None
|
|
626
|
+
):
|
|
627
|
+
# 需要确认
|
|
628
|
+
confirmed = EditFileNormalTool._confirm_multiple_matches(
|
|
629
|
+
agent,
|
|
630
|
+
file_path,
|
|
631
|
+
original_content,
|
|
632
|
+
iter_confirm_info["modified_content"],
|
|
633
|
+
iter_confirm_info["match_count"],
|
|
634
|
+
iter_confirm_info["search_text"],
|
|
635
|
+
iter_confirm_info["replace_text"],
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
if confirmed:
|
|
639
|
+
# 确认继续,应用当前 diff 的所有匹配替换
|
|
640
|
+
current_content = iter_confirm_info["modified_content"]
|
|
641
|
+
current_diff_idx = iter_confirm_info.get(
|
|
642
|
+
"diff_idx", iter_confirm_diff_idx
|
|
643
|
+
)
|
|
644
|
+
# 从下一个 diff 继续处理
|
|
645
|
+
# current_diff_idx 是 1-based(第几个 diff),转换为 0-based 列表索引
|
|
646
|
+
# 例如:diff_idx=2 表示第 2 个 diff(diffs[1]),下一个是 diffs[2],所以 start_idx=2
|
|
647
|
+
# 注意:current_diff_idx 是 1-based,下一个 diff 的 0-based 索引正好等于 current_diff_idx
|
|
648
|
+
current_start_idx = current_diff_idx
|
|
649
|
+
confirm_iteration += 1
|
|
650
|
+
# 继续循环处理剩余 diffs
|
|
651
|
+
continue
|
|
652
|
+
else:
|
|
653
|
+
# 确认取消
|
|
654
|
+
if backup_path and os.path.exists(backup_path):
|
|
655
|
+
try:
|
|
656
|
+
os.remove(backup_path)
|
|
657
|
+
except Exception:
|
|
658
|
+
pass
|
|
659
|
+
result_or_error = "操作已取消(发现多处匹配,已确认不继续)"
|
|
660
|
+
all_results.append(f"❌ {file_path}: {result_or_error}")
|
|
661
|
+
failed_files.append(file_path)
|
|
662
|
+
overall_success = False
|
|
663
|
+
success = False # 标记为失败,跳出循环
|
|
664
|
+
break
|
|
665
|
+
else:
|
|
666
|
+
# 没有确认信息或没有 agent,直接失败
|
|
667
|
+
success = False
|
|
668
|
+
result_or_error = iter_result_or_error
|
|
669
|
+
break
|
|
670
|
+
|
|
671
|
+
if not success:
|
|
672
|
+
# 处理失败,确保有错误信息
|
|
673
|
+
if confirm_iteration >= max_confirm_iterations:
|
|
674
|
+
# 达到最大确认次数,可能陷入循环
|
|
675
|
+
if not result_or_error:
|
|
676
|
+
result_or_error = f"处理失败:达到最大确认次数限制({max_confirm_iterations}),可能存在循环确认问题"
|
|
677
|
+
elif not result_or_error:
|
|
678
|
+
# 如果没有设置错误信息,使用默认错误信息
|
|
679
|
+
result_or_error = "处理失败:未知错误"
|
|
680
|
+
|
|
681
|
+
# 处理失败
|
|
682
|
+
if backup_path and os.path.exists(backup_path):
|
|
683
|
+
try:
|
|
684
|
+
os.remove(backup_path)
|
|
685
|
+
except Exception:
|
|
686
|
+
pass
|
|
687
|
+
all_results.append(f"❌ {file_path}: {result_or_error}")
|
|
418
688
|
failed_files.append(file_path)
|
|
419
689
|
overall_success = False
|
|
420
690
|
continue
|
|
421
691
|
|
|
422
|
-
#
|
|
423
|
-
|
|
692
|
+
# 编辑成功,继续写入文件
|
|
693
|
+
result_or_error = result_or_error # 此时 result_or_error 是新内容
|
|
424
694
|
|
|
425
|
-
|
|
695
|
+
# 写入文件(失败时回滚)
|
|
696
|
+
abs_path = os.path.abspath(file_path)
|
|
697
|
+
(
|
|
698
|
+
write_success,
|
|
699
|
+
write_error,
|
|
700
|
+
) = EditFileNormalTool._write_file_with_rollback(
|
|
701
|
+
abs_path, result_or_error, backup_path
|
|
702
|
+
)
|
|
703
|
+
if write_success:
|
|
704
|
+
# 写入成功,删除备份文件
|
|
705
|
+
if backup_path and os.path.exists(backup_path):
|
|
706
|
+
try:
|
|
707
|
+
os.remove(backup_path)
|
|
708
|
+
except Exception:
|
|
709
|
+
pass
|
|
426
710
|
all_results.append(f"✅ {file_path}: 修改成功")
|
|
427
711
|
successful_files.append(file_path)
|
|
428
712
|
else:
|
|
429
|
-
all_results.append(f"❌ {file_path}: {
|
|
713
|
+
all_results.append(f"❌ {file_path}: {write_error}")
|
|
430
714
|
failed_files.append(file_path)
|
|
431
715
|
overall_success = False
|
|
432
716
|
|
|
@@ -436,12 +720,12 @@ class EditFileTool:
|
|
|
436
720
|
output_lines.append(f"✅ 成功修改 {len(successful_files)} 个文件:")
|
|
437
721
|
for file_path in successful_files:
|
|
438
722
|
output_lines.append(f" - {file_path}")
|
|
439
|
-
|
|
723
|
+
|
|
440
724
|
if failed_files:
|
|
441
725
|
output_lines.append(f"\n❌ 失败 {len(failed_files)} 个文件:")
|
|
442
726
|
for file_path in failed_files:
|
|
443
727
|
output_lines.append(f" - {file_path}")
|
|
444
|
-
|
|
728
|
+
|
|
445
729
|
stdout_text = "\n".join(all_results)
|
|
446
730
|
summary = "\n".join(output_lines) if output_lines else ""
|
|
447
731
|
|
|
@@ -452,615 +736,30 @@ class EditFileTool:
|
|
|
452
736
|
"stderr": "",
|
|
453
737
|
}
|
|
454
738
|
else:
|
|
739
|
+
# 失败时,stderr 应该包含详细的错误信息,而不仅仅是文件列表
|
|
740
|
+
# 从 all_results 中提取失败文件的详细错误信息
|
|
741
|
+
failed_details = []
|
|
742
|
+
for result in all_results:
|
|
743
|
+
if result.startswith("❌"):
|
|
744
|
+
failed_details.append(result)
|
|
745
|
+
|
|
746
|
+
# 如果有详细的错误信息,使用它们;否则使用 summary
|
|
747
|
+
stderr_content = (
|
|
748
|
+
"\n".join(failed_details)
|
|
749
|
+
if failed_details
|
|
750
|
+
else (summary if summary else "部分文件修改失败")
|
|
751
|
+
)
|
|
752
|
+
|
|
455
753
|
return {
|
|
456
754
|
"success": False,
|
|
457
755
|
"stdout": stdout_text + ("\n\n" + summary if summary else ""),
|
|
458
|
-
"stderr":
|
|
756
|
+
"stderr": stderr_content,
|
|
459
757
|
}
|
|
460
758
|
|
|
461
759
|
except Exception as e:
|
|
462
760
|
error_msg = f"文件编辑失败: {str(e)}"
|
|
463
|
-
|
|
761
|
+
PrettyOutput.auto_print(f"❌ {error_msg}")
|
|
464
762
|
return {"success": False, "stdout": "", "stderr": error_msg}
|
|
465
763
|
|
|
466
|
-
@staticmethod
|
|
467
|
-
def _read_file_with_backup(file_path: str) -> Tuple[str, Optional[str]]:
|
|
468
|
-
"""读取文件并创建备份
|
|
469
|
-
|
|
470
|
-
Args:
|
|
471
|
-
file_path: 文件路径
|
|
472
|
-
|
|
473
|
-
Returns:
|
|
474
|
-
(文件内容, 备份文件路径或None)
|
|
475
|
-
"""
|
|
476
|
-
abs_path = os.path.abspath(file_path)
|
|
477
|
-
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
|
478
|
-
|
|
479
|
-
file_content = ""
|
|
480
|
-
backup_path = None
|
|
481
|
-
if os.path.exists(abs_path):
|
|
482
|
-
with open(abs_path, "r", encoding="utf-8") as f:
|
|
483
|
-
file_content = f.read()
|
|
484
|
-
# 创建备份文件
|
|
485
|
-
backup_path = abs_path + ".bak"
|
|
486
|
-
try:
|
|
487
|
-
shutil.copy2(abs_path, backup_path)
|
|
488
|
-
except Exception:
|
|
489
|
-
# 备份失败不影响主流程
|
|
490
|
-
backup_path = None
|
|
491
|
-
|
|
492
|
-
return file_content, backup_path
|
|
493
|
-
|
|
494
|
-
@staticmethod
|
|
495
|
-
def _order_patches_by_range(patches: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
|
496
|
-
"""按顺序返回补丁列表
|
|
497
|
-
|
|
498
|
-
注意:对于结构化编辑,由于需要在实际应用时才能获取块的行号范围,
|
|
499
|
-
这里暂时按原始顺序返回。如果需要优化,可以在应用时动态排序。
|
|
500
|
-
|
|
501
|
-
Args:
|
|
502
|
-
patches: 补丁列表
|
|
503
|
-
|
|
504
|
-
Returns:
|
|
505
|
-
补丁列表(当前按原始顺序返回)
|
|
506
|
-
"""
|
|
507
|
-
# 对于结构化编辑,暂时按原始顺序处理
|
|
508
|
-
# 如果需要按行号排序,需要在应用时动态获取块的行号范围
|
|
509
|
-
return patches
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
@staticmethod
|
|
513
|
-
def _restore_file_from_cache(cache_info: Dict[str, Any]) -> str:
|
|
514
|
-
"""从缓存恢复文件内容
|
|
515
|
-
|
|
516
|
-
Args:
|
|
517
|
-
cache_info: 缓存信息字典
|
|
518
|
-
|
|
519
|
-
Returns:
|
|
520
|
-
恢复的文件内容字符串(与原始文件内容完全一致)
|
|
521
|
-
"""
|
|
522
|
-
if not cache_info:
|
|
523
|
-
return ""
|
|
524
|
-
|
|
525
|
-
# 按照 id_list 的顺序恢复
|
|
526
|
-
id_list = cache_info.get("id_list", [])
|
|
527
|
-
blocks = cache_info.get("blocks", {})
|
|
528
|
-
file_ends_with_newline = cache_info.get("file_ends_with_newline", False)
|
|
529
|
-
|
|
530
|
-
result = []
|
|
531
|
-
for idx, block_id in enumerate(id_list):
|
|
532
|
-
block = blocks.get(block_id)
|
|
533
|
-
if block:
|
|
534
|
-
content = block.get('content', '')
|
|
535
|
-
if content:
|
|
536
|
-
result.append(content)
|
|
537
|
-
# 在块之间添加换行符(最后一个块后面根据文件是否以换行符结尾决定)
|
|
538
|
-
is_last_block = (idx == len(id_list) - 1)
|
|
539
|
-
if is_last_block:
|
|
540
|
-
# 最后一个块:如果文件以换行符结尾,添加换行符
|
|
541
|
-
if file_ends_with_newline:
|
|
542
|
-
result.append('\n')
|
|
543
|
-
else:
|
|
544
|
-
# 非最后一个块:在块之间添加换行符
|
|
545
|
-
result.append('\n')
|
|
546
|
-
|
|
547
|
-
return ''.join(result) if result else ""
|
|
548
|
-
|
|
549
|
-
@staticmethod
|
|
550
|
-
def _apply_structured_edit_to_cache(
|
|
551
|
-
cache_info: Dict[str, Any],
|
|
552
|
-
block_id: str,
|
|
553
|
-
action: str,
|
|
554
|
-
new_content: Optional[str] = None,
|
|
555
|
-
search: Optional[str] = None,
|
|
556
|
-
replace: Optional[str] = None
|
|
557
|
-
) -> Tuple[bool, Optional[str], Optional[EditErrorType]]:
|
|
558
|
-
"""在缓存中应用结构化编辑
|
|
559
|
-
|
|
560
|
-
Args:
|
|
561
|
-
cache_info: 缓存信息字典(会被修改)
|
|
562
|
-
block_id: 块id(字符串,从read_code工具获取)
|
|
563
|
-
action: 操作类型(delete, insert_before, insert_after, replace, edit)
|
|
564
|
-
new_content: 新内容(对于非delete和非edit操作)
|
|
565
|
-
search: 要搜索的文本(对于edit操作)
|
|
566
|
-
replace: 替换后的文本(对于edit操作)
|
|
567
|
-
|
|
568
|
-
Returns:
|
|
569
|
-
(是否成功, 错误信息, 错误类型)
|
|
570
|
-
"""
|
|
571
|
-
if not cache_info:
|
|
572
|
-
return (False, "缓存信息不完整", EditErrorType.CACHE_INVALID)
|
|
573
|
-
|
|
574
|
-
# 从 blocks 字典中查找
|
|
575
|
-
blocks = cache_info.get("blocks", {})
|
|
576
|
-
block = blocks.get(block_id)
|
|
577
|
-
|
|
578
|
-
if block is None:
|
|
579
|
-
return (False, f"未找到块id: {block_id}。请使用read_code工具查看文件的结构化块id。", EditErrorType.BLOCK_ID_NOT_FOUND)
|
|
580
|
-
|
|
581
|
-
# 根据操作类型执行编辑
|
|
582
|
-
if action == "delete":
|
|
583
|
-
# 删除块:将当前块的内容清空
|
|
584
|
-
block['content'] = ""
|
|
585
|
-
return (True, None, None)
|
|
586
|
-
|
|
587
|
-
elif action == "insert_before":
|
|
588
|
-
# 在块前插入:在当前块的内容前面插入文本
|
|
589
|
-
if new_content is None:
|
|
590
|
-
return (False, "insert_before操作需要提供content参数", EditErrorType.PARAMETER_MISSING)
|
|
591
|
-
|
|
592
|
-
current_content = block.get('content', '')
|
|
593
|
-
# 自动添加换行符:在插入内容后添加换行符(如果插入内容不以换行符结尾)
|
|
594
|
-
if new_content and not new_content.endswith('\n'):
|
|
595
|
-
new_content = new_content + '\n'
|
|
596
|
-
block['content'] = new_content + current_content
|
|
597
|
-
return (True, None, None)
|
|
598
|
-
|
|
599
|
-
elif action == "insert_after":
|
|
600
|
-
# 在块后插入:在当前块的内容后面插入文本
|
|
601
|
-
if new_content is None:
|
|
602
|
-
return (False, "insert_after操作需要提供content参数", EditErrorType.PARAMETER_MISSING)
|
|
603
|
-
|
|
604
|
-
current_content = block.get('content', '')
|
|
605
|
-
# 自动添加换行符:在插入内容前添加换行符(如果插入内容不以换行符开头)
|
|
606
|
-
# 避免重复换行符:如果当前内容以换行符结尾,则不需要添加
|
|
607
|
-
if new_content and not new_content.startswith('\n'):
|
|
608
|
-
# 如果当前内容不以换行符结尾,则在插入内容前添加换行符
|
|
609
|
-
if not current_content or not current_content.endswith('\n'):
|
|
610
|
-
new_content = '\n' + new_content
|
|
611
|
-
block['content'] = current_content + new_content
|
|
612
|
-
return (True, None, None)
|
|
613
|
-
|
|
614
|
-
elif action == "replace":
|
|
615
|
-
# 替换块
|
|
616
|
-
if new_content is None:
|
|
617
|
-
return (False, "replace操作需要提供content参数", EditErrorType.PARAMETER_MISSING)
|
|
618
|
-
|
|
619
|
-
block['content'] = new_content
|
|
620
|
-
return (True, None, None)
|
|
621
|
-
|
|
622
|
-
elif action == "edit":
|
|
623
|
-
# 在块内进行search/replace
|
|
624
|
-
if search is None:
|
|
625
|
-
return (False, "edit操作需要提供search参数", EditErrorType.PARAMETER_MISSING)
|
|
626
|
-
if replace is None:
|
|
627
|
-
return (False, "edit操作需要提供replace参数", EditErrorType.PARAMETER_MISSING)
|
|
628
|
-
|
|
629
|
-
current_content = block.get('content', '')
|
|
630
|
-
|
|
631
|
-
# 检查匹配次数:必须刚好只有一处匹配
|
|
632
|
-
match_count = current_content.count(search)
|
|
633
|
-
if match_count == 0:
|
|
634
|
-
return (False, f"在块 {block_id} 中未找到要搜索的文本: {search[:100]}...", EditErrorType.SEARCH_NOT_FOUND)
|
|
635
|
-
elif match_count > 1:
|
|
636
|
-
# 找到所有匹配位置,并显示上下文
|
|
637
|
-
lines = current_content.split('\n')
|
|
638
|
-
matches_info = []
|
|
639
|
-
search_lines = search.split('\n')
|
|
640
|
-
search_line_count = len(search_lines)
|
|
641
|
-
|
|
642
|
-
# 使用更精确的方法查找所有匹配位置(处理多行搜索)
|
|
643
|
-
start_pos = 0
|
|
644
|
-
match_idx = 0
|
|
645
|
-
while match_idx < match_count and start_pos < len(current_content):
|
|
646
|
-
pos = current_content.find(search, start_pos)
|
|
647
|
-
if pos == -1:
|
|
648
|
-
break
|
|
649
|
-
|
|
650
|
-
# 计算匹配位置所在的行号
|
|
651
|
-
content_before_match = current_content[:pos]
|
|
652
|
-
line_idx = content_before_match.count('\n')
|
|
653
|
-
|
|
654
|
-
# 显示上下文(前后各2行)
|
|
655
|
-
start_line = max(0, line_idx - 2)
|
|
656
|
-
end_line = min(len(lines), line_idx + search_line_count + 2)
|
|
657
|
-
context_lines = lines[start_line:end_line]
|
|
658
|
-
context = '\n'.join([
|
|
659
|
-
f" {start_line + i + 1:4d}: {context_lines[i]}"
|
|
660
|
-
for i in range(len(context_lines))
|
|
661
|
-
])
|
|
662
|
-
|
|
663
|
-
# 标记匹配的行
|
|
664
|
-
match_start_in_context = line_idx - start_line
|
|
665
|
-
match_end_in_context = match_start_in_context + search_line_count
|
|
666
|
-
matches_info.append(f"匹配位置 {len(matches_info) + 1} (行 {line_idx + 1}):\n{context}")
|
|
667
|
-
|
|
668
|
-
start_pos = pos + 1 # 继续查找下一个匹配
|
|
669
|
-
match_idx += 1
|
|
670
|
-
|
|
671
|
-
if len(matches_info) >= 5: # 最多显示5个匹配位置
|
|
672
|
-
break
|
|
673
|
-
|
|
674
|
-
matches_preview = "\n\n".join(matches_info)
|
|
675
|
-
if match_count > len(matches_info):
|
|
676
|
-
matches_preview += f"\n\n... 还有 {match_count - len(matches_info)} 处匹配未显示"
|
|
677
|
-
|
|
678
|
-
search_preview = search[:100] + "..." if len(search) > 100 else search
|
|
679
|
-
error_msg = (
|
|
680
|
-
f"在块 {block_id} 中找到 {match_count} 处匹配,但 edit 操作要求刚好只有一处匹配。\n"
|
|
681
|
-
f"搜索文本: {search_preview}\n\n"
|
|
682
|
-
f"匹配位置详情:\n{matches_preview}\n\n"
|
|
683
|
-
f"💡 提示:请提供更多的上下文(如包含前后几行代码)来唯一标识要替换的位置。"
|
|
684
|
-
)
|
|
685
|
-
return (False, error_msg, EditErrorType.MULTIPLE_MATCHES)
|
|
686
|
-
|
|
687
|
-
# 在块内进行替换(只替换第一次出现,此时已经确认只有一处)
|
|
688
|
-
block['content'] = current_content.replace(search, replace, 1)
|
|
689
|
-
return (True, None, None)
|
|
690
|
-
|
|
691
|
-
else:
|
|
692
|
-
return (False, f"不支持的操作类型: {action}", EditErrorType.UNSUPPORTED_ACTION)
|
|
693
|
-
|
|
694
|
-
@staticmethod
|
|
695
|
-
def _format_patch_description(patch: Dict[str, str]) -> str:
|
|
696
|
-
"""格式化补丁描述用于错误信息
|
|
697
|
-
|
|
698
|
-
Args:
|
|
699
|
-
patch: 补丁字典
|
|
700
|
-
|
|
701
|
-
Returns:
|
|
702
|
-
补丁描述字符串
|
|
703
|
-
"""
|
|
704
|
-
if "STRUCTURED_BLOCK_ID" in patch:
|
|
705
|
-
block_id = patch.get('STRUCTURED_BLOCK_ID', '')
|
|
706
|
-
action = patch.get('STRUCTURED_ACTION', '')
|
|
707
|
-
if action == "edit":
|
|
708
|
-
search = patch.get('STRUCTURED_SEARCH', '')
|
|
709
|
-
replace = patch.get('STRUCTURED_REPLACE', '')
|
|
710
|
-
search_preview = search[:50] + "..." if len(search) > 50 else search
|
|
711
|
-
replace_preview = replace[:50] + "..." if len(replace) > 50 else replace
|
|
712
|
-
return f"结构化编辑: block_id={block_id}, action={action}, search={search_preview}, replace={replace_preview}"
|
|
713
|
-
else:
|
|
714
|
-
content = patch.get('STRUCTURED_CONTENT', '')
|
|
715
|
-
if content:
|
|
716
|
-
content_preview = content[:100] + "..." if len(content) > 100 else content
|
|
717
|
-
return f"结构化编辑: block_id={block_id}, action={action}, content={content_preview}"
|
|
718
|
-
else:
|
|
719
|
-
return f"结构化编辑: block_id={block_id}, action={action}"
|
|
720
|
-
else:
|
|
721
|
-
return "未知的补丁格式"
|
|
722
|
-
|
|
723
|
-
@staticmethod
|
|
724
|
-
def _generate_error_summary(
|
|
725
|
-
abs_path: str,
|
|
726
|
-
failed_patches: List[Dict[str, Any]],
|
|
727
|
-
patch_count: int,
|
|
728
|
-
successful_patches: int
|
|
729
|
-
) -> str:
|
|
730
|
-
"""生成错误摘要
|
|
731
|
-
|
|
732
|
-
Args:
|
|
733
|
-
abs_path: 文件绝对路径
|
|
734
|
-
failed_patches: 失败的补丁列表
|
|
735
|
-
patch_count: 总补丁数
|
|
736
|
-
successful_patches: 成功的补丁数
|
|
737
|
-
|
|
738
|
-
Returns:
|
|
739
|
-
错误摘要字符串
|
|
740
|
-
"""
|
|
741
|
-
error_details = []
|
|
742
|
-
has_block_id_error = False # 是否有块id相关错误
|
|
743
|
-
has_cache_error = False # 是否有缓存相关错误
|
|
744
|
-
has_multiple_matches_error = False # 是否有多处匹配错误
|
|
745
|
-
has_other_error = False # 是否有其他错误
|
|
746
|
-
|
|
747
|
-
for p in failed_patches:
|
|
748
|
-
patch = p["patch"]
|
|
749
|
-
patch_desc = EditFileTool._format_patch_description(patch)
|
|
750
|
-
error_msg = p['error']
|
|
751
|
-
error_type = p.get('error_type') # 获取错误类型(如果存在)
|
|
752
|
-
error_details.append(f" - 失败的补丁: {patch_desc}\n 错误: {error_msg}")
|
|
753
|
-
|
|
754
|
-
# 优先使用错误类型进行判断(如果存在),否则回退到字符串匹配
|
|
755
|
-
if error_type:
|
|
756
|
-
if error_type == EditErrorType.BLOCK_ID_NOT_FOUND:
|
|
757
|
-
has_block_id_error = True
|
|
758
|
-
elif error_type == EditErrorType.CACHE_INVALID:
|
|
759
|
-
has_cache_error = True
|
|
760
|
-
elif error_type == EditErrorType.MULTIPLE_MATCHES:
|
|
761
|
-
has_multiple_matches_error = True
|
|
762
|
-
else:
|
|
763
|
-
has_other_error = True
|
|
764
|
-
else:
|
|
765
|
-
# 回退到字符串匹配(兼容旧代码或异常情况)
|
|
766
|
-
error_msg_lower = error_msg.lower()
|
|
767
|
-
|
|
768
|
-
# 块id相关错误:检查是否包含"块id"和"未找到"/"不存在"/"找不到"等关键词
|
|
769
|
-
if ("块id" in error_msg or "block_id" in error_msg_lower or "block id" in error_msg_lower) and (
|
|
770
|
-
"未找到" in error_msg or "不存在" in error_msg or "找不到" in error_msg or
|
|
771
|
-
"not found" in error_msg_lower
|
|
772
|
-
):
|
|
773
|
-
has_block_id_error = True
|
|
774
|
-
# 缓存相关错误:检查是否包含"缓存"或"cache"关键词
|
|
775
|
-
elif ("缓存" in error_msg or "cache" in error_msg_lower) and (
|
|
776
|
-
"信息不完整" in error_msg or "无效" in error_msg or "过期" in error_msg or
|
|
777
|
-
"invalid" in error_msg_lower or "expired" in error_msg_lower
|
|
778
|
-
):
|
|
779
|
-
has_cache_error = True
|
|
780
|
-
# 多处匹配错误:检查是否包含"匹配"和数量相关的关键词
|
|
781
|
-
elif ("匹配" in error_msg or "match" in error_msg_lower) and (
|
|
782
|
-
"处" in error_msg or "个" in error_msg or "multiple" in error_msg_lower or
|
|
783
|
-
("找到" in error_msg and ("处" in error_msg or "个" in error_msg))
|
|
784
|
-
):
|
|
785
|
-
# 识别多处匹配错误(错误消息中已经包含了详细提示)
|
|
786
|
-
has_multiple_matches_error = True
|
|
787
|
-
else:
|
|
788
|
-
has_other_error = True
|
|
789
|
-
|
|
790
|
-
if successful_patches == 0:
|
|
791
|
-
summary = (
|
|
792
|
-
f"文件 {abs_path} 修改失败(全部失败,文件未修改)。\n"
|
|
793
|
-
f"失败: {len(failed_patches)}/{patch_count}.\n"
|
|
794
|
-
f"失败详情:\n" + "\n".join(error_details)
|
|
795
|
-
)
|
|
796
|
-
else:
|
|
797
|
-
summary = (
|
|
798
|
-
f"文件 {abs_path} 修改部分成功。\n"
|
|
799
|
-
f"成功: {successful_patches}/{patch_count}, "
|
|
800
|
-
f"失败: {len(failed_patches)}/{patch_count}.\n"
|
|
801
|
-
f"失败详情:\n" + "\n".join(error_details)
|
|
802
|
-
)
|
|
803
|
-
|
|
804
|
-
# 根据错误类型添加不同的提示
|
|
805
|
-
# 注意:多处匹配错误的错误消息中已经包含了详细提示,不需要额外添加
|
|
806
|
-
hints = []
|
|
807
|
-
if has_block_id_error:
|
|
808
|
-
hints.append("💡 块id不存在:请检查块id是否正确,或使用 read_code 工具重新读取文件以获取最新的块id列表。")
|
|
809
|
-
if has_cache_error:
|
|
810
|
-
hints.append("💡 缓存问题:文件可能已被外部修改,请使用 read_code 工具重新读取文件。")
|
|
811
|
-
if has_other_error and not (has_block_id_error or has_cache_error or has_multiple_matches_error):
|
|
812
|
-
hints.append("💡 提示:请检查块id、操作类型和参数是否正确。")
|
|
813
|
-
|
|
814
|
-
if hints:
|
|
815
|
-
summary += "\n\n" + "\n".join(hints)
|
|
816
|
-
|
|
817
|
-
return summary
|
|
818
|
-
|
|
819
|
-
@staticmethod
|
|
820
|
-
def _write_file_with_rollback(
|
|
821
|
-
abs_path: str,
|
|
822
|
-
content: str,
|
|
823
|
-
backup_path: Optional[str]
|
|
824
|
-
) -> Tuple[bool, Optional[str]]:
|
|
825
|
-
"""写入文件,失败时回滚
|
|
826
|
-
|
|
827
|
-
Args:
|
|
828
|
-
abs_path: 文件绝对路径
|
|
829
|
-
content: 要写入的内容
|
|
830
|
-
backup_path: 备份文件路径或None
|
|
831
|
-
|
|
832
|
-
Returns:
|
|
833
|
-
(是否成功, 错误信息或None)
|
|
834
|
-
"""
|
|
835
|
-
try:
|
|
836
|
-
with open(abs_path, "w", encoding="utf-8") as f:
|
|
837
|
-
f.write(content)
|
|
838
|
-
return (True, None)
|
|
839
|
-
except Exception as write_error:
|
|
840
|
-
# 写入失败,尝试回滚
|
|
841
|
-
if backup_path and os.path.exists(backup_path):
|
|
842
|
-
try:
|
|
843
|
-
shutil.copy2(backup_path, abs_path)
|
|
844
|
-
os.remove(backup_path)
|
|
845
|
-
except Exception:
|
|
846
|
-
pass
|
|
847
|
-
error_msg = f"文件写入失败: {str(write_error)}"
|
|
848
|
-
print(f"❌ {error_msg}")
|
|
849
|
-
return (False, error_msg)
|
|
850
|
-
|
|
851
|
-
@staticmethod
|
|
852
|
-
def _fast_edit(file_path: str, patches: List[Dict[str, str]], agent: Any = None) -> Tuple[bool, str]:
|
|
853
|
-
"""快速应用补丁到文件
|
|
854
|
-
|
|
855
|
-
该方法基于缓存进行编辑:
|
|
856
|
-
1. 先检查缓存有效性,无效则提示重新读取
|
|
857
|
-
2. 在缓存中应用所有补丁
|
|
858
|
-
3. 从缓存恢复文件内容并写入
|
|
859
|
-
4. 更新缓存的时间戳
|
|
860
|
-
|
|
861
|
-
Args:
|
|
862
|
-
file_path: 要修改的文件路径,支持绝对路径和相对路径
|
|
863
|
-
patches: 补丁列表,每个补丁包含 STRUCTURED_BLOCK_ID
|
|
864
|
-
agent: Agent实例,用于访问缓存
|
|
865
|
-
|
|
866
|
-
Returns:
|
|
867
|
-
Tuple[bool, str]:
|
|
868
|
-
返回处理结果元组,第一个元素表示是否所有补丁都成功应用,
|
|
869
|
-
第二个元素为结果信息,全部成功时为修改后的文件内容,部分或全部失败时为错误信息
|
|
870
|
-
"""
|
|
871
|
-
abs_path = os.path.abspath(file_path)
|
|
872
|
-
backup_path = None
|
|
873
|
-
|
|
874
|
-
try:
|
|
875
|
-
# 检查缓存有效性
|
|
876
|
-
cache_info = EditFileTool._get_file_cache(agent, abs_path)
|
|
877
|
-
if not EditFileTool._is_cache_valid(cache_info, abs_path):
|
|
878
|
-
error_msg = (
|
|
879
|
-
f"⚠️ 缓存无效或文件已被外部修改。\n"
|
|
880
|
-
f"📋 文件: {abs_path}\n"
|
|
881
|
-
f"💡 请先使用 read_code 工具重新读取文件,然后再进行编辑。"
|
|
882
|
-
)
|
|
883
|
-
return False, error_msg
|
|
884
|
-
|
|
885
|
-
# 创建缓存副本,避免直接修改原缓存
|
|
886
|
-
cache_copy = {
|
|
887
|
-
"id_list": list(cache_info["id_list"]), # 浅拷贝列表
|
|
888
|
-
"blocks": {k: v.copy() for k, v in cache_info["blocks"].items()}, # 深拷贝字典
|
|
889
|
-
"total_lines": cache_info["total_lines"],
|
|
890
|
-
"read_time": cache_info.get("read_time", time.time()),
|
|
891
|
-
"file_mtime": cache_info.get("file_mtime", 0),
|
|
892
|
-
"file_ends_with_newline": cache_info.get("file_ends_with_newline", False),
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
# 创建备份
|
|
896
|
-
if os.path.exists(abs_path):
|
|
897
|
-
backup_path = abs_path + ".bak"
|
|
898
|
-
try:
|
|
899
|
-
shutil.copy2(abs_path, backup_path)
|
|
900
|
-
except Exception:
|
|
901
|
-
backup_path = None
|
|
902
|
-
|
|
903
|
-
# 对补丁进行排序
|
|
904
|
-
ordered_patches = EditFileTool._order_patches_by_range(patches)
|
|
905
|
-
patch_count = len(ordered_patches)
|
|
906
|
-
failed_patches: List[Dict[str, Any]] = []
|
|
907
|
-
successful_patches = 0
|
|
908
|
-
|
|
909
|
-
# 在缓存中应用所有补丁
|
|
910
|
-
for patch in ordered_patches:
|
|
911
|
-
# 结构化编辑模式
|
|
912
|
-
if "STRUCTURED_BLOCK_ID" in patch:
|
|
913
|
-
block_id = patch.get("STRUCTURED_BLOCK_ID", "")
|
|
914
|
-
action = patch.get("STRUCTURED_ACTION", "")
|
|
915
|
-
new_content = patch.get("STRUCTURED_CONTENT")
|
|
916
|
-
search = patch.get("STRUCTURED_SEARCH")
|
|
917
|
-
replace = patch.get("STRUCTURED_REPLACE")
|
|
918
|
-
try:
|
|
919
|
-
success, error_msg, error_type = EditFileTool._apply_structured_edit_to_cache(
|
|
920
|
-
cache_copy, block_id, action, new_content, search, replace
|
|
921
|
-
)
|
|
922
|
-
if success:
|
|
923
|
-
successful_patches += 1
|
|
924
|
-
else:
|
|
925
|
-
failed_patches.append({"patch": patch, "error": error_msg, "error_type": error_type})
|
|
926
|
-
except Exception as e:
|
|
927
|
-
error_msg = (
|
|
928
|
-
f"结构化编辑执行出错: {str(e)}\n"
|
|
929
|
-
f"block_id: {block_id}, action: {action}"
|
|
930
|
-
)
|
|
931
|
-
failed_patches.append({"patch": patch, "error": error_msg, "error_type": EditErrorType.OTHER})
|
|
932
|
-
else:
|
|
933
|
-
# 如果不支持的模式,记录错误
|
|
934
|
-
error_msg = "不支持的补丁格式。支持的格式: STRUCTURED_BLOCK_ID"
|
|
935
|
-
failed_patches.append({"patch": patch, "error": error_msg, "error_type": EditErrorType.OTHER})
|
|
936
|
-
|
|
937
|
-
# 如果有失败的补丁,且没有成功的补丁,则不写入文件
|
|
938
|
-
if failed_patches and successful_patches == 0:
|
|
939
|
-
if backup_path and os.path.exists(backup_path):
|
|
940
|
-
try:
|
|
941
|
-
os.remove(backup_path)
|
|
942
|
-
except Exception:
|
|
943
|
-
pass
|
|
944
|
-
summary = EditFileTool._generate_error_summary(
|
|
945
|
-
abs_path, failed_patches, patch_count, successful_patches
|
|
946
|
-
)
|
|
947
|
-
print(f"❌ {summary}")
|
|
948
|
-
return False, summary
|
|
949
|
-
|
|
950
|
-
# 从缓存恢复文件内容
|
|
951
|
-
modified_content = EditFileTool._restore_file_from_cache(cache_copy)
|
|
952
|
-
if not modified_content:
|
|
953
|
-
error_msg = (
|
|
954
|
-
"从缓存恢复文件内容失败。\n"
|
|
955
|
-
"可能原因:缓存数据结构损坏或文件结构异常。\n\n"
|
|
956
|
-
"💡 提示:请使用 read_code 工具重新读取文件,然后再进行编辑。"
|
|
957
|
-
)
|
|
958
|
-
if backup_path and os.path.exists(backup_path):
|
|
959
|
-
try:
|
|
960
|
-
os.remove(backup_path)
|
|
961
|
-
except Exception:
|
|
962
|
-
pass
|
|
963
|
-
return False, error_msg
|
|
964
|
-
|
|
965
|
-
# 写入文件
|
|
966
|
-
success, error_msg = EditFileTool._write_file_with_rollback(abs_path, modified_content, backup_path)
|
|
967
|
-
if not success:
|
|
968
|
-
# 写入失败通常是权限、磁盘空间等问题,不需要重新读取文件
|
|
969
|
-
error_msg += (
|
|
970
|
-
"\n\n💡 提示:文件写入失败,可能是权限不足、磁盘空间不足或文件被锁定。"
|
|
971
|
-
"请检查文件权限和磁盘空间,或稍后重试。"
|
|
972
|
-
)
|
|
973
|
-
return False, error_msg
|
|
974
|
-
|
|
975
|
-
# 写入成功,更新缓存
|
|
976
|
-
if agent:
|
|
977
|
-
cache = agent.get_user_data("read_code_cache")
|
|
978
|
-
if cache and abs_path in cache:
|
|
979
|
-
# 更新缓存内容
|
|
980
|
-
cache[abs_path] = cache_copy
|
|
981
|
-
# 更新缓存时间戳
|
|
982
|
-
EditFileTool._update_cache_timestamp(agent, abs_path)
|
|
983
|
-
agent.set_user_data("read_code_cache", cache)
|
|
984
|
-
|
|
985
|
-
# 写入成功,删除备份文件
|
|
986
|
-
if backup_path and os.path.exists(backup_path):
|
|
987
|
-
try:
|
|
988
|
-
os.remove(backup_path)
|
|
989
|
-
except Exception:
|
|
990
|
-
pass
|
|
991
|
-
|
|
992
|
-
# 如果有失败的补丁,返回部分成功信息
|
|
993
|
-
if failed_patches:
|
|
994
|
-
summary = EditFileTool._generate_error_summary(
|
|
995
|
-
abs_path, failed_patches, patch_count, successful_patches
|
|
996
|
-
)
|
|
997
|
-
print(f"❌ {summary}")
|
|
998
|
-
return False, summary
|
|
999
|
-
|
|
1000
|
-
return True, modified_content
|
|
1001
|
-
|
|
1002
|
-
except Exception as e:
|
|
1003
|
-
# 发生异常时,尝试回滚
|
|
1004
|
-
if backup_path and os.path.exists(backup_path):
|
|
1005
|
-
try:
|
|
1006
|
-
shutil.copy2(backup_path, abs_path)
|
|
1007
|
-
os.remove(backup_path)
|
|
1008
|
-
except Exception:
|
|
1009
|
-
pass
|
|
1010
|
-
|
|
1011
|
-
# 根据异常类型给出不同的提示
|
|
1012
|
-
error_type = type(e).__name__
|
|
1013
|
-
error_str = str(e)
|
|
1014
|
-
|
|
1015
|
-
# 检查是否是权限错误
|
|
1016
|
-
is_permission_error = (
|
|
1017
|
-
error_type == "PermissionError" or
|
|
1018
|
-
(error_type == "OSError" and hasattr(e, 'errno') and e.errno == 13) or
|
|
1019
|
-
"Permission denied" in error_str or
|
|
1020
|
-
"权限" in error_str or
|
|
1021
|
-
"permission" in error_str.lower()
|
|
1022
|
-
)
|
|
1023
|
-
|
|
1024
|
-
# 检查是否是磁盘空间错误
|
|
1025
|
-
is_space_error = (
|
|
1026
|
-
(error_type == "OSError" and hasattr(e, 'errno') and e.errno == 28) or
|
|
1027
|
-
"No space left" in error_str or
|
|
1028
|
-
"No space" in error_str or
|
|
1029
|
-
"ENOSPC" in error_str or
|
|
1030
|
-
"磁盘" in error_str or
|
|
1031
|
-
"空间" in error_str
|
|
1032
|
-
)
|
|
1033
|
-
|
|
1034
|
-
# 检查是否是文件不存在错误
|
|
1035
|
-
is_not_found_error = (
|
|
1036
|
-
error_type == "FileNotFoundError" or
|
|
1037
|
-
(error_type == "OSError" and hasattr(e, 'errno') and e.errno == 2) or
|
|
1038
|
-
"No such file" in error_str or
|
|
1039
|
-
"文件不存在" in error_str
|
|
1040
|
-
)
|
|
1041
|
-
|
|
1042
|
-
# 检查是否是缓存或块相关错误(这些通常是我们自己的错误消息)
|
|
1043
|
-
is_cache_error = (
|
|
1044
|
-
"cache" in error_str.lower() or
|
|
1045
|
-
"缓存" in error_str or
|
|
1046
|
-
"未找到块id" in error_str or
|
|
1047
|
-
"块id" in error_str
|
|
1048
|
-
)
|
|
1049
|
-
|
|
1050
|
-
if is_permission_error:
|
|
1051
|
-
hint = "💡 提示:文件权限不足,请检查文件权限或使用管理员权限运行。"
|
|
1052
|
-
elif is_space_error:
|
|
1053
|
-
hint = "💡 提示:磁盘空间不足,请清理磁盘空间后重试。"
|
|
1054
|
-
elif is_not_found_error:
|
|
1055
|
-
hint = "💡 提示:文件不存在,请检查文件路径是否正确。"
|
|
1056
|
-
elif is_cache_error:
|
|
1057
|
-
hint = "💡 提示:缓存或块id相关错误,请使用 read_code 工具重新读取文件,然后再进行编辑。"
|
|
1058
|
-
elif "block" in error_str.lower() or "块" in error_str:
|
|
1059
|
-
hint = "💡 提示:块操作错误,请使用 read_code 工具重新读取文件,然后再进行编辑。"
|
|
1060
|
-
else:
|
|
1061
|
-
hint = f"💡 提示:发生未知错误({error_type}),请检查错误信息或重试。如问题持续,请使用 read_code 工具重新读取文件。"
|
|
1062
|
-
|
|
1063
|
-
error_msg = f"文件修改失败: {error_str}\n\n{hint}"
|
|
1064
|
-
print(f"❌ {error_msg}")
|
|
1065
|
-
return False, error_msg
|
|
1066
764
|
|
|
765
|
+
__all__ = ["EditFileNormalTool"]
|