jarvis-ai-assistant 0.7.8__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.8.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.8.dist-info/RECORD +0 -218
- {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/top_level.txt +0 -0
|
@@ -1,78 +1,64 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
1
|
"""Jarvis代码代理模块。
|
|
3
2
|
|
|
4
3
|
该模块提供CodeAgent类,用于处理代码修改任务。
|
|
5
4
|
"""
|
|
6
5
|
|
|
6
|
+
import hashlib
|
|
7
7
|
import os
|
|
8
|
+
|
|
9
|
+
from jarvis.jarvis_utils.output import PrettyOutput
|
|
10
|
+
|
|
11
|
+
# -*- coding: utf-8 -*-
|
|
8
12
|
import subprocess
|
|
9
13
|
import sys
|
|
10
|
-
import
|
|
11
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
14
|
+
from typing import Any, Optional
|
|
12
15
|
|
|
13
16
|
import typer
|
|
14
|
-
import yaml
|
|
15
17
|
|
|
16
18
|
from jarvis.jarvis_agent import Agent
|
|
17
19
|
from jarvis.jarvis_agent.events import AFTER_TOOL_CALL
|
|
18
|
-
from jarvis.jarvis_code_agent.lint import (
|
|
19
|
-
get_lint_tools,
|
|
20
|
-
get_lint_commands_for_files,
|
|
21
|
-
group_commands_by_tool,
|
|
22
|
-
get_format_commands_for_files,
|
|
23
|
-
)
|
|
24
|
-
from jarvis.jarvis_code_agent.code_analyzer.build_validator import BuildValidator, BuildResult, FallbackBuildValidator
|
|
25
20
|
from jarvis.jarvis_code_agent.build_validation_config import BuildValidationConfig
|
|
26
|
-
from jarvis.
|
|
21
|
+
from jarvis.jarvis_code_agent.code_agent_build import BuildValidationManager
|
|
22
|
+
from jarvis.jarvis_code_agent.code_agent_diff import DiffManager
|
|
23
|
+
from jarvis.jarvis_code_agent.code_agent_git import GitManager
|
|
24
|
+
from jarvis.jarvis_code_agent.code_agent_impact import ImpactManager
|
|
25
|
+
from jarvis.jarvis_code_agent.code_agent_lint import LintManager
|
|
26
|
+
from jarvis.jarvis_code_agent.code_agent_llm import LLMManager
|
|
27
|
+
from jarvis.jarvis_code_agent.code_agent_postprocess import PostProcessManager
|
|
28
|
+
from jarvis.jarvis_code_agent.code_agent_prompts import get_system_prompt
|
|
29
|
+
from jarvis.jarvis_code_agent.code_agent_rules import RulesManager
|
|
27
30
|
from jarvis.jarvis_code_agent.code_analyzer import ContextManager
|
|
28
|
-
from jarvis.jarvis_code_agent.code_analyzer.llm_context_recommender import
|
|
29
|
-
|
|
30
|
-
from jarvis.jarvis_utils.config import (
|
|
31
|
-
is_confirm_before_apply_patch,
|
|
32
|
-
is_enable_static_analysis,
|
|
33
|
-
is_enable_build_validation,
|
|
34
|
-
get_build_validation_timeout,
|
|
35
|
-
get_git_check_mode,
|
|
36
|
-
set_config,
|
|
37
|
-
get_data_dir,
|
|
38
|
-
is_enable_intent_recognition,
|
|
39
|
-
is_enable_impact_analysis,
|
|
40
|
-
get_smart_platform_name,
|
|
41
|
-
get_smart_model_name,
|
|
31
|
+
from jarvis.jarvis_code_agent.code_analyzer.llm_context_recommender import (
|
|
32
|
+
ContextRecommender,
|
|
42
33
|
)
|
|
43
|
-
from jarvis.jarvis_platform.registry import PlatformRegistry
|
|
44
34
|
from jarvis.jarvis_code_agent.utils import get_project_overview
|
|
45
|
-
from jarvis.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
from jarvis.jarvis_utils.
|
|
58
|
-
from jarvis.jarvis_utils.
|
|
59
|
-
from jarvis.jarvis_utils.
|
|
35
|
+
from jarvis.jarvis_platform.registry import PlatformRegistry
|
|
36
|
+
from jarvis.jarvis_utils.config import get_smart_model_name
|
|
37
|
+
from jarvis.jarvis_utils.config import get_smart_platform_name
|
|
38
|
+
from jarvis.jarvis_utils.config import is_confirm_before_apply_patch
|
|
39
|
+
from jarvis.jarvis_utils.config import is_enable_intent_recognition
|
|
40
|
+
from jarvis.jarvis_utils.config import set_config
|
|
41
|
+
from jarvis.jarvis_utils.git_utils import detect_large_code_deletion
|
|
42
|
+
from jarvis.jarvis_utils.git_utils import find_git_root_and_cd
|
|
43
|
+
from jarvis.jarvis_utils.git_utils import get_commits_between
|
|
44
|
+
from jarvis.jarvis_utils.git_utils import get_diff
|
|
45
|
+
from jarvis.jarvis_utils.git_utils import get_diff_between_commits
|
|
46
|
+
from jarvis.jarvis_utils.git_utils import get_diff_file_list
|
|
47
|
+
from jarvis.jarvis_utils.git_utils import get_latest_commit_hash
|
|
48
|
+
from jarvis.jarvis_utils.git_utils import handle_commit_workflow
|
|
49
|
+
from jarvis.jarvis_utils.git_utils import revert_change
|
|
50
|
+
from jarvis.jarvis_utils.input import get_multiline_input
|
|
51
|
+
from jarvis.jarvis_utils.input import user_confirm
|
|
52
|
+
from jarvis.jarvis_utils.output import OutputType # 保留用于语法高亮
|
|
53
|
+
from jarvis.jarvis_utils.utils import _acquire_single_instance_lock
|
|
54
|
+
from jarvis.jarvis_utils.utils import init_env
|
|
55
|
+
from jarvis.jarvis_utils.tag import ot
|
|
56
|
+
from jarvis.jarvis_utils.globals import set_current_agent
|
|
57
|
+
from jarvis.jarvis_utils.globals import clear_current_agent
|
|
60
58
|
|
|
61
59
|
app = typer.Typer(help="Jarvis 代码助手")
|
|
62
60
|
|
|
63
61
|
|
|
64
|
-
def _format_build_error(result: BuildResult, max_len: int = 2000) -> str:
|
|
65
|
-
"""格式化构建错误信息,限制输出长度"""
|
|
66
|
-
error_msg = result.error_message or ""
|
|
67
|
-
output = result.output or ""
|
|
68
|
-
|
|
69
|
-
full_error = f"{error_msg}\n{output}".strip()
|
|
70
|
-
|
|
71
|
-
if len(full_error) > max_len:
|
|
72
|
-
return full_error[:max_len] + "\n... (输出已截断)"
|
|
73
|
-
return full_error
|
|
74
|
-
|
|
75
|
-
|
|
76
62
|
class CodeAgent(Agent):
|
|
77
63
|
"""Jarvis系统的代码修改代理。
|
|
78
64
|
|
|
@@ -85,27 +71,48 @@ class CodeAgent(Agent):
|
|
|
85
71
|
need_summary: bool = True,
|
|
86
72
|
append_tools: Optional[str] = None,
|
|
87
73
|
tool_group: Optional[str] = None,
|
|
88
|
-
non_interactive: Optional[bool] =
|
|
74
|
+
non_interactive: Optional[bool] = True,
|
|
89
75
|
rule_names: Optional[str] = None,
|
|
90
|
-
|
|
91
|
-
|
|
76
|
+
disable_review: bool = False,
|
|
77
|
+
review_max_iterations: int = 0,
|
|
78
|
+
enable_task_list_manager: bool = True,
|
|
79
|
+
**kwargs: Any,
|
|
80
|
+
) -> None:
|
|
92
81
|
self.root_dir = os.getcwd()
|
|
93
82
|
self.tool_group = tool_group
|
|
83
|
+
# 记录当前是否为非交互模式,便于在提示词/输入中动态调整行为说明
|
|
84
|
+
self.non_interactive: bool = bool(non_interactive)
|
|
85
|
+
# Review 相关配置
|
|
86
|
+
self.disable_review = disable_review
|
|
87
|
+
self.review_max_iterations = review_max_iterations
|
|
88
|
+
|
|
89
|
+
# 存储开始时的commit hash,用于后续git diff获取
|
|
90
|
+
self.start_commit: Optional[str] = None
|
|
94
91
|
|
|
95
92
|
# 初始化上下文管理器
|
|
96
93
|
self.context_manager = ContextManager(self.root_dir)
|
|
97
94
|
# 上下文推荐器将在Agent创建后初始化(需要LLM模型)
|
|
98
95
|
self.context_recommender: Optional[ContextRecommender] = None
|
|
99
96
|
|
|
97
|
+
# 初始化各个管理器
|
|
98
|
+
self.rules_manager = RulesManager(self.root_dir)
|
|
99
|
+
self.git_manager = GitManager(self.root_dir)
|
|
100
|
+
self.diff_manager = DiffManager(self.root_dir)
|
|
101
|
+
self.impact_manager = ImpactManager(self.root_dir, self.context_manager)
|
|
102
|
+
self.build_validation_manager = BuildValidationManager(self.root_dir)
|
|
103
|
+
self.lint_manager = LintManager(self.root_dir)
|
|
104
|
+
self.post_process_manager = PostProcessManager(self.root_dir)
|
|
105
|
+
# LLM管理器将在模型初始化后创建
|
|
106
|
+
|
|
100
107
|
# 检测 git username 和 email 是否已设置
|
|
101
|
-
self.
|
|
108
|
+
self.git_manager.check_git_config()
|
|
102
109
|
base_tools = [
|
|
103
110
|
"execute_script",
|
|
104
111
|
"read_code",
|
|
105
|
-
"edit_file",
|
|
106
|
-
"rewrite_file",
|
|
107
|
-
"lsp_client", # LSP客户端工具,用于获取代码补全、悬停等信息
|
|
112
|
+
"edit_file", # 普通 search/replace 编辑
|
|
108
113
|
]
|
|
114
|
+
if enable_task_list_manager:
|
|
115
|
+
base_tools.append("task_list_manager") # 任务列表管理工具
|
|
109
116
|
|
|
110
117
|
if append_tools:
|
|
111
118
|
additional_tools = [
|
|
@@ -115,48 +122,15 @@ class CodeAgent(Agent):
|
|
|
115
122
|
# 去重
|
|
116
123
|
base_tools = list(dict.fromkeys(base_tools))
|
|
117
124
|
|
|
118
|
-
code_system_prompt =
|
|
119
|
-
|
|
120
|
-
global_rules = self._read_global_rules()
|
|
121
|
-
project_rules = self._read_project_rules()
|
|
122
|
-
|
|
123
|
-
combined_parts: List[str] = []
|
|
124
|
-
loaded_rule_names: List[str] = [] # 记录加载的规则名称
|
|
125
|
-
|
|
126
|
-
if global_rules:
|
|
127
|
-
combined_parts.append(global_rules)
|
|
128
|
-
loaded_rule_names.append("global_rule")
|
|
129
|
-
if project_rules:
|
|
130
|
-
combined_parts.append(project_rules)
|
|
131
|
-
loaded_rule_names.append("project_rule")
|
|
132
|
-
|
|
133
|
-
# 如果指定了 rule_names,从 rules.yaml 文件中读取并添加多个规则
|
|
134
|
-
if rule_names:
|
|
135
|
-
rule_list = [name.strip() for name in rule_names.split(',') if name.strip()]
|
|
136
|
-
for rule_name in rule_list:
|
|
137
|
-
named_rule = self._get_named_rule(rule_name)
|
|
138
|
-
if named_rule:
|
|
139
|
-
combined_parts.append(named_rule)
|
|
140
|
-
loaded_rule_names.append(rule_name)
|
|
141
|
-
|
|
142
|
-
if combined_parts:
|
|
143
|
-
merged_rules = "\n\n".join(combined_parts)
|
|
144
|
-
code_system_prompt = (
|
|
145
|
-
f"{code_system_prompt}\n\n"
|
|
146
|
-
f"<rules>\n{merged_rules}\n</rules>"
|
|
147
|
-
)
|
|
148
|
-
# 显示加载的规则名称
|
|
149
|
-
if loaded_rule_names:
|
|
150
|
-
rules_display = ", ".join(loaded_rule_names)
|
|
151
|
-
print(f"ℹ️ 已加载规则: {rules_display}")
|
|
152
|
-
|
|
125
|
+
code_system_prompt = get_system_prompt()
|
|
126
|
+
|
|
153
127
|
# 调用父类 Agent 的初始化
|
|
154
128
|
# 默认禁用方法论和分析,但允许通过 kwargs 覆盖
|
|
155
129
|
use_methodology = kwargs.pop("use_methodology", False)
|
|
156
130
|
use_analysis = kwargs.pop("use_analysis", False)
|
|
157
131
|
# name 使用传入的值,如果没有传入则使用默认值 "CodeAgent"
|
|
158
132
|
name = kwargs.pop("name", "CodeAgent")
|
|
159
|
-
|
|
133
|
+
|
|
160
134
|
# 准备显式传递给 super().__init__ 的参数
|
|
161
135
|
# 注意:这些参数如果也在 kwargs 中,需要先移除,避免重复传递错误
|
|
162
136
|
explicit_params = {
|
|
@@ -170,12 +144,12 @@ class CodeAgent(Agent):
|
|
|
170
144
|
"non_interactive": non_interactive,
|
|
171
145
|
"use_tools": base_tools,
|
|
172
146
|
}
|
|
173
|
-
|
|
147
|
+
|
|
174
148
|
# 自动移除所有显式传递的参数,避免重复传递错误
|
|
175
149
|
# 这样以后添加新参数时,只要在 explicit_params 中声明,就会自动处理
|
|
176
150
|
for key in explicit_params:
|
|
177
151
|
kwargs.pop(key, None)
|
|
178
|
-
|
|
152
|
+
|
|
179
153
|
super().__init__(
|
|
180
154
|
**explicit_params,
|
|
181
155
|
**kwargs,
|
|
@@ -192,32 +166,36 @@ class CodeAgent(Agent):
|
|
|
192
166
|
parent_model = None
|
|
193
167
|
if self.model:
|
|
194
168
|
parent_model = self.model
|
|
195
|
-
|
|
169
|
+
|
|
196
170
|
self.context_recommender = ContextRecommender(
|
|
197
|
-
self.context_manager,
|
|
198
|
-
parent_model=parent_model
|
|
171
|
+
self.context_manager, parent_model=parent_model
|
|
199
172
|
)
|
|
200
173
|
except Exception as e:
|
|
201
174
|
# LLM推荐器初始化失败
|
|
202
|
-
|
|
175
|
+
PrettyOutput.auto_print(
|
|
176
|
+
f"⚠️ 上下文推荐器初始化失败: {e},将跳过上下文推荐功能"
|
|
177
|
+
)
|
|
203
178
|
|
|
204
179
|
self.event_bus.subscribe(AFTER_TOOL_CALL, self._on_after_tool_call)
|
|
205
|
-
|
|
180
|
+
|
|
206
181
|
# 打印语言功能支持表格
|
|
207
182
|
try:
|
|
208
|
-
from jarvis.jarvis_agent.language_support_info import
|
|
183
|
+
from jarvis.jarvis_agent.language_support_info import (
|
|
184
|
+
print_language_support_table,
|
|
185
|
+
)
|
|
186
|
+
|
|
209
187
|
print_language_support_table()
|
|
210
188
|
except Exception:
|
|
211
189
|
pass
|
|
212
190
|
|
|
213
|
-
def _init_model(self, model_group: Optional[str]):
|
|
191
|
+
def _init_model(self, model_group: Optional[str]) -> None:
|
|
214
192
|
"""初始化模型平台(CodeAgent使用smart平台,适用于代码生成等复杂场景)"""
|
|
215
193
|
platform_name = get_smart_platform_name(model_group)
|
|
216
194
|
model_name = get_smart_model_name(model_group)
|
|
217
195
|
|
|
218
196
|
maybe_model = PlatformRegistry().create_platform(platform_name)
|
|
219
197
|
if maybe_model is None:
|
|
220
|
-
|
|
198
|
+
PrettyOutput.auto_print(f"⚠️ 平台 {platform_name} 不存在,将使用smart模型")
|
|
221
199
|
maybe_model = PlatformRegistry().get_smart_platform()
|
|
222
200
|
|
|
223
201
|
# 在此处收敛为非可选类型,确保后续赋值满足类型检查
|
|
@@ -229,690 +207,19 @@ class CodeAgent(Agent):
|
|
|
229
207
|
self.model.set_model_group(model_group)
|
|
230
208
|
self.model.set_suppress_output(False)
|
|
231
209
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
你是Jarvis代码工程师,专注于**项目级代码分析、精准修改与问题排查**,核心原则:自主决策不犹豫、高效精准不冗余、修改审慎可回退、工具精通不臆测。
|
|
236
|
-
|
|
237
|
-
工作流程(闭环执行,每步必落地):
|
|
238
|
-
1. 需求拆解与项目对齐:
|
|
239
|
-
- 先明确用户需求的核心目标(如“修复XX报错”“新增XX功能”“优化XX性能”),标注关键约束(如“兼容Python 3.8+”“不修改核心依赖”);
|
|
240
|
-
- 快速定位项目核心目录(如src/、main/)、技术栈(语言/框架/版本)、代码风格规范(如PEP8、ESLint规则),避免无的放矢。
|
|
241
|
-
|
|
242
|
-
2. 目标文件精准定位(工具优先,拒绝盲搜):
|
|
243
|
-
- 优先通过 lsp_client 的 search_symbol(符号搜索)定位关联文件(如函数、类、变量所属文件);
|
|
244
|
-
- 若符号不明确,用全文搜索工具按“关键词+文件类型过滤”(如“关键词:user_login + 后缀:.py”)缩小范围;
|
|
245
|
-
- 仅当工具无法定位时,才用 read_code 读取疑似目录下的核心文件(如入口文件、配置文件),避免无效读取。
|
|
246
|
-
|
|
247
|
-
3. 代码深度分析(基于工具,禁止虚构):
|
|
248
|
-
- 符号分析:用 lsp_client 的 document_symbols(文档符号)、get_symbol_info(符号详情)、definition(定义跳转)、references(引用查询),确认符号的作用域、依赖关系、调用链路;
|
|
249
|
-
- 内容分析:用 read_code 读取目标文件完整内容,重点关注“逻辑分支、异常处理、依赖引入、配置参数”,记录关键代码片段(如报错位置、待修改逻辑);
|
|
250
|
-
- 影响范围评估:用 lsp_client 的 references 查询待修改符号的所有引用场景,预判修改可能波及的模块,避免“改一处崩一片”。
|
|
251
|
-
|
|
252
|
-
4. 最小变更方案设计(可回退、易维护):
|
|
253
|
-
- 优先选择“局部修改”(如修改函数内逻辑、补充条件判断),而非“重构”或“全文件重写”;
|
|
254
|
-
- 方案需满足:① 覆盖需求核心;② 不破坏现有功能;③ 符合项目代码风格;④ 便于后续回退(如仅修改必要行,不删无关代码);
|
|
255
|
-
- 若需修改核心逻辑(如公共函数、配置文件),先记录原始代码片段(如用临时文件保存到 /tmp/backup_xxx.txt),再执行修改。
|
|
256
|
-
|
|
257
|
-
5. 先读后写,精准执行(工具规范使用):
|
|
258
|
-
- 必须先通过 read_code 读取目标文件完整内容,确认待修改位置的上下文(如前后代码逻辑、缩进格式),再调用编辑工具;
|
|
259
|
-
- 编辑工具选择:
|
|
260
|
-
- 局部修改(改少数行、补代码块):用 edit_file,明确标注“修改范围(行号/代码片段)+ 修改内容”(如“替换第15-20行的循环逻辑为:xxx”);
|
|
261
|
-
- 全文件重写(如格式统一、逻辑重构):仅当局部修改无法满足需求时使用 rewrite_file,重写前必须备份原始文件到 /tmp/rewrite_backup_xxx.txt。
|
|
262
|
-
|
|
263
|
-
6. 验证与兜底(避免无效交付):
|
|
264
|
-
- 修改后优先通过 lsp_client 的语法检查功能(若支持)验证代码无语法错误;
|
|
265
|
-
- 若涉及功能变更,建议补充1-2行核心测试用例(或提示用户验证场景),确保修改生效;
|
|
266
|
-
- 记录修改日志(保存到 /tmp/modify_log_xxx.txt),内容包括:修改时间、目标文件、修改原因、原始代码片段、修改后代码片段,便于问题追溯。
|
|
267
|
-
|
|
268
|
-
工具使用规范(精准调用,不浪费资源):
|
|
269
|
-
- lsp_client:仅传递有效参数(如符号名精准、文件路径明确),避免模糊查询(如无关键词的全局搜索);
|
|
270
|
-
- 全文搜索:必须添加“文件类型过滤”“目录过滤”,减少无效结果(如仅搜索 src/ 目录下的 .java 文件);
|
|
271
|
-
- read_code:仅读取目标文件和关联依赖文件,不读取日志、测试数据、第三方依赖包等无关文件;
|
|
272
|
-
- edit_file/rewrite_file:修改后必须保持代码缩进、命名规范与原文件一致(如原文件用4空格缩进,不改为2空格),不引入多余空行、注释。
|
|
273
|
-
|
|
274
|
-
代码质量约束(底线要求,不可突破):
|
|
275
|
-
1. 语法正确性:修改后代码无语法错误、无未定义变量/函数、无依赖缺失;
|
|
276
|
-
2. 功能兼容性:不破坏现有正常功能,修改后的代码能适配项目已有的调用场景;
|
|
277
|
-
3. 风格一致性:严格遵循项目既有风格(如命名规范、缩进、注释格式),不引入个人风格;
|
|
278
|
-
4. 可维护性:修改逻辑清晰,关键改动可加简洁注释(如“// 修复XX报错:XX场景下变量未初始化”),不写“魔法值”“冗余代码”。
|
|
279
|
-
|
|
280
|
-
调试指引(问题闭环,高效排查):
|
|
281
|
-
- 定位报错:优先用 lsp_client 定位报错位置,结合 read_code 查看上下文,确认报错类型(语法错/逻辑错/运行时错);
|
|
282
|
-
- 日志补充:若报错模糊,在关键位置(如函数入口、循环内、异常捕获前)增加打印日志,内容包括“变量值、执行步骤、时间戳”(如 print(f"[DEBUG] user_login: username={username}, status={status}")),日志输出到 /tmp/ 目录,不污染项目日志;
|
|
283
|
-
- 中间结果保存:复杂逻辑修改时,用临时文件(/tmp/temp_result_xxx.txt)保存中间数据(如计算结果、接口返回值),便于验证逻辑正确性;
|
|
284
|
-
- 回退机制:若修改后出现新问题,立即用备份文件回退,重新分析,不盲目叠加修改。
|
|
285
|
-
|
|
286
|
-
禁止行为(红线不可碰):
|
|
287
|
-
1. 禁止虚构代码、依赖、文件路径,所有结论必须基于工具返回结果或实际读取的代码;
|
|
288
|
-
2. 禁止无差别读取项目所有文件,避免浪费资源;
|
|
289
|
-
3. 禁止大篇幅删除、重构未明确要求修改的代码;
|
|
290
|
-
4. 禁止引入项目未依赖的第三方库(除非用户明确允许);
|
|
291
|
-
5. 禁止修改 /tmp/ 以外的非项目目录文件,避免污染环境。
|
|
292
|
-
|
|
293
|
-
"""
|
|
294
|
-
|
|
295
|
-
def _read_project_rules(self) -> Optional[str]:
|
|
296
|
-
"""读取 .jarvis/rules 内容,如果存在则返回字符串,否则返回 None"""
|
|
210
|
+
# 初始化LLM管理器(使用普通模型,不使用smart模型)
|
|
211
|
+
self.llm_manager = LLMManager(parent_model=self.model, model_group=model_group)
|
|
212
|
+
# 同步模型组到全局,便于后续工具(如提交信息生成)获取一致的模型配置
|
|
297
213
|
try:
|
|
298
|
-
|
|
299
|
-
if os.path.exists(rules_path) and os.path.isfile(rules_path):
|
|
300
|
-
with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
|
|
301
|
-
content = f.read().strip()
|
|
302
|
-
return content if content else None
|
|
303
|
-
except Exception:
|
|
304
|
-
# 读取规则失败时忽略,不影响主流程
|
|
305
|
-
pass
|
|
306
|
-
return None
|
|
214
|
+
from jarvis.jarvis_utils.globals import set_global_model_group
|
|
307
215
|
|
|
308
|
-
|
|
309
|
-
"""读取数据目录 rules 内容,如果存在则返回字符串,否则返回 None"""
|
|
310
|
-
try:
|
|
311
|
-
rules_path = os.path.join(get_data_dir(), "rule")
|
|
312
|
-
if os.path.exists(rules_path) and os.path.isfile(rules_path):
|
|
313
|
-
with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
|
|
314
|
-
content = f.read().strip()
|
|
315
|
-
return content if content else None
|
|
216
|
+
set_global_model_group(model_group)
|
|
316
217
|
except Exception:
|
|
317
|
-
#
|
|
218
|
+
# 若全局同步失败,不影响主流程
|
|
318
219
|
pass
|
|
319
|
-
return None
|
|
320
|
-
|
|
321
|
-
def _get_named_rule(self, rule_name: str) -> Optional[str]:
|
|
322
|
-
"""从 rules.yaml 文件中获取指定名称的规则
|
|
323
|
-
|
|
324
|
-
参数:
|
|
325
|
-
rule_name: 规则名称
|
|
326
|
-
|
|
327
|
-
返回:
|
|
328
|
-
str: 规则内容,如果未找到则返回 None
|
|
329
|
-
"""
|
|
330
|
-
try:
|
|
331
|
-
# 读取全局数据目录下的 rules.yaml
|
|
332
|
-
global_rules_yaml_path = os.path.join(get_data_dir(), "rules.yaml")
|
|
333
|
-
global_rules = {}
|
|
334
|
-
if os.path.exists(global_rules_yaml_path) and os.path.isfile(global_rules_yaml_path):
|
|
335
|
-
with open(global_rules_yaml_path, "r", encoding="utf-8", errors="replace") as f:
|
|
336
|
-
global_rules = yaml.safe_load(f) or {}
|
|
337
|
-
|
|
338
|
-
# 读取 git 根目录下的 rules.yaml
|
|
339
|
-
project_rules_yaml_path = os.path.join(self.root_dir, "rules.yaml")
|
|
340
|
-
project_rules = {}
|
|
341
|
-
if os.path.exists(project_rules_yaml_path) and os.path.isfile(project_rules_yaml_path):
|
|
342
|
-
with open(project_rules_yaml_path, "r", encoding="utf-8", errors="replace") as f:
|
|
343
|
-
project_rules = yaml.safe_load(f) or {}
|
|
344
|
-
|
|
345
|
-
# 合并配置:项目配置覆盖全局配置
|
|
346
|
-
merged_rules = {**global_rules, **project_rules}
|
|
347
|
-
|
|
348
|
-
# 查找指定的规则
|
|
349
|
-
if rule_name in merged_rules:
|
|
350
|
-
rule_value = merged_rules[rule_name]
|
|
351
|
-
# 如果值是字符串,直接返回
|
|
352
|
-
if isinstance(rule_value, str):
|
|
353
|
-
return rule_value.strip() if rule_value.strip() else None
|
|
354
|
-
# 如果值是其他类型,转换为字符串
|
|
355
|
-
return str(rule_value).strip() if str(rule_value).strip() else None
|
|
356
|
-
|
|
357
|
-
return None
|
|
358
|
-
except Exception as e:
|
|
359
|
-
# 读取规则失败时忽略,不影响主流程
|
|
360
|
-
print(f"⚠️ 读取 rules.yaml 失败: {e}")
|
|
361
|
-
return None
|
|
362
|
-
|
|
363
|
-
def _check_git_config(self) -> None:
|
|
364
|
-
"""检查 git username 和 email 是否已设置,如果没有则提示并退出"""
|
|
365
|
-
try:
|
|
366
|
-
# 检查 git user.name
|
|
367
|
-
result = subprocess.run(
|
|
368
|
-
["git", "config", "--get", "user.name"],
|
|
369
|
-
capture_output=True,
|
|
370
|
-
text=True,
|
|
371
|
-
check=False,
|
|
372
|
-
)
|
|
373
|
-
username = result.stdout.strip()
|
|
374
|
-
|
|
375
|
-
# 检查 git user.email
|
|
376
|
-
result = subprocess.run(
|
|
377
|
-
["git", "config", "--get", "user.email"],
|
|
378
|
-
capture_output=True,
|
|
379
|
-
text=True,
|
|
380
|
-
check=False,
|
|
381
|
-
)
|
|
382
|
-
email = result.stdout.strip()
|
|
383
|
-
|
|
384
|
-
# 如果任一配置未设置,提示并退出
|
|
385
|
-
if not username or not email:
|
|
386
|
-
missing_configs = []
|
|
387
|
-
if not username:
|
|
388
|
-
missing_configs.append(
|
|
389
|
-
' git config --global user.name "Your Name"'
|
|
390
|
-
)
|
|
391
|
-
if not email:
|
|
392
|
-
missing_configs.append(
|
|
393
|
-
' git config --global user.email "your.email@example.com"'
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
message = "❌ Git 配置不完整\n\n请运行以下命令配置 Git:\n" + "\n".join(
|
|
397
|
-
missing_configs
|
|
398
|
-
)
|
|
399
|
-
print(f"⚠️ {message}")
|
|
400
|
-
# 通过配置控制严格校验模式(JARVIS_GIT_CHECK_MODE):
|
|
401
|
-
# - warn: 仅告警并继续,后续提交可能失败
|
|
402
|
-
# - strict: 严格模式(默认),直接退出
|
|
403
|
-
mode = get_git_check_mode().lower()
|
|
404
|
-
if mode == "warn":
|
|
405
|
-
print("ℹ️ 已启用 Git 校验警告模式(JARVIS_GIT_CHECK_MODE=warn),将继续运行。"
|
|
406
|
-
"注意:后续提交可能失败,请尽快配置 git user.name 与 user.email。")
|
|
407
|
-
return
|
|
408
|
-
sys.exit(1)
|
|
409
|
-
|
|
410
|
-
except FileNotFoundError:
|
|
411
|
-
print("❌ 未找到 git 命令,请先安装 Git")
|
|
412
|
-
sys.exit(1)
|
|
413
|
-
except Exception as e:
|
|
414
|
-
print(f"❌ 检查 Git 配置时出错: {str(e)}")
|
|
415
|
-
sys.exit(1)
|
|
416
|
-
|
|
417
|
-
def _find_git_root(self) -> str:
|
|
418
|
-
"""查找并切换到git根目录
|
|
419
|
-
|
|
420
|
-
返回:
|
|
421
|
-
str: git根目录路径
|
|
422
|
-
"""
|
|
423
|
-
|
|
424
|
-
curr_dir = os.getcwd()
|
|
425
|
-
git_dir = find_git_root_and_cd(curr_dir)
|
|
426
|
-
self.root_dir = git_dir
|
|
427
|
-
|
|
428
|
-
return git_dir
|
|
429
|
-
|
|
430
|
-
def _update_gitignore(self, git_dir: str) -> None:
|
|
431
|
-
"""检查并更新.gitignore文件,确保忽略.jarvis目录,并追加常用语言的忽略规则(若缺失)
|
|
432
|
-
|
|
433
|
-
参数:
|
|
434
|
-
git_dir: git根目录路径
|
|
435
|
-
"""
|
|
436
|
-
gitignore_path = os.path.join(git_dir, ".gitignore")
|
|
437
|
-
|
|
438
|
-
# 常用忽略规则(按语言/场景分组)
|
|
439
|
-
sections = {
|
|
440
|
-
"General": [
|
|
441
|
-
".jarvis",
|
|
442
|
-
".DS_Store",
|
|
443
|
-
"Thumbs.db",
|
|
444
|
-
"*.log",
|
|
445
|
-
"*.tmp",
|
|
446
|
-
"*.swp",
|
|
447
|
-
"*.swo",
|
|
448
|
-
".idea/",
|
|
449
|
-
".vscode/",
|
|
450
|
-
],
|
|
451
|
-
"Python": [
|
|
452
|
-
"__pycache__/",
|
|
453
|
-
"*.py[cod]",
|
|
454
|
-
"*$py.class",
|
|
455
|
-
".Python",
|
|
456
|
-
"env/",
|
|
457
|
-
"venv/",
|
|
458
|
-
".venv/",
|
|
459
|
-
"build/",
|
|
460
|
-
"dist/",
|
|
461
|
-
"develop-eggs/",
|
|
462
|
-
"downloads/",
|
|
463
|
-
"eggs/",
|
|
464
|
-
".eggs/",
|
|
465
|
-
"lib/",
|
|
466
|
-
"lib64/",
|
|
467
|
-
"parts/",
|
|
468
|
-
"sdist/",
|
|
469
|
-
"var/",
|
|
470
|
-
"wheels/",
|
|
471
|
-
"pip-wheel-metadata/",
|
|
472
|
-
"share/python-wheels/",
|
|
473
|
-
"*.egg-info/",
|
|
474
|
-
".installed.cfg",
|
|
475
|
-
"*.egg",
|
|
476
|
-
"MANIFEST",
|
|
477
|
-
".mypy_cache/",
|
|
478
|
-
".pytest_cache/",
|
|
479
|
-
".ruff_cache/",
|
|
480
|
-
".tox/",
|
|
481
|
-
".coverage",
|
|
482
|
-
".coverage.*",
|
|
483
|
-
"htmlcov/",
|
|
484
|
-
".hypothesis/",
|
|
485
|
-
".ipynb_checkpoints",
|
|
486
|
-
".pyre/",
|
|
487
|
-
".pytype/",
|
|
488
|
-
],
|
|
489
|
-
"Rust": [
|
|
490
|
-
"target/",
|
|
491
|
-
],
|
|
492
|
-
"Node": [
|
|
493
|
-
"node_modules/",
|
|
494
|
-
"npm-debug.log*",
|
|
495
|
-
"yarn-debug.log*",
|
|
496
|
-
"yarn-error.log*",
|
|
497
|
-
"pnpm-debug.log*",
|
|
498
|
-
"lerna-debug.log*",
|
|
499
|
-
"dist/",
|
|
500
|
-
"coverage/",
|
|
501
|
-
".turbo/",
|
|
502
|
-
".next/",
|
|
503
|
-
".nuxt/",
|
|
504
|
-
"out/",
|
|
505
|
-
],
|
|
506
|
-
"Go": [
|
|
507
|
-
"bin/",
|
|
508
|
-
"vendor/",
|
|
509
|
-
"coverage.out",
|
|
510
|
-
],
|
|
511
|
-
"Java": [
|
|
512
|
-
"target/",
|
|
513
|
-
"*.class",
|
|
514
|
-
".gradle/",
|
|
515
|
-
"build/",
|
|
516
|
-
"out/",
|
|
517
|
-
],
|
|
518
|
-
"C/C++": [
|
|
519
|
-
"build/",
|
|
520
|
-
"cmake-build-*/",
|
|
521
|
-
"*.o",
|
|
522
|
-
"*.a",
|
|
523
|
-
"*.so",
|
|
524
|
-
"*.obj",
|
|
525
|
-
"*.dll",
|
|
526
|
-
"*.dylib",
|
|
527
|
-
"*.exe",
|
|
528
|
-
"*.pdb",
|
|
529
|
-
],
|
|
530
|
-
".NET": [
|
|
531
|
-
"bin/",
|
|
532
|
-
"obj/",
|
|
533
|
-
],
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
existing_content = ""
|
|
537
|
-
if os.path.exists(gitignore_path):
|
|
538
|
-
with open(gitignore_path, "r", encoding="utf-8", errors="replace") as f:
|
|
539
|
-
existing_content = f.read()
|
|
540
|
-
|
|
541
|
-
# 已存在的忽略项(去除注释与空行)
|
|
542
|
-
existing_set = set(
|
|
543
|
-
ln.strip()
|
|
544
|
-
for ln in existing_content.splitlines()
|
|
545
|
-
if ln.strip() and not ln.strip().startswith("#")
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
# 计算缺失项并准备追加内容
|
|
549
|
-
new_lines: List[str] = []
|
|
550
|
-
for name, patterns in sections.items():
|
|
551
|
-
missing = [p for p in patterns if p not in existing_set]
|
|
552
|
-
if missing:
|
|
553
|
-
new_lines.append(f"# {name}")
|
|
554
|
-
new_lines.extend(missing)
|
|
555
|
-
new_lines.append("") # 分组空行
|
|
556
|
-
|
|
557
|
-
if not os.path.exists(gitignore_path):
|
|
558
|
-
# 新建 .gitignore(仅包含缺失项;此处即为全部常用规则)
|
|
559
|
-
with open(gitignore_path, "w", encoding="utf-8", newline="\n") as f:
|
|
560
|
-
content_to_write = "\n".join(new_lines).rstrip()
|
|
561
|
-
if content_to_write:
|
|
562
|
-
f.write(content_to_write + "\n")
|
|
563
|
-
print("✅ 已创建 .gitignore 并添加常用忽略规则")
|
|
564
|
-
else:
|
|
565
|
-
if new_lines:
|
|
566
|
-
# 追加缺失的规则
|
|
567
|
-
with open(gitignore_path, "a", encoding="utf-8", newline="\n") as f:
|
|
568
|
-
# 若原文件不以换行结尾,先补一行
|
|
569
|
-
if existing_content and not existing_content.endswith("\n"):
|
|
570
|
-
f.write("\n")
|
|
571
|
-
f.write("\n".join(new_lines).rstrip() + "\n")
|
|
572
|
-
print("✅ 已更新 .gitignore,追加常用忽略规则")
|
|
573
|
-
|
|
574
|
-
def _handle_git_changes(self, prefix: str, suffix: str) -> None:
|
|
575
|
-
"""处理git仓库中的未提交修改"""
|
|
576
|
-
|
|
577
|
-
if has_uncommitted_changes():
|
|
578
|
-
|
|
579
|
-
git_commiter = GitCommitTool()
|
|
580
|
-
git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self, "model_group": getattr(self.model, "model_group", None)})
|
|
581
|
-
|
|
582
|
-
def _init_env(self, prefix: str, suffix: str) -> None:
|
|
583
|
-
"""初始化环境,组合以下功能:
|
|
584
|
-
1. 查找git根目录
|
|
585
|
-
2. 检查并更新.gitignore文件
|
|
586
|
-
3. 处理未提交的修改
|
|
587
|
-
4. 配置git对换行符变化不敏感
|
|
588
|
-
"""
|
|
589
|
-
|
|
590
|
-
git_dir = self._find_git_root()
|
|
591
|
-
self._update_gitignore(git_dir)
|
|
592
|
-
self._handle_git_changes(prefix, suffix)
|
|
593
|
-
# 配置git对换行符变化不敏感
|
|
594
|
-
self._configure_line_ending_settings()
|
|
595
|
-
|
|
596
|
-
def _configure_line_ending_settings(self) -> None:
|
|
597
|
-
"""配置git对换行符变化不敏感,只在当前设置与目标设置不一致时修改"""
|
|
598
|
-
target_settings = {
|
|
599
|
-
"core.autocrlf": "false",
|
|
600
|
-
"core.safecrlf": "false",
|
|
601
|
-
"core.whitespace": "cr-at-eol", # 忽略行尾的CR
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
# 获取当前设置并检查是否需要修改
|
|
605
|
-
need_change = False
|
|
606
|
-
current_settings = {}
|
|
607
|
-
for key, target_value in target_settings.items():
|
|
608
|
-
result = subprocess.run(
|
|
609
|
-
["git", "config", "--get", key],
|
|
610
|
-
capture_output=True,
|
|
611
|
-
text=True,
|
|
612
|
-
check=False,
|
|
613
|
-
)
|
|
614
|
-
current_value = result.stdout.strip()
|
|
615
|
-
current_settings[key] = current_value
|
|
616
|
-
if current_value != target_value:
|
|
617
|
-
need_change = True
|
|
618
|
-
|
|
619
|
-
if not need_change:
|
|
620
|
-
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
print("⚠️ 正在修改git换行符敏感设置,这会影响所有文件的换行符处理方式")
|
|
624
|
-
# 避免在循环中逐条打印,先拼接后统一打印
|
|
625
|
-
lines = ["将进行以下设置:"]
|
|
626
|
-
for key, value in target_settings.items():
|
|
627
|
-
current = current_settings.get(key, "未设置")
|
|
628
|
-
lines.append(f"{key}: {current} -> {value}")
|
|
629
|
-
joined_lines = '\n'.join(lines)
|
|
630
|
-
print(f"ℹ️ {joined_lines}")
|
|
631
|
-
|
|
632
|
-
# 直接执行设置,不需要用户确认
|
|
633
|
-
for key, value in target_settings.items():
|
|
634
|
-
subprocess.run(["git", "config", key, value], check=True)
|
|
635
|
-
|
|
636
|
-
# 对于Windows系统,提示用户可以创建.gitattributes文件
|
|
637
|
-
if sys.platform.startswith("win"):
|
|
638
|
-
self._handle_windows_line_endings()
|
|
639
|
-
|
|
640
|
-
print("✅ git换行符敏感设置已更新")
|
|
641
|
-
|
|
642
|
-
def _handle_windows_line_endings(self) -> None:
|
|
643
|
-
"""在Windows系统上处理换行符问题,提供建议而非强制修改"""
|
|
644
|
-
gitattributes_path = os.path.join(self.root_dir, ".gitattributes")
|
|
645
|
-
|
|
646
|
-
# 检查是否已存在.gitattributes文件
|
|
647
|
-
if os.path.exists(gitattributes_path):
|
|
648
|
-
with open(gitattributes_path, "r", encoding="utf-8") as f:
|
|
649
|
-
content = f.read()
|
|
650
|
-
# 如果已经有换行符相关配置,就不再提示
|
|
651
|
-
if any(keyword in content for keyword in ["text=", "eol=", "binary"]):
|
|
652
|
-
return
|
|
653
|
-
|
|
654
|
-
print("ℹ️ 提示:在Windows系统上,建议配置 .gitattributes 文件来避免换行符问题。")
|
|
655
|
-
print("ℹ️ 这可以防止仅因换行符不同而导致整个文件被标记为修改。")
|
|
656
|
-
|
|
657
|
-
if user_confirm("是否要创建一个最小化的.gitattributes文件?", False):
|
|
658
|
-
# 最小化的内容,只影响特定类型的文件
|
|
659
|
-
minimal_content = """# Jarvis建议的最小化换行符配置
|
|
660
|
-
# 默认所有文本文件使用LF,只有Windows特定文件使用CRLF
|
|
661
|
-
|
|
662
|
-
# 默认所有文本文件使用LF
|
|
663
|
-
* text=auto eol=lf
|
|
664
|
-
|
|
665
|
-
# Windows批处理文件需要CRLF
|
|
666
|
-
*.bat text eol=crlf
|
|
667
|
-
*.cmd text eol=crlf
|
|
668
|
-
*.ps1 text eol=crlf
|
|
669
|
-
"""
|
|
670
|
-
|
|
671
|
-
if not os.path.exists(gitattributes_path):
|
|
672
|
-
with open(gitattributes_path, "w", encoding="utf-8", newline="\n") as f:
|
|
673
|
-
f.write(minimal_content)
|
|
674
|
-
print("✅ 已创建最小化的 .gitattributes 文件")
|
|
675
|
-
else:
|
|
676
|
-
print("ℹ️ 将以下内容追加到现有 .gitattributes 文件:")
|
|
677
|
-
PrettyOutput.print(minimal_content, OutputType.CODE, lang="text") # 保留语法高亮
|
|
678
|
-
if user_confirm("是否追加到现有文件?", True):
|
|
679
|
-
with open(
|
|
680
|
-
gitattributes_path, "a", encoding="utf-8", newline="\n"
|
|
681
|
-
) as f:
|
|
682
|
-
f.write("\n" + minimal_content)
|
|
683
|
-
print("✅ 已更新 .gitattributes 文件")
|
|
684
|
-
else:
|
|
685
|
-
print("ℹ️ 跳过 .gitattributes 文件创建。如遇换行符问题,可手动创建此文件。")
|
|
686
|
-
|
|
687
|
-
def _record_code_changes_stats(self, diff_text: str) -> None:
|
|
688
|
-
"""记录代码变更的统计信息。
|
|
689
|
-
|
|
690
|
-
Args:
|
|
691
|
-
diff_text: git diff的文本输出
|
|
692
|
-
"""
|
|
693
|
-
from jarvis.jarvis_stats.stats import StatsManager
|
|
694
|
-
import re
|
|
695
|
-
|
|
696
|
-
# 匹配插入行数
|
|
697
|
-
insertions_match = re.search(r"(\d+)\s+insertions?\(\+\)", diff_text)
|
|
698
|
-
if insertions_match:
|
|
699
|
-
insertions = int(insertions_match.group(1))
|
|
700
|
-
StatsManager.increment(
|
|
701
|
-
"code_lines_inserted", amount=insertions, group="code_agent"
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
# 匹配删除行数
|
|
705
|
-
deletions_match = re.search(r"(\d+)\s+deletions?\(\-\)", diff_text)
|
|
706
|
-
if deletions_match:
|
|
707
|
-
deletions = int(deletions_match.group(1))
|
|
708
|
-
StatsManager.increment(
|
|
709
|
-
"code_lines_deleted", amount=deletions, group="code_agent"
|
|
710
|
-
)
|
|
711
|
-
|
|
712
|
-
def _handle_uncommitted_changes(self) -> None:
|
|
713
|
-
"""处理未提交的修改,包括:
|
|
714
|
-
1. 提示用户确认是否提交
|
|
715
|
-
2. 如果确认,则检查新增文件数量
|
|
716
|
-
3. 如果新增文件超过20个,让用户确认是否添加
|
|
717
|
-
4. 如果用户拒绝添加大量文件,提示修改.gitignore并重新检测
|
|
718
|
-
5. 暂存并提交所有修改
|
|
719
|
-
"""
|
|
720
|
-
if has_uncommitted_changes():
|
|
721
|
-
# 获取代码变更统计
|
|
722
|
-
try:
|
|
723
|
-
diff_result = subprocess.run(
|
|
724
|
-
["git", "diff", "HEAD", "--shortstat"],
|
|
725
|
-
capture_output=True,
|
|
726
|
-
text=True,
|
|
727
|
-
encoding="utf-8",
|
|
728
|
-
errors="replace",
|
|
729
|
-
check=True,
|
|
730
|
-
)
|
|
731
|
-
if diff_result.returncode == 0 and diff_result.stdout:
|
|
732
|
-
self._record_code_changes_stats(diff_result.stdout)
|
|
733
|
-
except subprocess.CalledProcessError:
|
|
734
|
-
pass
|
|
735
|
-
|
|
736
|
-
print("⚠️ 检测到未提交的修改,是否要提交?")
|
|
737
|
-
if not user_confirm("是否要提交?", True):
|
|
738
|
-
return
|
|
739
|
-
|
|
740
|
-
try:
|
|
741
|
-
confirm_add_new_files()
|
|
742
|
-
|
|
743
|
-
if not has_uncommitted_changes():
|
|
744
|
-
return
|
|
745
|
-
|
|
746
|
-
# 获取当前分支的提交总数
|
|
747
|
-
# 兼容空仓库或无 HEAD 的场景:失败时将提交计数视为 0,继续执行提交流程
|
|
748
|
-
commit_count = 0
|
|
749
|
-
try:
|
|
750
|
-
commit_result = subprocess.run(
|
|
751
|
-
["git", "rev-list", "--count", "HEAD"],
|
|
752
|
-
capture_output=True,
|
|
753
|
-
text=True,
|
|
754
|
-
encoding="utf-8",
|
|
755
|
-
errors="replace",
|
|
756
|
-
check=False,
|
|
757
|
-
)
|
|
758
|
-
if commit_result.returncode == 0:
|
|
759
|
-
out = commit_result.stdout.strip()
|
|
760
|
-
if out.isdigit():
|
|
761
|
-
commit_count = int(out)
|
|
762
|
-
except Exception:
|
|
763
|
-
commit_count = 0
|
|
764
|
-
|
|
765
|
-
# 暂存所有修改
|
|
766
|
-
subprocess.run(["git", "add", "."], check=True)
|
|
767
|
-
|
|
768
|
-
# 提交变更
|
|
769
|
-
subprocess.run(
|
|
770
|
-
["git", "commit", "-m", f"CheckPoint #{commit_count + 1}"],
|
|
771
|
-
check=True,
|
|
772
|
-
)
|
|
773
|
-
except subprocess.CalledProcessError as e:
|
|
774
|
-
print(f"❌ 提交失败: {str(e)}")
|
|
775
|
-
|
|
776
|
-
def _show_commit_history(
|
|
777
|
-
self, start_commit: Optional[str], end_commit: Optional[str]
|
|
778
|
-
) -> List[Tuple[str, str]]:
|
|
779
|
-
"""显示两个提交之间的提交历史
|
|
780
|
-
|
|
781
|
-
参数:
|
|
782
|
-
start_commit: 起始提交hash
|
|
783
|
-
end_commit: 结束提交hash
|
|
784
|
-
|
|
785
|
-
返回:
|
|
786
|
-
包含(commit_hash, commit_message)的元组列表
|
|
787
|
-
"""
|
|
788
|
-
if start_commit and end_commit:
|
|
789
|
-
commits = get_commits_between(start_commit, end_commit)
|
|
790
|
-
else:
|
|
791
|
-
commits = []
|
|
792
|
-
|
|
793
|
-
if commits:
|
|
794
|
-
# 统计生成的commit数量
|
|
795
|
-
from jarvis.jarvis_stats.stats import StatsManager
|
|
796
|
-
|
|
797
|
-
StatsManager.increment("commits_generated", group="code_agent")
|
|
798
|
-
|
|
799
|
-
commit_messages = "检测到以下提交记录:\n" + "\n".join(
|
|
800
|
-
f"- {commit_hash[:7]}: {message}" for commit_hash, message in commits
|
|
801
|
-
)
|
|
802
|
-
print(f"ℹ️ {commit_messages}")
|
|
803
|
-
return commits
|
|
804
|
-
|
|
805
|
-
def _format_modified_files(self, modified_files: List[str]) -> None:
|
|
806
|
-
"""格式化修改的文件
|
|
807
|
-
|
|
808
|
-
Args:
|
|
809
|
-
modified_files: 修改的文件列表
|
|
810
|
-
"""
|
|
811
|
-
if not modified_files:
|
|
812
|
-
return
|
|
813
|
-
|
|
814
|
-
# 获取格式化命令
|
|
815
|
-
format_commands = get_format_commands_for_files(modified_files, self.root_dir)
|
|
816
|
-
if not format_commands:
|
|
817
|
-
return
|
|
818
|
-
|
|
819
|
-
print("🔧 正在格式化代码...")
|
|
820
|
-
|
|
821
|
-
# 执行格式化命令
|
|
822
|
-
formatted_files = set()
|
|
823
|
-
for tool_name, file_path, command in format_commands:
|
|
824
|
-
try:
|
|
825
|
-
# 检查文件是否存在
|
|
826
|
-
abs_file_path = os.path.join(self.root_dir, file_path) if not os.path.isabs(file_path) else file_path
|
|
827
|
-
if not os.path.exists(abs_file_path):
|
|
828
|
-
continue
|
|
829
|
-
|
|
830
|
-
# 执行格式化命令
|
|
831
|
-
result = subprocess.run(
|
|
832
|
-
command,
|
|
833
|
-
shell=True,
|
|
834
|
-
cwd=self.root_dir,
|
|
835
|
-
capture_output=True,
|
|
836
|
-
text=True,
|
|
837
|
-
encoding="utf-8",
|
|
838
|
-
errors="replace",
|
|
839
|
-
timeout=300, # 300秒超时
|
|
840
|
-
)
|
|
841
|
-
|
|
842
|
-
if result.returncode == 0:
|
|
843
|
-
formatted_files.add(file_path)
|
|
844
|
-
print(f"✅ 已格式化: {os.path.basename(file_path)} ({tool_name})")
|
|
845
|
-
else:
|
|
846
|
-
# 格式化失败,记录但不中断流程
|
|
847
|
-
error_msg = (result.stderr or result.stdout or "").strip()
|
|
848
|
-
if error_msg:
|
|
849
|
-
print(f"⚠️ 格式化失败 ({os.path.basename(file_path)}, {tool_name}): {error_msg[:200]}")
|
|
850
|
-
except subprocess.TimeoutExpired:
|
|
851
|
-
print(f"⚠️ 格式化超时: {os.path.basename(file_path)} ({tool_name})")
|
|
852
|
-
except FileNotFoundError:
|
|
853
|
-
# 工具未安装,跳过
|
|
854
|
-
continue
|
|
855
|
-
except Exception as e:
|
|
856
|
-
# 其他错误,记录但继续
|
|
857
|
-
print(f"⚠️ 格式化失败 ({os.path.basename(file_path)}, {tool_name}): {str(e)[:100]}")
|
|
858
|
-
continue
|
|
859
|
-
|
|
860
|
-
if formatted_files:
|
|
861
|
-
print(f"✅ 已格式化 {len(formatted_files)} 个文件")
|
|
862
|
-
# 暂存格式化后的文件
|
|
863
|
-
try:
|
|
864
|
-
for file_path in formatted_files:
|
|
865
|
-
abs_file_path = os.path.join(self.root_dir, file_path) if not os.path.isabs(file_path) else file_path
|
|
866
|
-
if os.path.exists(abs_file_path):
|
|
867
|
-
subprocess.run(
|
|
868
|
-
["git", "add", file_path],
|
|
869
|
-
cwd=self.root_dir,
|
|
870
|
-
check=False,
|
|
871
|
-
stdout=subprocess.DEVNULL,
|
|
872
|
-
stderr=subprocess.DEVNULL,
|
|
873
|
-
)
|
|
874
|
-
except Exception:
|
|
875
|
-
pass
|
|
876
|
-
|
|
877
|
-
def _handle_commit_confirmation(
|
|
878
|
-
self,
|
|
879
|
-
commits: List[Tuple[str, str]],
|
|
880
|
-
start_commit: Optional[str],
|
|
881
|
-
prefix: str,
|
|
882
|
-
suffix: str,
|
|
883
|
-
) -> None:
|
|
884
|
-
"""处理提交确认和可能的重置"""
|
|
885
|
-
if commits and user_confirm("是否接受以上提交记录?", True):
|
|
886
|
-
# 统计接受的commit数量
|
|
887
|
-
from jarvis.jarvis_stats.stats import StatsManager
|
|
888
|
-
|
|
889
|
-
StatsManager.increment("commits_accepted", group="code_agent")
|
|
890
|
-
|
|
891
|
-
subprocess.run(
|
|
892
|
-
["git", "reset", "--mixed", str(start_commit)],
|
|
893
|
-
stdout=subprocess.DEVNULL,
|
|
894
|
-
stderr=subprocess.DEVNULL,
|
|
895
|
-
check=True,
|
|
896
|
-
)
|
|
897
|
-
|
|
898
|
-
# 检测变更文件并格式化
|
|
899
|
-
modified_files = get_diff_file_list()
|
|
900
|
-
if modified_files:
|
|
901
|
-
self._format_modified_files(modified_files)
|
|
902
|
-
|
|
903
|
-
git_commiter = GitCommitTool()
|
|
904
|
-
git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self, "model_group": getattr(self.model, "model_group", None)})
|
|
905
|
-
|
|
906
|
-
# 在用户接受commit后,根据配置决定是否保存记忆
|
|
907
|
-
if self.force_save_memory:
|
|
908
|
-
self.memory_manager.prompt_memory_save()
|
|
909
|
-
elif start_commit:
|
|
910
|
-
if user_confirm("是否要重置到初始提交?", True):
|
|
911
|
-
os.system(f"git reset --hard {str(start_commit)}") # 确保转换为字符串
|
|
912
|
-
print("ℹ️ 已重置到初始提交")
|
|
913
220
|
|
|
914
221
|
def run(self, user_input: str, prefix: str = "", suffix: str = "") -> Optional[str]:
|
|
915
|
-
"""
|
|
222
|
+
"""使用给定的用户输入运行代码代理.
|
|
916
223
|
|
|
917
224
|
参数:
|
|
918
225
|
user_input: 用户的需求/请求
|
|
@@ -920,10 +227,42 @@ class CodeAgent(Agent):
|
|
|
920
227
|
返回:
|
|
921
228
|
str: 描述执行结果的输出,成功时返回None
|
|
922
229
|
"""
|
|
923
|
-
prev_dir = os.getcwd()
|
|
924
230
|
try:
|
|
925
|
-
self.
|
|
231
|
+
set_current_agent(self.name, self)
|
|
232
|
+
|
|
233
|
+
# 根据当前模式生成额外说明,供 LLM 感知执行策略
|
|
234
|
+
prev_dir = os.getcwd()
|
|
235
|
+
non_interactive_note = ""
|
|
236
|
+
if getattr(self, "non_interactive", False):
|
|
237
|
+
non_interactive_note = (
|
|
238
|
+
"\n\n[系统说明]\n"
|
|
239
|
+
"本次会话处于**非交互模式**:\n"
|
|
240
|
+
"- 在 PLAN 模式中给出清晰、可执行的详细计划后,应**自动进入 EXECUTE 模式执行计划**,不要等待用户额外确认;\n"
|
|
241
|
+
"- 在 EXECUTE 模式中,保持一步一步的小步提交和可回退策略,但不需要向用户反复询问“是否继续”;\n"
|
|
242
|
+
"- 如遇信息严重不足,可以在 RESEARCH 模式中自行补充必要分析,而不是卡在等待用户输入。\n"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
self.git_manager.init_env(prefix, suffix, self)
|
|
926
246
|
start_commit = get_latest_commit_hash()
|
|
247
|
+
self.start_commit = start_commit
|
|
248
|
+
|
|
249
|
+
# 将初始 commit 信息添加到 addon_prompt(安全回退点)
|
|
250
|
+
if start_commit:
|
|
251
|
+
initial_commit_prompt = f"""
|
|
252
|
+
**🔖 初始 Git Commit(安全回退点)**:
|
|
253
|
+
本次任务开始时的初始 commit 是:`{start_commit}`
|
|
254
|
+
|
|
255
|
+
**⚠️ 重要提示**:如果文件被破坏得很严重无法恢复,可以使用以下命令重置到这个初始 commit:
|
|
256
|
+
```bash
|
|
257
|
+
git reset --hard {start_commit}
|
|
258
|
+
```
|
|
259
|
+
这将丢弃所有未提交的更改,将工作区恢复到任务开始时的状态。请谨慎使用此命令,确保这是你真正想要的操作。
|
|
260
|
+
"""
|
|
261
|
+
# 将初始 commit 信息追加到现有的 addon_prompt
|
|
262
|
+
current_addon = self.session.addon_prompt or ""
|
|
263
|
+
self.set_addon_prompt(
|
|
264
|
+
f"{current_addon}\n{initial_commit_prompt}".strip()
|
|
265
|
+
)
|
|
927
266
|
|
|
928
267
|
# 获取项目概况信息
|
|
929
268
|
project_overview = get_project_overview(self.root_dir)
|
|
@@ -932,9 +271,11 @@ class CodeAgent(Agent):
|
|
|
932
271
|
1. 每次响应仅执行一步操作,先分析再修改,避免一步多改。
|
|
933
272
|
2. 充分利用工具理解用户需求和现有代码,禁止凭空假设。
|
|
934
273
|
3. 如果不清楚要修改的文件,必须先分析并找出需要修改的文件,明确目标后再进行编辑。
|
|
935
|
-
4.
|
|
936
|
-
5.
|
|
937
|
-
6.
|
|
274
|
+
4. 对于简单的文本替换,推荐使用 edit_file 工具进行精确修改。避免使用 sed 命令,因为sed极易出错且可能产生不可预期的结果。对于复杂代码(超过50行或涉及多文件协调),禁止直接使用sed或python脚本编辑,必须使用task_list_manager创建任务列表进行安全拆分。
|
|
275
|
+
5. 代码编辑任务优先使用 PATCH 操作,确保搜索文本在目标文件中有且仅有一次精确匹配,保证修改的准确性和安全性。
|
|
276
|
+
6. 如需大范围重写(超过200行或涉及重构),请使用 edit_file 工具配合空search参数 "",并提前备份原始文件。
|
|
277
|
+
7. 如遇信息不明,优先调用工具补充分析,不要主观臆断。
|
|
278
|
+
8. **重要:清理临时文件**:开发过程中产生的临时文件(如测试文件、调试脚本、备份文件、临时日志等)必须在提交前清理删除,否则会被自动提交到git仓库。如果创建了临时文件用于调试或测试,完成后必须立即删除。
|
|
938
279
|
"""
|
|
939
280
|
|
|
940
281
|
# 智能上下文推荐:根据用户输入推荐相关上下文
|
|
@@ -943,25 +284,21 @@ class CodeAgent(Agent):
|
|
|
943
284
|
# 在意图识别和上下文推荐期间抑制模型输出
|
|
944
285
|
was_suppressed = False
|
|
945
286
|
if self.model:
|
|
946
|
-
was_suppressed = getattr(self.model,
|
|
287
|
+
was_suppressed = getattr(self.model, "_suppress_output", False)
|
|
947
288
|
self.model.set_suppress_output(True)
|
|
948
289
|
try:
|
|
949
|
-
print("🔍 正在进行智能上下文推荐....")
|
|
950
|
-
|
|
951
290
|
# 生成上下文推荐(基于关键词和项目上下文)
|
|
952
291
|
recommendation = self.context_recommender.recommend_context(
|
|
953
292
|
user_input=user_input,
|
|
954
293
|
)
|
|
955
|
-
|
|
294
|
+
|
|
956
295
|
# 格式化推荐结果
|
|
957
|
-
context_recommendation_text =
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
print(f"ℹ️ {context_recommendation_text}")
|
|
962
|
-
except Exception as e:
|
|
296
|
+
context_recommendation_text = (
|
|
297
|
+
self.context_recommender.format_recommendation(recommendation)
|
|
298
|
+
)
|
|
299
|
+
except Exception:
|
|
963
300
|
# 上下文推荐失败不应该影响主流程
|
|
964
|
-
|
|
301
|
+
pass
|
|
965
302
|
finally:
|
|
966
303
|
# 恢复模型输出设置
|
|
967
304
|
if self.model:
|
|
@@ -972,553 +309,147 @@ class CodeAgent(Agent):
|
|
|
972
309
|
project_overview
|
|
973
310
|
+ "\n\n"
|
|
974
311
|
+ first_tip
|
|
312
|
+
+ non_interactive_note
|
|
975
313
|
+ context_recommendation_text
|
|
976
314
|
+ "\n\n任务描述:\n"
|
|
977
315
|
+ user_input
|
|
978
316
|
)
|
|
979
317
|
else:
|
|
980
|
-
enhanced_input =
|
|
318
|
+
enhanced_input = (
|
|
319
|
+
first_tip
|
|
320
|
+
+ non_interactive_note
|
|
321
|
+
+ context_recommendation_text
|
|
322
|
+
+ "\n\n任务描述:\n"
|
|
323
|
+
+ user_input
|
|
324
|
+
)
|
|
981
325
|
|
|
982
326
|
try:
|
|
983
327
|
if self.model:
|
|
984
328
|
self.model.set_suppress_output(False)
|
|
985
|
-
super().run(enhanced_input)
|
|
329
|
+
result = super().run(enhanced_input)
|
|
330
|
+
# 确保返回值是 str 或 None
|
|
331
|
+
if result is None:
|
|
332
|
+
result_str = None
|
|
333
|
+
else:
|
|
334
|
+
result_str = str(result)
|
|
986
335
|
except RuntimeError as e:
|
|
987
|
-
|
|
336
|
+
PrettyOutput.auto_print(f"⚠️ 执行失败: {str(e)}")
|
|
988
337
|
return str(e)
|
|
989
338
|
|
|
339
|
+
# 处理未提交的更改(在 review 之前先提交)
|
|
340
|
+
self.git_manager.handle_uncommitted_changes()
|
|
341
|
+
|
|
342
|
+
# 如果启用了 review,执行 review 和修复循环
|
|
343
|
+
if not self.disable_review:
|
|
344
|
+
self._review_and_fix(
|
|
345
|
+
user_input=user_input,
|
|
346
|
+
enhanced_input=enhanced_input,
|
|
347
|
+
prefix=prefix,
|
|
348
|
+
suffix=suffix,
|
|
349
|
+
code_generation_summary=result_str,
|
|
350
|
+
)
|
|
990
351
|
|
|
991
|
-
|
|
992
|
-
self._handle_uncommitted_changes()
|
|
993
352
|
end_commit = get_latest_commit_hash()
|
|
994
|
-
commits = self.
|
|
995
|
-
|
|
996
|
-
|
|
353
|
+
commits = self.git_manager.show_commit_history(
|
|
354
|
+
self.start_commit, end_commit
|
|
355
|
+
)
|
|
356
|
+
self.git_manager.handle_commit_confirmation(
|
|
357
|
+
commits,
|
|
358
|
+
self.start_commit,
|
|
359
|
+
prefix,
|
|
360
|
+
suffix,
|
|
361
|
+
self,
|
|
362
|
+
self.post_process_manager.post_process_modified_files,
|
|
363
|
+
)
|
|
364
|
+
return result_str
|
|
997
365
|
|
|
998
366
|
except RuntimeError as e:
|
|
999
367
|
return f"Error during execution: {str(e)}"
|
|
1000
368
|
finally:
|
|
369
|
+
# 在run方法结束时反注册agent
|
|
370
|
+
clear_current_agent()
|
|
371
|
+
|
|
1001
372
|
# Ensure switching back to the original working directory after CodeAgent completes
|
|
1002
373
|
try:
|
|
1003
374
|
os.chdir(prev_dir)
|
|
1004
375
|
except Exception:
|
|
1005
376
|
pass
|
|
1006
377
|
|
|
1007
|
-
def
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
cmd,
|
|
1017
|
-
capture_output=True,
|
|
1018
|
-
text=True,
|
|
1019
|
-
encoding="utf-8",
|
|
1020
|
-
errors="replace",
|
|
1021
|
-
check=False,
|
|
1022
|
-
)
|
|
1023
|
-
finally:
|
|
1024
|
-
subprocess.run(["git", "reset"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
1025
|
-
|
|
1026
|
-
if res.returncode == 0 and res.stdout:
|
|
1027
|
-
for line in res.stdout.splitlines():
|
|
1028
|
-
if not line.strip():
|
|
1029
|
-
continue
|
|
1030
|
-
parts = line.split("\t")
|
|
1031
|
-
if not parts:
|
|
1032
|
-
continue
|
|
1033
|
-
status = parts[0]
|
|
1034
|
-
if status.startswith("R") or status.startswith("C"):
|
|
1035
|
-
# 重命名/复制:使用新路径作为键
|
|
1036
|
-
if len(parts) >= 3:
|
|
1037
|
-
old_path, new_path = parts[1], parts[2]
|
|
1038
|
-
status_map[new_path] = status
|
|
1039
|
-
# 也记录旧路径,便于匹配 name-only 的结果
|
|
1040
|
-
status_map[old_path] = status
|
|
1041
|
-
elif len(parts) >= 2:
|
|
1042
|
-
status_map[parts[-1]] = status
|
|
1043
|
-
else:
|
|
1044
|
-
if len(parts) >= 2:
|
|
1045
|
-
status_map[parts[1]] = status
|
|
1046
|
-
return status_map
|
|
378
|
+
def _on_after_tool_call(
|
|
379
|
+
self,
|
|
380
|
+
agent: Agent,
|
|
381
|
+
current_response: Optional[str] = None,
|
|
382
|
+
need_return: Optional[bool] = None,
|
|
383
|
+
tool_prompt: Optional[str] = None,
|
|
384
|
+
**kwargs: Any,
|
|
385
|
+
) -> None:
|
|
386
|
+
"""工具调用后回调函数。"""
|
|
1047
387
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
head_exists = bool(get_latest_commit_hash())
|
|
1051
|
-
try:
|
|
1052
|
-
# 为了让未跟踪文件也能展示diff,临时 -N 该文件
|
|
1053
|
-
subprocess.run(["git", "add", "-N", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
1054
|
-
cmd = ["git", "diff"] + (["HEAD"] if head_exists else []) + ["--", file_path]
|
|
1055
|
-
res = subprocess.run(
|
|
1056
|
-
cmd,
|
|
1057
|
-
capture_output=True,
|
|
1058
|
-
text=True,
|
|
1059
|
-
encoding="utf-8",
|
|
1060
|
-
errors="replace",
|
|
1061
|
-
check=False,
|
|
1062
|
-
)
|
|
1063
|
-
if res.returncode == 0:
|
|
1064
|
-
return res.stdout or ""
|
|
1065
|
-
return ""
|
|
1066
|
-
finally:
|
|
1067
|
-
subprocess.run(["git", "reset", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
388
|
+
final_ret = ""
|
|
389
|
+
diff = get_diff()
|
|
1068
390
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
lines: List[str] = []
|
|
391
|
+
if diff:
|
|
392
|
+
start_hash = get_latest_commit_hash()
|
|
393
|
+
modified_files = get_diff_file_list()
|
|
1073
394
|
|
|
1074
|
-
|
|
1075
|
-
"""获取单文件的新增/删除行数,失败时返回(0,0)"""
|
|
1076
|
-
head_exists = bool(get_latest_commit_hash())
|
|
395
|
+
# 使用增强的 diff 可视化(如果可用)
|
|
1077
396
|
try:
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
cmd = ["git", "diff", "--numstat"] + (["HEAD"] if head_exists else []) + ["--", file_path]
|
|
1081
|
-
res = subprocess.run(
|
|
1082
|
-
cmd,
|
|
1083
|
-
capture_output=True,
|
|
1084
|
-
text=True,
|
|
1085
|
-
encoding="utf-8",
|
|
1086
|
-
errors="replace",
|
|
1087
|
-
check=False,
|
|
397
|
+
from jarvis.jarvis_code_agent.diff_visualizer import (
|
|
398
|
+
visualize_diff_enhanced,
|
|
1088
399
|
)
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
total_changes = adds + dels
|
|
1111
|
-
|
|
1112
|
-
# 删除文件:不展示diff,仅提示(附带删除行数信息如果可用)
|
|
1113
|
-
if (status.startswith("D")) or (not os.path.exists(f)):
|
|
1114
|
-
if dels > 0:
|
|
1115
|
-
lines.append(f"- {f} 文件被删除(删除{dels}行)")
|
|
1116
|
-
else:
|
|
1117
|
-
lines.append(f"- {f} 文件被删除")
|
|
1118
|
-
continue
|
|
1119
|
-
|
|
1120
|
-
# 变更过大:仅提示新增/删除行数,避免输出超长diff
|
|
1121
|
-
if total_changes > 300:
|
|
1122
|
-
lines.append(f"- {f} 新增{adds}行/删除{dels}行(变更过大,预览已省略)")
|
|
1123
|
-
continue
|
|
400
|
+
from jarvis.jarvis_utils.config import get_diff_show_line_numbers
|
|
401
|
+
from jarvis.jarvis_utils.config import get_diff_visualization_mode
|
|
402
|
+
|
|
403
|
+
# 显示整体 diff(使用增强可视化)
|
|
404
|
+
visualization_mode = get_diff_visualization_mode()
|
|
405
|
+
show_line_numbers = get_diff_show_line_numbers()
|
|
406
|
+
# 构建文件路径显示(多文件时显示所有文件名)
|
|
407
|
+
file_path_display = ", ".join(modified_files) if modified_files else ""
|
|
408
|
+
visualize_diff_enhanced(
|
|
409
|
+
diff,
|
|
410
|
+
file_path=file_path_display,
|
|
411
|
+
mode=visualization_mode,
|
|
412
|
+
show_line_numbers=show_line_numbers,
|
|
413
|
+
)
|
|
414
|
+
except ImportError:
|
|
415
|
+
# 如果导入失败,回退到原有方式
|
|
416
|
+
PrettyOutput.print(diff, OutputType.CODE, lang="diff")
|
|
417
|
+
except Exception as e:
|
|
418
|
+
# 如果可视化失败,回退到原有方式
|
|
419
|
+
PrettyOutput.auto_print(f"⚠️ Diff 可视化失败,使用默认方式: {e}")
|
|
420
|
+
PrettyOutput.print(diff, OutputType.CODE, lang="diff")
|
|
1124
421
|
|
|
1125
|
-
#
|
|
1126
|
-
|
|
1127
|
-
if file_diff.strip():
|
|
1128
|
-
lines.append(f"文件: {f}\n```diff\n{file_diff}\n```")
|
|
1129
|
-
else:
|
|
1130
|
-
# 当无法获取到diff(例如重命名或特殊状态),避免空输出
|
|
1131
|
-
lines.append(f"- {f} 变更已记录(无可展示的文本差异)")
|
|
1132
|
-
return "\n".join(lines)
|
|
422
|
+
# 更新上下文管理器
|
|
423
|
+
self.impact_manager.update_context_for_modified_files(modified_files)
|
|
1133
424
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
if not modified_files:
|
|
1137
|
-
return
|
|
1138
|
-
print("🔄 正在更新代码上下文...")
|
|
1139
|
-
for file_path in modified_files:
|
|
1140
|
-
if os.path.exists(file_path):
|
|
1141
|
-
try:
|
|
1142
|
-
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
1143
|
-
content = f.read()
|
|
1144
|
-
self.context_manager.update_context_for_file(file_path, content)
|
|
1145
|
-
except Exception:
|
|
1146
|
-
# 如果读取文件失败,跳过更新
|
|
1147
|
-
pass
|
|
425
|
+
# 进行影响范围分析
|
|
426
|
+
impact_report = self.impact_manager.analyze_edit_impact(modified_files)
|
|
1148
427
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
Returns:
|
|
1153
|
-
ImpactReport: 影响分析报告,如果未启用或失败则返回None
|
|
1154
|
-
"""
|
|
1155
|
-
if not is_enable_impact_analysis():
|
|
1156
|
-
return None
|
|
1157
|
-
|
|
1158
|
-
print("🔍 正在进行变更影响分析...")
|
|
1159
|
-
try:
|
|
1160
|
-
impact_analyzer = ImpactAnalyzer(self.context_manager)
|
|
1161
|
-
all_edits = []
|
|
1162
|
-
for file_path in modified_files:
|
|
1163
|
-
if os.path.exists(file_path):
|
|
1164
|
-
edits = parse_git_diff_to_edits(file_path, self.root_dir)
|
|
1165
|
-
all_edits.extend(edits)
|
|
1166
|
-
|
|
1167
|
-
if not all_edits:
|
|
1168
|
-
return None
|
|
1169
|
-
|
|
1170
|
-
# 按文件分组编辑
|
|
1171
|
-
edits_by_file = {}
|
|
1172
|
-
for edit in all_edits:
|
|
1173
|
-
if edit.file_path not in edits_by_file:
|
|
1174
|
-
edits_by_file[edit.file_path] = []
|
|
1175
|
-
edits_by_file[edit.file_path].append(edit)
|
|
1176
|
-
|
|
1177
|
-
# 对每个文件进行影响分析
|
|
1178
|
-
impact_report = None
|
|
1179
|
-
for file_path, edits in edits_by_file.items():
|
|
1180
|
-
report = impact_analyzer.analyze_edit_impact(file_path, edits)
|
|
1181
|
-
if report:
|
|
1182
|
-
# 合并报告
|
|
1183
|
-
if impact_report is None:
|
|
1184
|
-
impact_report = report
|
|
1185
|
-
else:
|
|
1186
|
-
# 合并多个报告,去重
|
|
1187
|
-
impact_report.affected_files = list(set(impact_report.affected_files + report.affected_files))
|
|
1188
|
-
|
|
1189
|
-
# 合并符号(基于文件路径和名称去重)
|
|
1190
|
-
symbol_map = {}
|
|
1191
|
-
for symbol in impact_report.affected_symbols + report.affected_symbols:
|
|
1192
|
-
key = (symbol.file_path, symbol.name, symbol.line_start)
|
|
1193
|
-
if key not in symbol_map:
|
|
1194
|
-
symbol_map[key] = symbol
|
|
1195
|
-
impact_report.affected_symbols = list(symbol_map.values())
|
|
1196
|
-
|
|
1197
|
-
impact_report.affected_tests = list(set(impact_report.affected_tests + report.affected_tests))
|
|
1198
|
-
|
|
1199
|
-
# 合并接口变更(基于符号名和文件路径去重)
|
|
1200
|
-
interface_map = {}
|
|
1201
|
-
for change in impact_report.interface_changes + report.interface_changes:
|
|
1202
|
-
key = (change.file_path, change.symbol_name, change.change_type)
|
|
1203
|
-
if key not in interface_map:
|
|
1204
|
-
interface_map[key] = change
|
|
1205
|
-
impact_report.interface_changes = list(interface_map.values())
|
|
1206
|
-
|
|
1207
|
-
impact_report.impacts.extend(report.impacts)
|
|
1208
|
-
|
|
1209
|
-
# 合并建议
|
|
1210
|
-
impact_report.recommendations = list(set(impact_report.recommendations + report.recommendations))
|
|
1211
|
-
|
|
1212
|
-
# 使用更高的风险等级
|
|
1213
|
-
if report.risk_level.value == 'high' or impact_report.risk_level.value == 'high':
|
|
1214
|
-
impact_report.risk_level = report.risk_level if report.risk_level.value == 'high' else impact_report.risk_level
|
|
1215
|
-
elif report.risk_level.value == 'medium':
|
|
1216
|
-
impact_report.risk_level = report.risk_level
|
|
1217
|
-
|
|
1218
|
-
return impact_report
|
|
1219
|
-
except Exception as e:
|
|
1220
|
-
# 影响分析失败不应该影响主流程,仅记录日志
|
|
1221
|
-
print(f"⚠️ 影响范围分析失败: {e}")
|
|
1222
|
-
return None
|
|
1223
|
-
|
|
1224
|
-
def _handle_impact_report(self, impact_report: Optional[Any], agent: Agent, final_ret: str) -> str:
|
|
1225
|
-
"""处理影响范围分析报告
|
|
1226
|
-
|
|
1227
|
-
Args:
|
|
1228
|
-
impact_report: 影响分析报告
|
|
1229
|
-
agent: Agent实例
|
|
1230
|
-
final_ret: 当前的结果字符串
|
|
1231
|
-
|
|
1232
|
-
Returns:
|
|
1233
|
-
更新后的结果字符串
|
|
1234
|
-
"""
|
|
1235
|
-
if not impact_report:
|
|
1236
|
-
return final_ret
|
|
1237
|
-
|
|
1238
|
-
impact_summary = impact_report.to_string(self.root_dir)
|
|
1239
|
-
final_ret += f"\n\n{impact_summary}\n"
|
|
1240
|
-
|
|
1241
|
-
# 如果是高风险,在提示词中提醒
|
|
1242
|
-
if impact_report.risk_level.value == 'high':
|
|
1243
|
-
agent.set_addon_prompt(
|
|
1244
|
-
f"{agent.get_addon_prompt() or ''}\n\n"
|
|
1245
|
-
f"⚠️ 高风险编辑警告:\n"
|
|
1246
|
-
f"检测到此编辑为高风险操作,请仔细检查以下内容:\n"
|
|
1247
|
-
f"- 受影响文件: {len(impact_report.affected_files)} 个\n"
|
|
1248
|
-
f"- 接口变更: {len(impact_report.interface_changes)} 个\n"
|
|
1249
|
-
f"- 相关测试: {len(impact_report.affected_tests)} 个\n"
|
|
1250
|
-
f"建议运行相关测试并检查所有受影响文件。"
|
|
1251
|
-
)
|
|
1252
|
-
|
|
1253
|
-
return final_ret
|
|
1254
|
-
|
|
1255
|
-
def _handle_build_validation_disabled(self, modified_files: List[str], config: Any, agent: Agent, final_ret: str) -> str:
|
|
1256
|
-
"""处理构建验证已禁用的情况
|
|
1257
|
-
|
|
1258
|
-
Returns:
|
|
1259
|
-
更新后的结果字符串
|
|
1260
|
-
"""
|
|
1261
|
-
reason = config.get_disable_reason()
|
|
1262
|
-
reason_text = f"(原因: {reason})" if reason else ""
|
|
1263
|
-
final_ret += f"\n\nℹ️ 构建验证已禁用{reason_text},仅进行基础静态检查\n"
|
|
1264
|
-
|
|
1265
|
-
# 输出基础静态检查日志
|
|
1266
|
-
file_count = len(modified_files)
|
|
1267
|
-
files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
|
|
1268
|
-
if file_count > 3:
|
|
1269
|
-
files_str += f" 等{file_count}个文件"
|
|
1270
|
-
|
|
1271
|
-
# 使用兜底验证器进行基础静态检查
|
|
1272
|
-
fallback_validator = FallbackBuildValidator(self.root_dir, timeout=get_build_validation_timeout())
|
|
1273
|
-
static_check_result = fallback_validator.validate(modified_files)
|
|
1274
|
-
if not static_check_result.success:
|
|
1275
|
-
final_ret += f"\n⚠️ 基础静态检查失败:\n{static_check_result.error_message or static_check_result.output}\n"
|
|
1276
|
-
agent.set_addon_prompt(
|
|
1277
|
-
f"基础静态检查失败,请根据以下错误信息修复代码:\n{static_check_result.error_message or static_check_result.output}\n"
|
|
1278
|
-
)
|
|
1279
|
-
else:
|
|
1280
|
-
final_ret += f"\n✅ 基础静态检查通过(耗时 {static_check_result.duration:.2f}秒)\n"
|
|
1281
|
-
|
|
1282
|
-
return final_ret
|
|
1283
|
-
|
|
1284
|
-
def _handle_build_validation_failure(self, build_validation_result: Any, config: Any, modified_files: List[str], agent: Agent, final_ret: str) -> str:
|
|
1285
|
-
"""处理构建验证失败的情况
|
|
1286
|
-
|
|
1287
|
-
Returns:
|
|
1288
|
-
更新后的结果字符串
|
|
1289
|
-
"""
|
|
1290
|
-
if not config.has_been_asked():
|
|
1291
|
-
# 首次失败,询问用户
|
|
1292
|
-
error_preview = _format_build_error(build_validation_result)
|
|
1293
|
-
print(f"\n⚠️ 构建验证失败:\n{error_preview}\n")
|
|
1294
|
-
print("ℹ️ 提示:如果此项目需要在特殊环境(如容器)中构建,或使用独立构建脚本,"
|
|
1295
|
-
"可以选择禁用构建验证,后续将仅进行基础静态检查。")
|
|
1296
|
-
|
|
1297
|
-
if user_confirm(
|
|
1298
|
-
"是否要禁用构建验证,后续仅进行基础静态检查?",
|
|
1299
|
-
default=False,
|
|
1300
|
-
):
|
|
1301
|
-
# 用户选择禁用
|
|
1302
|
-
config.disable_build_validation(
|
|
1303
|
-
reason="用户选择禁用(项目可能需要在特殊环境中构建)"
|
|
1304
|
-
)
|
|
1305
|
-
config.mark_as_asked()
|
|
1306
|
-
final_ret += "\n\nℹ️ 已禁用构建验证,后续将仅进行基础静态检查\n"
|
|
1307
|
-
|
|
1308
|
-
# 输出基础静态检查日志
|
|
1309
|
-
file_count = len(modified_files)
|
|
1310
|
-
files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
|
|
1311
|
-
if file_count > 3:
|
|
1312
|
-
files_str += f" 等{file_count}个文件"
|
|
1313
|
-
|
|
1314
|
-
# 立即进行基础静态检查
|
|
1315
|
-
fallback_validator = FallbackBuildValidator(self.root_dir, timeout=get_build_validation_timeout())
|
|
1316
|
-
static_check_result = fallback_validator.validate(modified_files)
|
|
1317
|
-
if not static_check_result.success:
|
|
1318
|
-
final_ret += f"\n⚠️ 基础静态检查失败:\n{static_check_result.error_message or static_check_result.output}\n"
|
|
1319
|
-
agent.set_addon_prompt(
|
|
1320
|
-
f"基础静态检查失败,请根据以下错误信息修复代码:\n{static_check_result.error_message or static_check_result.output}\n"
|
|
1321
|
-
)
|
|
1322
|
-
else:
|
|
1323
|
-
final_ret += f"\n✅ 基础静态检查通过(耗时 {static_check_result.duration:.2f}秒)\n"
|
|
1324
|
-
else:
|
|
1325
|
-
# 用户选择继续验证,标记为已询问
|
|
1326
|
-
config.mark_as_asked()
|
|
1327
|
-
final_ret += f"\n\n⚠️ 构建验证失败:\n{_format_build_error(build_validation_result)}\n"
|
|
1328
|
-
# 如果构建失败,添加修复提示
|
|
1329
|
-
agent.set_addon_prompt(
|
|
1330
|
-
f"构建验证失败,请根据以下错误信息修复代码:\n{_format_build_error(build_validation_result)}\n"
|
|
1331
|
-
"请仔细检查错误信息,修复编译/构建错误后重新提交。"
|
|
1332
|
-
)
|
|
1333
|
-
else:
|
|
1334
|
-
# 已经询问过,直接显示错误
|
|
1335
|
-
final_ret += f"\n\n⚠️ 构建验证失败:\n{_format_build_error(build_validation_result)}\n"
|
|
1336
|
-
# 如果构建失败,添加修复提示
|
|
1337
|
-
agent.set_addon_prompt(
|
|
1338
|
-
f"构建验证失败,请根据以下错误信息修复代码:\n{_format_build_error(build_validation_result)}\n"
|
|
1339
|
-
"请仔细检查错误信息,修复编译/构建错误后重新提交。"
|
|
428
|
+
per_file_preview = self.diff_manager.build_per_file_patch_preview(
|
|
429
|
+
modified_files, use_enhanced_visualization=False
|
|
1340
430
|
)
|
|
1341
|
-
|
|
1342
|
-
return final_ret
|
|
1343
|
-
|
|
1344
|
-
def _handle_build_validation(self, modified_files: List[str], agent: Agent, final_ret: str) -> Tuple[Optional[Any], str]:
|
|
1345
|
-
"""处理构建验证
|
|
1346
|
-
|
|
1347
|
-
Returns:
|
|
1348
|
-
(build_validation_result, updated_final_ret)
|
|
1349
|
-
"""
|
|
1350
|
-
if not is_enable_build_validation():
|
|
1351
|
-
return None, final_ret
|
|
1352
|
-
|
|
1353
|
-
config = BuildValidationConfig(self.root_dir)
|
|
1354
|
-
|
|
1355
|
-
# 检查是否已禁用构建验证
|
|
1356
|
-
if config.is_build_validation_disabled():
|
|
1357
|
-
final_ret = self._handle_build_validation_disabled(modified_files, config, agent, final_ret)
|
|
1358
|
-
return None, final_ret
|
|
1359
|
-
|
|
1360
|
-
# 未禁用,进行构建验证
|
|
1361
|
-
build_validation_result = self._validate_build_after_edit(modified_files)
|
|
1362
|
-
if build_validation_result:
|
|
1363
|
-
if not build_validation_result.success:
|
|
1364
|
-
final_ret = self._handle_build_validation_failure(
|
|
1365
|
-
build_validation_result, config, modified_files, agent, final_ret
|
|
1366
|
-
)
|
|
1367
|
-
else:
|
|
1368
|
-
build_system_info = f" ({build_validation_result.build_system.value})" if build_validation_result.build_system else ""
|
|
1369
|
-
final_ret += f"\n\n✅ 构建验证通过{build_system_info}(耗时 {build_validation_result.duration:.2f}秒)\n"
|
|
1370
|
-
|
|
1371
|
-
return build_validation_result, final_ret
|
|
1372
|
-
|
|
1373
|
-
def _handle_static_analysis(self, modified_files: List[str], build_validation_result: Optional[Any], config: Any, agent: Agent, final_ret: str) -> str:
|
|
1374
|
-
"""处理静态分析
|
|
1375
|
-
|
|
1376
|
-
Returns:
|
|
1377
|
-
更新后的结果字符串
|
|
1378
|
-
"""
|
|
1379
|
-
# 检查是否启用静态分析
|
|
1380
|
-
if not is_enable_static_analysis():
|
|
1381
|
-
print("ℹ️ 静态分析已禁用,跳过静态检查")
|
|
1382
|
-
return final_ret
|
|
1383
|
-
|
|
1384
|
-
# 检查是否有可用的lint工具
|
|
1385
|
-
lint_tools_info = "\n".join(
|
|
1386
|
-
f" - {file}: 使用 {'、'.join(get_lint_tools(file))}"
|
|
1387
|
-
for file in modified_files
|
|
1388
|
-
if get_lint_tools(file)
|
|
1389
|
-
)
|
|
1390
|
-
|
|
1391
|
-
if not lint_tools_info:
|
|
1392
|
-
print("ℹ️ 未找到可用的静态检查工具,跳过静态检查")
|
|
1393
|
-
return final_ret
|
|
1394
|
-
|
|
1395
|
-
# 如果构建验证失败且未禁用,不进行静态分析(避免重复错误)
|
|
1396
|
-
# 如果构建验证已禁用,则进行静态分析(因为只做了基础静态检查)
|
|
1397
|
-
should_skip_static = (
|
|
1398
|
-
build_validation_result
|
|
1399
|
-
and not build_validation_result.success
|
|
1400
|
-
and not config.is_build_validation_disabled()
|
|
1401
|
-
)
|
|
1402
|
-
|
|
1403
|
-
if should_skip_static:
|
|
1404
|
-
print("ℹ️ 构建验证失败,跳过静态分析(避免重复错误)")
|
|
1405
|
-
return final_ret
|
|
1406
|
-
|
|
1407
|
-
# 直接执行静态扫描
|
|
1408
|
-
lint_results = self._run_static_analysis(modified_files)
|
|
1409
|
-
if lint_results:
|
|
1410
|
-
# 有错误或警告,让大模型修复
|
|
1411
|
-
errors_summary = self._format_lint_results(lint_results)
|
|
1412
|
-
# 打印完整的检查结果
|
|
1413
|
-
print(f"⚠️ 静态扫描发现问题:\n{errors_summary}")
|
|
1414
|
-
addon_prompt = f"""
|
|
1415
|
-
静态扫描发现以下问题,请根据错误信息修复代码:
|
|
1416
|
-
|
|
1417
|
-
{errors_summary}
|
|
1418
|
-
|
|
1419
|
-
请仔细检查并修复所有问题。
|
|
1420
|
-
"""
|
|
1421
|
-
agent.set_addon_prompt(addon_prompt)
|
|
1422
|
-
final_ret += "\n\n⚠️ 静态扫描发现问题,已提示修复\n"
|
|
1423
|
-
else:
|
|
1424
|
-
final_ret += "\n\n✅ 静态扫描通过\n"
|
|
1425
|
-
|
|
1426
|
-
return final_ret
|
|
1427
431
|
|
|
1428
|
-
def _ask_llm_about_large_deletion(self, detection_result: Dict[str, int], preview: str) -> bool:
|
|
1429
|
-
"""询问大模型大量代码删除是否合理
|
|
1430
|
-
|
|
1431
|
-
参数:
|
|
1432
|
-
detection_result: 检测结果字典,包含 'insertions', 'deletions', 'net_deletions'
|
|
1433
|
-
preview: 补丁预览内容
|
|
1434
|
-
|
|
1435
|
-
返回:
|
|
1436
|
-
bool: 如果大模型认为合理返回True,否则返回False
|
|
1437
|
-
"""
|
|
1438
|
-
if not self.model:
|
|
1439
|
-
# 如果没有模型,默认认为合理
|
|
1440
|
-
return True
|
|
1441
|
-
|
|
1442
|
-
insertions = detection_result['insertions']
|
|
1443
|
-
deletions = detection_result['deletions']
|
|
1444
|
-
net_deletions = detection_result['net_deletions']
|
|
1445
|
-
|
|
1446
|
-
prompt = f"""检测到大量代码删除,请判断是否合理:
|
|
1447
|
-
|
|
1448
|
-
统计信息:
|
|
1449
|
-
- 新增行数: {insertions}
|
|
1450
|
-
- 删除行数: {deletions}
|
|
1451
|
-
- 净删除行数: {net_deletions}
|
|
1452
|
-
|
|
1453
|
-
补丁预览:
|
|
1454
|
-
{preview}
|
|
1455
|
-
|
|
1456
|
-
请仔细分析以上代码变更,判断这些大量代码删除是否合理。可能的情况包括:
|
|
1457
|
-
1. 重构代码,删除冗余或过时的代码
|
|
1458
|
-
2. 简化实现,用更简洁的代码替换复杂的实现
|
|
1459
|
-
3. 删除未使用的代码或功能
|
|
1460
|
-
4. 错误地删除了重要代码
|
|
1461
|
-
|
|
1462
|
-
请使用以下协议回答(必须包含且仅包含以下标记之一):
|
|
1463
|
-
- 如果认为这些删除是合理的,回答: <!!!YES!!!>
|
|
1464
|
-
- 如果认为这些删除不合理或存在风险,回答: <!!!NO!!!>
|
|
1465
|
-
|
|
1466
|
-
请严格按照协议格式回答,不要添加其他内容。
|
|
1467
|
-
"""
|
|
1468
|
-
|
|
1469
|
-
try:
|
|
1470
|
-
print("🤖 正在询问大模型判断大量代码删除是否合理...")
|
|
1471
|
-
response = self.model.chat_until_success(prompt) # type: ignore
|
|
1472
|
-
|
|
1473
|
-
# 使用确定的协议标记解析回答
|
|
1474
|
-
if "<!!!YES!!!>" in response:
|
|
1475
|
-
print("✅ 大模型确认:代码删除合理")
|
|
1476
|
-
return True
|
|
1477
|
-
elif "<!!!NO!!!>" in response:
|
|
1478
|
-
print("⚠️ 大模型确认:代码删除不合理")
|
|
1479
|
-
return False
|
|
1480
|
-
else:
|
|
1481
|
-
# 如果无法找到协议标记,默认认为不合理(保守策略)
|
|
1482
|
-
print(f"⚠️ 无法找到协议标记,默认认为不合理。回答内容: {response[:200]}")
|
|
1483
|
-
return False
|
|
1484
|
-
except Exception as e:
|
|
1485
|
-
# 如果询问失败,默认认为不合理(保守策略)
|
|
1486
|
-
print(f"⚠️ 询问大模型失败: {str(e)},默认认为不合理")
|
|
1487
|
-
return False
|
|
1488
|
-
|
|
1489
|
-
def _on_after_tool_call(self, agent: Agent, current_response=None, need_return=None, tool_prompt=None, **kwargs) -> None:
|
|
1490
|
-
"""工具调用后回调函数。"""
|
|
1491
|
-
final_ret = ""
|
|
1492
|
-
diff = get_diff()
|
|
1493
|
-
|
|
1494
|
-
if diff:
|
|
1495
|
-
start_hash = get_latest_commit_hash()
|
|
1496
|
-
PrettyOutput.print(diff, OutputType.CODE, lang="diff") # 保留语法高亮
|
|
1497
|
-
modified_files = get_diff_file_list()
|
|
1498
|
-
|
|
1499
|
-
# 更新上下文管理器
|
|
1500
|
-
self._update_context_for_modified_files(modified_files)
|
|
1501
|
-
|
|
1502
|
-
# 进行影响范围分析
|
|
1503
|
-
impact_report = self._analyze_edit_impact(modified_files)
|
|
1504
|
-
|
|
1505
|
-
per_file_preview = self._build_per_file_patch_preview(modified_files)
|
|
1506
|
-
|
|
1507
432
|
# 所有模式下,在提交前检测大量代码删除并询问大模型
|
|
1508
433
|
detection_result = detect_large_code_deletion()
|
|
1509
434
|
if detection_result is not None:
|
|
1510
435
|
# 检测到大量代码删除,询问大模型是否合理
|
|
1511
|
-
is_reasonable = self.
|
|
436
|
+
is_reasonable = self.llm_manager.ask_llm_about_large_deletion(
|
|
437
|
+
detection_result, per_file_preview
|
|
438
|
+
)
|
|
1512
439
|
if not is_reasonable:
|
|
1513
440
|
# 大模型认为不合理,撤销修改
|
|
1514
|
-
|
|
441
|
+
PrettyOutput.auto_print("ℹ️ 已撤销修改(大模型认为代码删除不合理)")
|
|
1515
442
|
revert_change()
|
|
1516
|
-
final_ret +=
|
|
443
|
+
final_ret += (
|
|
444
|
+
"\n\n修改被撤销(检测到大量代码删除且大模型判断不合理)\n"
|
|
445
|
+
)
|
|
1517
446
|
final_ret += f"# 补丁预览(按文件):\n{per_file_preview}"
|
|
1518
|
-
PrettyOutput.print(
|
|
447
|
+
PrettyOutput.print(
|
|
448
|
+
final_ret, OutputType.USER, lang="markdown"
|
|
449
|
+
) # 保留语法高亮
|
|
1519
450
|
self.session.prompt += final_ret
|
|
1520
451
|
return
|
|
1521
|
-
|
|
452
|
+
|
|
1522
453
|
commited = handle_commit_workflow()
|
|
1523
454
|
if commited:
|
|
1524
455
|
# 统计代码行数变化
|
|
@@ -1533,7 +464,7 @@ class CodeAgent(Agent):
|
|
|
1533
464
|
check=True,
|
|
1534
465
|
)
|
|
1535
466
|
if diff_result.returncode == 0 and diff_result.stdout:
|
|
1536
|
-
self.
|
|
467
|
+
self.git_manager.record_code_changes_stats(diff_result.stdout)
|
|
1537
468
|
except subprocess.CalledProcessError:
|
|
1538
469
|
pass
|
|
1539
470
|
|
|
@@ -1550,8 +481,12 @@ class CodeAgent(Agent):
|
|
|
1550
481
|
if commits:
|
|
1551
482
|
# 获取最新的提交信息(commits列表按时间倒序,第一个是最新的)
|
|
1552
483
|
latest_commit_hash, latest_commit_message = commits[0]
|
|
1553
|
-
commit_short_hash =
|
|
1554
|
-
|
|
484
|
+
commit_short_hash = (
|
|
485
|
+
latest_commit_hash[:7]
|
|
486
|
+
if len(latest_commit_hash) >= 7
|
|
487
|
+
else latest_commit_hash
|
|
488
|
+
)
|
|
489
|
+
|
|
1555
490
|
final_ret += (
|
|
1556
491
|
f"\n\n代码已修改完成\n"
|
|
1557
492
|
f"✅ 已自动提交\n"
|
|
@@ -1559,16 +494,25 @@ class CodeAgent(Agent):
|
|
|
1559
494
|
f" 提交信息: {latest_commit_message}\n"
|
|
1560
495
|
f"\n补丁内容(按文件):\n{per_file_preview}\n"
|
|
1561
496
|
)
|
|
1562
|
-
|
|
497
|
+
|
|
1563
498
|
# 添加影响范围分析报告
|
|
1564
|
-
final_ret = self.
|
|
1565
|
-
|
|
499
|
+
final_ret = self.impact_manager.handle_impact_report(
|
|
500
|
+
impact_report, self, final_ret
|
|
501
|
+
)
|
|
502
|
+
|
|
1566
503
|
# 构建验证
|
|
1567
504
|
config = BuildValidationConfig(self.root_dir)
|
|
1568
|
-
|
|
1569
|
-
|
|
505
|
+
(
|
|
506
|
+
build_validation_result,
|
|
507
|
+
final_ret,
|
|
508
|
+
) = self.build_validation_manager.handle_build_validation(
|
|
509
|
+
modified_files, self, final_ret
|
|
510
|
+
)
|
|
511
|
+
|
|
1570
512
|
# 静态分析
|
|
1571
|
-
final_ret = self.
|
|
513
|
+
final_ret = self.lint_manager.handle_static_analysis(
|
|
514
|
+
modified_files, build_validation_result, config, self, final_ret
|
|
515
|
+
)
|
|
1572
516
|
else:
|
|
1573
517
|
# 如果没有获取到commits,尝试直接从end_hash获取commit信息
|
|
1574
518
|
commit_info = ""
|
|
@@ -1582,9 +526,20 @@ class CodeAgent(Agent):
|
|
|
1582
526
|
errors="replace",
|
|
1583
527
|
check=False,
|
|
1584
528
|
)
|
|
1585
|
-
if
|
|
1586
|
-
|
|
1587
|
-
|
|
529
|
+
if (
|
|
530
|
+
result.returncode == 0
|
|
531
|
+
and result.stdout
|
|
532
|
+
and "|" in result.stdout
|
|
533
|
+
):
|
|
534
|
+
(
|
|
535
|
+
commit_hash,
|
|
536
|
+
commit_message,
|
|
537
|
+
) = result.stdout.strip().split("|", 1)
|
|
538
|
+
commit_short_hash = (
|
|
539
|
+
commit_hash[:7]
|
|
540
|
+
if len(commit_hash) >= 7
|
|
541
|
+
else commit_hash
|
|
542
|
+
)
|
|
1588
543
|
commit_info = (
|
|
1589
544
|
f"\n✅ 已自动提交\n"
|
|
1590
545
|
f" Commit ID: {commit_short_hash} ({commit_hash})\n"
|
|
@@ -1592,7 +547,7 @@ class CodeAgent(Agent):
|
|
|
1592
547
|
)
|
|
1593
548
|
except Exception:
|
|
1594
549
|
pass
|
|
1595
|
-
|
|
550
|
+
|
|
1596
551
|
if commit_info:
|
|
1597
552
|
final_ret += f"\n\n代码已修改完成{commit_info}\n"
|
|
1598
553
|
else:
|
|
@@ -1619,186 +574,487 @@ class CodeAgent(Agent):
|
|
|
1619
574
|
self.session.prompt += final_ret
|
|
1620
575
|
return
|
|
1621
576
|
|
|
1622
|
-
def
|
|
1623
|
-
"""
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
577
|
+
def _truncate_diff_for_review(self, git_diff: str, token_ratio: float = 0.4) -> str:
|
|
578
|
+
"""截断 git diff 以适应 token 限制(用于 review)
|
|
579
|
+
|
|
580
|
+
参数:
|
|
581
|
+
git_diff: 原始的 git diff 内容
|
|
582
|
+
token_ratio: token 使用比例(默认 0.4,即 40%,review 需要更多上下文)
|
|
583
|
+
|
|
584
|
+
返回:
|
|
585
|
+
str: 截断后的 git diff(如果超出限制则截断并添加提示、文件列表和起始 commit)
|
|
1631
586
|
"""
|
|
1632
|
-
if not
|
|
1633
|
-
return
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
#
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
587
|
+
if not git_diff or not git_diff.strip():
|
|
588
|
+
return git_diff
|
|
589
|
+
|
|
590
|
+
from jarvis.jarvis_utils.embedding import get_context_token_count
|
|
591
|
+
from jarvis.jarvis_utils.config import get_max_input_token_count
|
|
592
|
+
|
|
593
|
+
# 获取最大输入 token 数量
|
|
594
|
+
model_group = self.model.model_group if self.model else None
|
|
595
|
+
try:
|
|
596
|
+
max_input_tokens = get_max_input_token_count(model_group)
|
|
597
|
+
except Exception:
|
|
598
|
+
# 如果获取失败,使用默认值(约 100000 tokens)
|
|
599
|
+
max_input_tokens = 100000
|
|
600
|
+
|
|
601
|
+
# 使用指定比例作为 diff 的 token 限制
|
|
602
|
+
max_diff_tokens = int(max_input_tokens * token_ratio)
|
|
603
|
+
|
|
604
|
+
# 计算 diff 的 token 数量
|
|
605
|
+
diff_token_count = get_context_token_count(git_diff)
|
|
606
|
+
|
|
607
|
+
if diff_token_count <= max_diff_tokens:
|
|
608
|
+
return git_diff
|
|
609
|
+
|
|
610
|
+
# 如果 diff 内容太大,进行截断
|
|
611
|
+
# 先提取修改的文件列表和起始 commit
|
|
612
|
+
import re
|
|
613
|
+
|
|
614
|
+
files = set()
|
|
615
|
+
# 匹配 "diff --git a/path b/path" 格式
|
|
616
|
+
pattern = r"^diff --git a/([^\s]+) b/([^\s]+)$"
|
|
617
|
+
for line in git_diff.split("\n"):
|
|
618
|
+
match = re.match(pattern, line)
|
|
619
|
+
if match:
|
|
620
|
+
file_a = match.group(1)
|
|
621
|
+
file_b = match.group(2)
|
|
622
|
+
files.add(file_b)
|
|
623
|
+
if file_a != file_b:
|
|
624
|
+
files.add(file_a)
|
|
625
|
+
modified_files = sorted(list(files))
|
|
626
|
+
|
|
627
|
+
# 获取起始 commit id
|
|
628
|
+
start_commit = self.start_commit if hasattr(self, "start_commit") else None
|
|
629
|
+
|
|
630
|
+
lines = git_diff.split("\n")
|
|
631
|
+
truncated_lines = []
|
|
632
|
+
current_tokens = 0
|
|
633
|
+
|
|
634
|
+
for line in lines:
|
|
635
|
+
line_tokens = get_context_token_count(line)
|
|
636
|
+
if current_tokens + line_tokens > max_diff_tokens:
|
|
637
|
+
# 添加截断提示
|
|
638
|
+
truncated_lines.append("")
|
|
639
|
+
truncated_lines.append(
|
|
640
|
+
"# ⚠️ diff内容过大,已截断显示(review 需要更多上下文)"
|
|
641
|
+
)
|
|
642
|
+
truncated_lines.append(
|
|
643
|
+
f"# 原始diff共 {len(lines)} 行,{diff_token_count} tokens"
|
|
644
|
+
)
|
|
645
|
+
truncated_lines.append(
|
|
646
|
+
f"# 显示前 {len(truncated_lines) - 3} 行,约 {current_tokens} tokens"
|
|
647
|
+
)
|
|
648
|
+
truncated_lines.append(
|
|
649
|
+
f"# 限制: {max_diff_tokens} tokens (输入窗口的 {token_ratio * 100:.0f}%)"
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# 添加起始 commit id
|
|
653
|
+
if start_commit:
|
|
654
|
+
truncated_lines.append("")
|
|
655
|
+
truncated_lines.append(f"# 起始 Commit ID: {start_commit}")
|
|
656
|
+
|
|
657
|
+
# 添加完整修改文件列表
|
|
658
|
+
if modified_files:
|
|
659
|
+
truncated_lines.append("")
|
|
660
|
+
truncated_lines.append(
|
|
661
|
+
f"# 完整修改文件列表(共 {len(modified_files)} 个文件):"
|
|
1681
662
|
)
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
663
|
+
for file_path in modified_files:
|
|
664
|
+
truncated_lines.append(f"# - {file_path}")
|
|
665
|
+
|
|
666
|
+
break
|
|
667
|
+
|
|
668
|
+
truncated_lines.append(line)
|
|
669
|
+
current_tokens += line_tokens
|
|
670
|
+
|
|
671
|
+
return "\n".join(truncated_lines)
|
|
672
|
+
|
|
673
|
+
def _build_review_prompts(
|
|
674
|
+
self,
|
|
675
|
+
user_input: str,
|
|
676
|
+
git_diff: str,
|
|
677
|
+
code_generation_summary: Optional[str] = None,
|
|
678
|
+
) -> tuple:
|
|
679
|
+
"""构建 review Agent 的 prompts
|
|
680
|
+
|
|
681
|
+
参数:
|
|
682
|
+
user_input: 用户原始需求
|
|
683
|
+
git_diff: 代码修改的 git diff(会自动进行 token 限制处理)
|
|
684
|
+
|
|
685
|
+
返回:
|
|
686
|
+
tuple: (system_prompt, user_prompt, summary_prompt)
|
|
687
|
+
"""
|
|
688
|
+
system_prompt = """你是代码审查专家。你的任务是审查代码修改是否正确完成了用户需求。
|
|
689
|
+
|
|
690
|
+
审查标准:
|
|
691
|
+
1. 功能完整性:代码修改是否完整实现了用户需求的所有功能点?
|
|
692
|
+
2. 代码正确性:修改的代码逻辑是否正确,有无明显的 bug 或错误?
|
|
693
|
+
3. 代码质量:代码是否符合最佳实践,有无明显的代码异味?
|
|
694
|
+
4. 潜在风险:修改是否可能引入新的问题或破坏现有功能?
|
|
695
|
+
|
|
696
|
+
审查要求:
|
|
697
|
+
- 仔细阅读用户需求、代码生成总结(summary)和代码修改(git diff)
|
|
698
|
+
- **对代码生成总结中的关键信息进行充分验证**:不能盲目信任总结,必须结合 git diff 和实际代码逐条核对
|
|
699
|
+
- 如需了解更多上下文,必须使用 read_code 工具读取相关文件以验证总结中提到的行为/位置/文件是否真实存在并符合描述
|
|
700
|
+
- 基于实际代码进行审查,不要凭空假设
|
|
701
|
+
- 如果代码生成总结与实际代码不一致,应以实际代码为准,并将不一致情况作为问题记录
|
|
702
|
+
- 只关注本次修改相关的问题,不要审查无关代码"""
|
|
703
|
+
|
|
704
|
+
user_prompt = f"""请审查以下代码修改是否正确完成了用户需求。
|
|
705
|
+
|
|
706
|
+
【用户需求】
|
|
707
|
+
{user_input}
|
|
708
|
+
|
|
709
|
+
【代码生成总结】
|
|
710
|
+
{code_generation_summary if code_generation_summary else "无代码生成总结信息(如为空,说明主 Agent 未生成总结,请完全依赖 git diff 和实际代码进行审查)"}
|
|
711
|
+
|
|
712
|
+
【代码修改(Git Diff)】
|
|
713
|
+
```diff
|
|
714
|
+
{git_diff}
|
|
715
|
+
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
请仔细审查代码修改,并特别注意:
|
|
719
|
+
- 不要直接相信代码生成总结中的描述,而是将其视为“待核实的说明”
|
|
720
|
+
- 对总结中提到的每一个关键修改点(如函数/文件/行为变化),都应在 git diff 或实际代码中找到对应依据
|
|
721
|
+
- 如发现总结与实际代码不一致,必须在审查结果中指出
|
|
722
|
+
|
|
723
|
+
如需要可使用 read_code 工具查看更多上下文。
|
|
724
|
+
|
|
725
|
+
如果审查完毕,直接输出 {ot("!!!COMPLETE!!!")},不要输出其他任何内容。
|
|
726
|
+
"""
|
|
727
|
+
|
|
728
|
+
summary_prompt = """请输出 JSON 格式的审查结果,格式如下:
|
|
729
|
+
|
|
730
|
+
```json
|
|
731
|
+
{
|
|
732
|
+
"ok": true/false, // 审查是否通过
|
|
733
|
+
"issues": [ // 发现的问题列表(如果 ok 为 true,可以为空数组)
|
|
734
|
+
{
|
|
735
|
+
"type": "问题类型", // 如:功能缺失、逻辑错误、代码质量、潜在风险
|
|
736
|
+
"description": "问题描述",
|
|
737
|
+
"location": "问题位置(文件:行号)",
|
|
738
|
+
"suggestion": "修复建议"
|
|
739
|
+
}
|
|
740
|
+
],
|
|
741
|
+
"summary": "审查总结" // 简要说明审查结论
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
注意:
|
|
746
|
+
- 如果代码修改完全满足用户需求且无明显问题,设置 ok 为 true
|
|
747
|
+
- 如果存在需要修复的问题,设置 ok 为 false,并在 issues 中列出所有问题
|
|
748
|
+
- 每个问题都要提供具体的修复建议"""
|
|
749
|
+
|
|
750
|
+
return system_prompt, user_prompt, summary_prompt
|
|
751
|
+
|
|
752
|
+
def _parse_review_result(
|
|
753
|
+
self, summary: str, review_agent: Optional[Any] = None, max_retries: int = 3
|
|
754
|
+
) -> dict:
|
|
755
|
+
"""解析 review 结果
|
|
756
|
+
|
|
757
|
+
参数:
|
|
758
|
+
summary: review Agent 的输出
|
|
759
|
+
review_agent: review Agent 实例,用于格式修复
|
|
760
|
+
max_retries: 最大重试次数
|
|
761
|
+
|
|
762
|
+
返回:
|
|
763
|
+
dict: 解析后的审查结果,包含 ok 和 issues 字段
|
|
764
|
+
"""
|
|
765
|
+
import json
|
|
766
|
+
import re
|
|
767
|
+
|
|
768
|
+
def _try_parse_json(content: str) -> tuple[bool, dict | None, str | None]:
|
|
769
|
+
"""尝试解析JSON,返回(成功, 结果, json字符串)"""
|
|
770
|
+
# 尝试从输出中提取 JSON
|
|
771
|
+
# 首先尝试匹配 ```json ... ``` 代码块
|
|
772
|
+
json_match = re.search(r"```json\s*([\s\S]*?)\s*```", content)
|
|
773
|
+
if json_match:
|
|
774
|
+
json_str = json_match.group(1).strip()
|
|
1732
775
|
else:
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
776
|
+
# 尝试匹配裸 JSON 对象
|
|
777
|
+
json_match = re.search(r'\{[\s\S]*"ok"[\s\S]*\}', content)
|
|
778
|
+
if json_match:
|
|
779
|
+
json_str = json_match.group(0)
|
|
780
|
+
else:
|
|
781
|
+
return False, None, None
|
|
782
|
+
|
|
783
|
+
try:
|
|
784
|
+
result = json.loads(json_str)
|
|
785
|
+
if isinstance(result, dict):
|
|
786
|
+
return True, result, json_str
|
|
787
|
+
else:
|
|
788
|
+
return False, None, json_str
|
|
789
|
+
except json.JSONDecodeError:
|
|
790
|
+
return False, None, json_str
|
|
791
|
+
|
|
792
|
+
# 第一次尝试解析
|
|
793
|
+
success, result, json_str = _try_parse_json(summary)
|
|
794
|
+
if success and result is not None:
|
|
795
|
+
return {
|
|
796
|
+
"ok": result.get("ok", True),
|
|
797
|
+
"issues": result.get("issues", []),
|
|
798
|
+
"summary": result.get("summary", ""),
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
# 如果没有提供review_agent,无法修复,返回默认值
|
|
802
|
+
if review_agent is None:
|
|
803
|
+
PrettyOutput.auto_print("⚠️ 无法解析 review 结果,且无法修复格式")
|
|
804
|
+
return {"ok": True, "issues": [], "summary": "无法解析审查结果"}
|
|
805
|
+
|
|
806
|
+
# 尝试修复格式
|
|
807
|
+
for retry in range(max_retries):
|
|
808
|
+
PrettyOutput.auto_print(
|
|
809
|
+
f"🔧 第 {retry + 1}/{max_retries} 次尝试修复 JSON 格式..."
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
fix_prompt = f"""
|
|
813
|
+
之前的review回复格式不正确,无法解析为有效的JSON格式。
|
|
814
|
+
|
|
815
|
+
原始回复内容:
|
|
816
|
+
```
|
|
817
|
+
{summary}
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
请严格按照以下JSON格式重新组织你的回复:
|
|
821
|
+
|
|
822
|
+
```json
|
|
823
|
+
{{
|
|
824
|
+
"ok": true/false, // 表示代码是否通过审查
|
|
825
|
+
"summary": "总体评价和建议", // 简短总结
|
|
826
|
+
"issues": [ // 问题列表,如果没有问题则为空数组
|
|
827
|
+
{{
|
|
828
|
+
"type": "问题类型", // 如: bug, style, performance, security等
|
|
829
|
+
"description": "问题描述",
|
|
830
|
+
"location": "问题位置", // 文件名和行号
|
|
831
|
+
"suggestion": "修复建议"
|
|
832
|
+
}}
|
|
833
|
+
]
|
|
834
|
+
}}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
确保回复只包含上述JSON格式,不要包含其他解释或文本。"""
|
|
838
|
+
|
|
839
|
+
try:
|
|
840
|
+
# 使用review_agent的底层model进行修复,保持review_agent的专用配置和系统prompt
|
|
841
|
+
fixed_summary = review_agent.model.chat_until_success(fix_prompt)
|
|
842
|
+
if fixed_summary:
|
|
843
|
+
success, result, _ = _try_parse_json(str(fixed_summary))
|
|
844
|
+
if success and result is not None:
|
|
845
|
+
PrettyOutput.auto_print(
|
|
846
|
+
f"✅ JSON格式修复成功(第 {retry + 1} 次)"
|
|
847
|
+
)
|
|
848
|
+
return {
|
|
849
|
+
"ok": result.get("ok", True),
|
|
850
|
+
"issues": result.get("issues", []),
|
|
851
|
+
"summary": result.get("summary", ""),
|
|
852
|
+
}
|
|
853
|
+
else:
|
|
854
|
+
PrettyOutput.auto_print("⚠️ 修复后的格式仍不正确,继续尝试...")
|
|
855
|
+
summary = str(fixed_summary) # 使用修复后的内容继续尝试
|
|
856
|
+
else:
|
|
857
|
+
PrettyOutput.auto_print("⚠️ 修复请求无响应")
|
|
858
|
+
|
|
859
|
+
except Exception as e:
|
|
860
|
+
PrettyOutput.auto_print(f"⚠️ 修复过程中出错: {e}")
|
|
861
|
+
|
|
862
|
+
# 3次修复都失败,标记需要重新review
|
|
863
|
+
PrettyOutput.auto_print("❌ JSON格式修复失败,需要重新进行review")
|
|
864
|
+
return {
|
|
865
|
+
"ok": False,
|
|
866
|
+
"issues": [],
|
|
867
|
+
"summary": "JSON_FORMAT_ERROR",
|
|
868
|
+
"need_re_review": True,
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
def _review_and_fix(
|
|
872
|
+
self,
|
|
873
|
+
user_input: str,
|
|
874
|
+
enhanced_input: str,
|
|
875
|
+
prefix: str = "",
|
|
876
|
+
suffix: str = "",
|
|
877
|
+
code_generation_summary: Optional[str] = None,
|
|
878
|
+
) -> None:
|
|
879
|
+
"""执行 review 和修复循环
|
|
880
|
+
|
|
881
|
+
参数:
|
|
882
|
+
user_input: 用户原始需求
|
|
883
|
+
enhanced_input: 增强后的用户输入(用于修复)
|
|
884
|
+
prefix: 前缀
|
|
885
|
+
suffix: 后缀
|
|
1747
886
|
"""
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
887
|
+
from jarvis.jarvis_agent import Agent
|
|
888
|
+
|
|
889
|
+
iteration = 0
|
|
890
|
+
max_iterations = self.review_max_iterations
|
|
891
|
+
# 如果 max_iterations 为 0,表示无限 review
|
|
892
|
+
is_infinite = max_iterations == 0
|
|
893
|
+
|
|
894
|
+
while is_infinite or iteration < max_iterations:
|
|
895
|
+
iteration += 1
|
|
896
|
+
|
|
897
|
+
# 获取从开始到当前的 git diff(提前检测是否有代码修改)
|
|
898
|
+
current_commit = get_latest_commit_hash()
|
|
899
|
+
if self.start_commit is None or current_commit == self.start_commit:
|
|
900
|
+
git_diff = get_diff() # 获取未提交的更改
|
|
1758
901
|
else:
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
902
|
+
git_diff = get_diff_between_commits(self.start_commit, current_commit)
|
|
903
|
+
|
|
904
|
+
if not git_diff or not git_diff.strip():
|
|
905
|
+
PrettyOutput.auto_print("ℹ️ 没有代码修改,跳过审查")
|
|
906
|
+
return
|
|
907
|
+
|
|
908
|
+
# 每轮审查开始前显示清晰的提示信息
|
|
909
|
+
if not self.non_interactive:
|
|
910
|
+
if is_infinite:
|
|
911
|
+
PrettyOutput.auto_print(
|
|
912
|
+
f"\n🔄 代码审查循环 - 第 {iteration} 轮(无限模式)"
|
|
913
|
+
)
|
|
914
|
+
else:
|
|
915
|
+
PrettyOutput.auto_print(
|
|
916
|
+
f"\n🔄 代码审查循环 - 第 {iteration}/{max_iterations} 轮"
|
|
917
|
+
)
|
|
918
|
+
if not user_confirm("是否开始本轮代码审查?", default=True):
|
|
919
|
+
PrettyOutput.auto_print("ℹ️ 用户终止了代码审查")
|
|
920
|
+
return
|
|
921
|
+
else:
|
|
922
|
+
if is_infinite:
|
|
923
|
+
PrettyOutput.auto_print(
|
|
924
|
+
f"\n🔍 开始第 {iteration} 轮代码审查...(无限模式)"
|
|
925
|
+
)
|
|
926
|
+
else:
|
|
927
|
+
PrettyOutput.auto_print(
|
|
928
|
+
f"\n🔍 开始第 {iteration}/{max_iterations} 轮代码审查..."
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
# 对 git diff 进行 token 限制处理(review 需要更多上下文,使用 40% 的 token 比例)
|
|
932
|
+
truncated_git_diff = self._truncate_diff_for_review(
|
|
933
|
+
git_diff, token_ratio=0.4
|
|
934
|
+
)
|
|
935
|
+
if truncated_git_diff != git_diff:
|
|
936
|
+
PrettyOutput.auto_print("⚠️ Git diff 内容过大,已截断以适应 token 限制")
|
|
937
|
+
|
|
938
|
+
# 构建 review prompts
|
|
939
|
+
sys_prompt, usr_prompt, sum_prompt = self._build_review_prompts(
|
|
940
|
+
user_input, truncated_git_diff, code_generation_summary
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
rules_prompt = self.rules_manager.load_all_rules(
|
|
944
|
+
",".join(self.loaded_rule_names)
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
review_agent = Agent(
|
|
948
|
+
system_prompt=sys_prompt + f"\n\n<rules>\n{rules_prompt}\n</rules>",
|
|
949
|
+
name=f"CodeReview-Agent-{iteration}",
|
|
950
|
+
model_group=self.model.model_group if self.model else None,
|
|
951
|
+
summary_prompt=sum_prompt,
|
|
952
|
+
need_summary=True,
|
|
953
|
+
auto_complete=True,
|
|
954
|
+
use_tools=["execute_script", "read_code"],
|
|
955
|
+
non_interactive=self.non_interactive,
|
|
956
|
+
use_methodology=False,
|
|
957
|
+
use_analysis=False,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
# 运行 review
|
|
961
|
+
summary = review_agent.run(usr_prompt)
|
|
962
|
+
|
|
963
|
+
# 解析审查结果,支持格式修复和重新review
|
|
964
|
+
result = self._parse_review_result(
|
|
965
|
+
str(summary) if summary else "", review_agent=review_agent
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
# 检查是否需要重新review(JSON格式错误3次修复失败)
|
|
969
|
+
if result.get("need_re_review", False):
|
|
970
|
+
PrettyOutput.auto_print(
|
|
971
|
+
f"\n🔄 JSON格式修复失败,重新进行代码审查(第 {iteration} 轮)"
|
|
972
|
+
)
|
|
973
|
+
# 跳过当前迭代,重新开始review流程
|
|
974
|
+
continue
|
|
975
|
+
|
|
976
|
+
if result["ok"]:
|
|
977
|
+
PrettyOutput.auto_print(f"\n✅ 代码审查通过(第 {iteration} 轮)")
|
|
978
|
+
if result.get("summary"):
|
|
979
|
+
PrettyOutput.auto_print(f" {result['summary']}")
|
|
980
|
+
return
|
|
981
|
+
|
|
982
|
+
# 审查未通过,需要修复
|
|
983
|
+
PrettyOutput.auto_print(f"\n⚠️ 代码审查发现问题(第 {iteration} 轮):")
|
|
984
|
+
for i, issue in enumerate(result.get("issues", []), 1):
|
|
985
|
+
issue_type = issue.get("type", "未知")
|
|
986
|
+
description = issue.get("description", "无描述")
|
|
987
|
+
location = issue.get("location", "未知位置")
|
|
988
|
+
suggestion = issue.get("suggestion", "无建议")
|
|
989
|
+
PrettyOutput.auto_print(f" {i}. [{issue_type}] {description}")
|
|
990
|
+
PrettyOutput.auto_print(f" 位置: {location}")
|
|
991
|
+
PrettyOutput.auto_print(f" 建议: {suggestion}")
|
|
992
|
+
|
|
993
|
+
# 在每轮审查后给用户一个终止选择
|
|
994
|
+
if not self.non_interactive:
|
|
995
|
+
if not user_confirm("是否继续修复这些问题?", default=True):
|
|
996
|
+
PrettyOutput.auto_print("ℹ️ 用户选择终止审查,保持当前代码状态")
|
|
997
|
+
return
|
|
998
|
+
|
|
999
|
+
# 只有在非无限模式下才检查是否达到最大迭代次数
|
|
1000
|
+
if not is_infinite and iteration >= max_iterations:
|
|
1001
|
+
PrettyOutput.auto_print(
|
|
1002
|
+
f"\n⚠️ 已达到最大审查次数 ({max_iterations}),停止审查"
|
|
1003
|
+
)
|
|
1004
|
+
# 在非交互模式下直接返回,交互模式下询问用户
|
|
1005
|
+
if not self.non_interactive:
|
|
1006
|
+
if not user_confirm("是否继续修复?", default=False):
|
|
1007
|
+
return
|
|
1008
|
+
# 用户选择继续,重置迭代次数
|
|
1009
|
+
iteration = 0
|
|
1010
|
+
max_iterations = self.review_max_iterations
|
|
1011
|
+
is_infinite = max_iterations == 0
|
|
1012
|
+
else:
|
|
1013
|
+
return
|
|
1014
|
+
|
|
1015
|
+
# 构建修复 prompt
|
|
1016
|
+
fix_prompt = f"""代码审查发现以下问题,请修复:
|
|
1017
|
+
|
|
1018
|
+
【审查结果】
|
|
1019
|
+
{result.get("summary", "")}
|
|
1020
|
+
|
|
1021
|
+
【问题列表】
|
|
1022
|
+
"""
|
|
1023
|
+
for i, issue in enumerate(result.get("issues", []), 1):
|
|
1024
|
+
fix_prompt += f"{i}. [{issue.get('type', '未知')}] {issue.get('description', '')}\n"
|
|
1025
|
+
fix_prompt += f" 位置: {issue.get('location', '')}\n"
|
|
1026
|
+
fix_prompt += f" 建议: {issue.get('suggestion', '')}\n\n"
|
|
1027
|
+
|
|
1028
|
+
fix_prompt += "\n请根据上述问题进行修复,确保代码正确实现用户需求。"
|
|
1029
|
+
|
|
1030
|
+
PrettyOutput.auto_print("\n🔧 开始修复问题...")
|
|
1031
|
+
|
|
1032
|
+
# 调用 super().run() 进行修复
|
|
1033
|
+
try:
|
|
1034
|
+
if self.model:
|
|
1035
|
+
self.model.set_suppress_output(False)
|
|
1036
|
+
super().run(fix_prompt)
|
|
1037
|
+
except RuntimeError as e:
|
|
1038
|
+
PrettyOutput.auto_print(f"⚠️ 修复失败: {str(e)}")
|
|
1039
|
+
return
|
|
1040
|
+
|
|
1041
|
+
# 处理未提交的更改
|
|
1042
|
+
self.git_manager.handle_uncommitted_changes()
|
|
1043
|
+
|
|
1044
|
+
def add_runtime_rule(self, rule_name: str) -> None:
|
|
1045
|
+
"""添加运行时加载的规则到跟踪列表
|
|
1046
|
+
|
|
1047
|
+
用于记录通过builtin_input_handler等方式动态加载的规则,
|
|
1048
|
+
确保这些规则能够被后续的子代理继承。
|
|
1049
|
+
|
|
1050
|
+
参数:
|
|
1051
|
+
rule_name: 规则名称
|
|
1776
1052
|
"""
|
|
1777
|
-
if not
|
|
1778
|
-
return
|
|
1779
|
-
|
|
1780
|
-
#
|
|
1781
|
-
|
|
1782
|
-
if config.is_build_validation_disabled():
|
|
1783
|
-
# 已禁用,返回None,由调用方处理基础静态检查
|
|
1784
|
-
return None
|
|
1785
|
-
|
|
1786
|
-
# 输出编译检查日志
|
|
1787
|
-
file_count = len(modified_files)
|
|
1788
|
-
files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
|
|
1789
|
-
if file_count > 3:
|
|
1790
|
-
files_str += f" 等{file_count}个文件"
|
|
1791
|
-
print(f"🔨 正在进行编译检查 ({files_str})...")
|
|
1792
|
-
|
|
1793
|
-
try:
|
|
1794
|
-
timeout = get_build_validation_timeout()
|
|
1795
|
-
validator = BuildValidator(self.root_dir, timeout=timeout)
|
|
1796
|
-
result = validator.validate(modified_files)
|
|
1797
|
-
return result
|
|
1798
|
-
except Exception as e:
|
|
1799
|
-
# 构建验证失败不应该影响主流程,仅记录日志
|
|
1800
|
-
print(f"⚠️ 构建验证执行失败: {e}")
|
|
1801
|
-
return None
|
|
1053
|
+
if not rule_name or not isinstance(rule_name, str):
|
|
1054
|
+
return
|
|
1055
|
+
|
|
1056
|
+
# 同时更新完整规则集合(自动去重)
|
|
1057
|
+
self.loaded_rule_names.add(rule_name)
|
|
1802
1058
|
|
|
1803
1059
|
|
|
1804
1060
|
@app.command()
|
|
@@ -1834,23 +1090,33 @@ def cli(
|
|
|
1834
1090
|
help="提交信息后缀(用换行分隔)",
|
|
1835
1091
|
),
|
|
1836
1092
|
non_interactive: bool = typer.Option(
|
|
1837
|
-
False,
|
|
1093
|
+
False,
|
|
1094
|
+
"-n",
|
|
1095
|
+
"--non-interactive",
|
|
1096
|
+
help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟",
|
|
1838
1097
|
),
|
|
1839
1098
|
rule_names: Optional[str] = typer.Option(
|
|
1840
|
-
None,
|
|
1099
|
+
None,
|
|
1100
|
+
"--rule-names",
|
|
1101
|
+
help="指定规则名称列表,用逗号分隔,从 rules.yaml 文件中读取对应的规则内容",
|
|
1102
|
+
),
|
|
1103
|
+
disable_review: bool = typer.Option(
|
|
1104
|
+
False,
|
|
1105
|
+
"--disable-review",
|
|
1106
|
+
help="启用代码审查:在代码修改完成后自动进行代码审查,发现问题则自动修复",
|
|
1107
|
+
),
|
|
1108
|
+
review_max_iterations: int = typer.Option(
|
|
1109
|
+
0,
|
|
1110
|
+
"--review-max-iterations",
|
|
1111
|
+
help="代码审查最大迭代次数,达到上限后停止审查(默认3次)",
|
|
1841
1112
|
),
|
|
1842
1113
|
) -> None:
|
|
1843
1114
|
"""Jarvis主入口点。"""
|
|
1844
|
-
# CLI 标志:非交互模式(不依赖配置文件)
|
|
1845
|
-
if non_interactive:
|
|
1846
|
-
try:
|
|
1847
|
-
os.environ["JARVIS_NON_INTERACTIVE"] = "true"
|
|
1848
|
-
except Exception:
|
|
1849
|
-
pass
|
|
1850
|
-
# 注意:全局配置同步放在 init_env 之后执行,避免被 init_env 覆盖
|
|
1851
1115
|
# 非交互模式要求从命令行传入任务
|
|
1852
1116
|
if non_interactive and not (requirement and str(requirement).strip()):
|
|
1853
|
-
|
|
1117
|
+
PrettyOutput.auto_print(
|
|
1118
|
+
"❌ 非交互模式已启用:必须使用 --requirement 传入任务内容,因多行输入不可用。"
|
|
1119
|
+
)
|
|
1854
1120
|
raise typer.Exit(code=2)
|
|
1855
1121
|
init_env(
|
|
1856
1122
|
"欢迎使用 Jarvis-CodeAgent,您的代码工程助手已准备就绪!",
|
|
@@ -1862,13 +1128,11 @@ def cli(
|
|
|
1862
1128
|
# 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
|
|
1863
1129
|
try:
|
|
1864
1130
|
if model_group:
|
|
1865
|
-
set_config("
|
|
1131
|
+
set_config("llm_group", str(model_group))
|
|
1866
1132
|
if tool_group:
|
|
1867
|
-
set_config("
|
|
1133
|
+
set_config("tool_group", str(tool_group))
|
|
1868
1134
|
if restore_session:
|
|
1869
|
-
set_config("
|
|
1870
|
-
if non_interactive:
|
|
1871
|
-
set_config("JARVIS_NON_INTERACTIVE", True)
|
|
1135
|
+
set_config("restore_session", True)
|
|
1872
1136
|
except Exception:
|
|
1873
1137
|
# 静默忽略同步异常,不影响主流程
|
|
1874
1138
|
pass
|
|
@@ -1882,9 +1146,13 @@ def cli(
|
|
|
1882
1146
|
)
|
|
1883
1147
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
1884
1148
|
curr_dir_path = os.getcwd()
|
|
1885
|
-
|
|
1886
|
-
init_git =
|
|
1887
|
-
|
|
1149
|
+
PrettyOutput.auto_print(f"⚠️ 警告:当前目录 '{curr_dir_path}' 不是一个git仓库。")
|
|
1150
|
+
init_git = (
|
|
1151
|
+
True
|
|
1152
|
+
if non_interactive
|
|
1153
|
+
else user_confirm(
|
|
1154
|
+
f"是否要在 '{curr_dir_path}' 中初始化一个新的git仓库?", default=True
|
|
1155
|
+
)
|
|
1888
1156
|
)
|
|
1889
1157
|
if init_git:
|
|
1890
1158
|
try:
|
|
@@ -1896,12 +1164,12 @@ def cli(
|
|
|
1896
1164
|
encoding="utf-8",
|
|
1897
1165
|
errors="replace",
|
|
1898
1166
|
)
|
|
1899
|
-
|
|
1167
|
+
PrettyOutput.auto_print("✅ 已成功初始化git仓库。")
|
|
1900
1168
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
1901
|
-
|
|
1169
|
+
PrettyOutput.auto_print(f"❌ 初始化git仓库失败: {e}")
|
|
1902
1170
|
sys.exit(1)
|
|
1903
1171
|
else:
|
|
1904
|
-
|
|
1172
|
+
PrettyOutput.auto_print("ℹ️ 操作已取消。Jarvis需要在git仓库中运行。")
|
|
1905
1173
|
sys.exit(0)
|
|
1906
1174
|
|
|
1907
1175
|
curr_dir = os.getcwd()
|
|
@@ -1909,13 +1177,14 @@ def cli(
|
|
|
1909
1177
|
# 在定位到 git 根目录后,按仓库维度加锁,避免跨仓库互斥
|
|
1910
1178
|
try:
|
|
1911
1179
|
repo_root = os.getcwd()
|
|
1912
|
-
lock_name =
|
|
1180
|
+
lock_name = (
|
|
1181
|
+
f"code_agent_{hashlib.md5(repo_root.encode('utf-8')).hexdigest()}.lock"
|
|
1182
|
+
)
|
|
1913
1183
|
_acquire_single_instance_lock(lock_name=lock_name)
|
|
1914
1184
|
except Exception:
|
|
1915
1185
|
# 回退到全局锁,确保至少有互斥保护
|
|
1916
1186
|
_acquire_single_instance_lock(lock_name="code_agent.lock")
|
|
1917
1187
|
try:
|
|
1918
|
-
|
|
1919
1188
|
agent = CodeAgent(
|
|
1920
1189
|
model_group=model_group,
|
|
1921
1190
|
need_summary=False,
|
|
@@ -1923,31 +1192,221 @@ def cli(
|
|
|
1923
1192
|
tool_group=tool_group,
|
|
1924
1193
|
non_interactive=non_interactive,
|
|
1925
1194
|
rule_names=rule_names,
|
|
1195
|
+
disable_review=disable_review,
|
|
1196
|
+
review_max_iterations=review_max_iterations,
|
|
1926
1197
|
)
|
|
1927
1198
|
|
|
1199
|
+
# 显示可用的规则信息
|
|
1200
|
+
_print_available_rules(agent.rules_manager, rule_names)
|
|
1201
|
+
|
|
1928
1202
|
# 尝试恢复会话
|
|
1929
1203
|
if restore_session:
|
|
1930
1204
|
if agent.restore_session():
|
|
1931
|
-
|
|
1205
|
+
PrettyOutput.auto_print("✅ 已从 .jarvis/saved_session.json 恢复会话。")
|
|
1932
1206
|
else:
|
|
1933
|
-
|
|
1207
|
+
PrettyOutput.auto_print(
|
|
1208
|
+
"⚠️ 无法从 .jarvis/saved_session.json 恢复会话。"
|
|
1209
|
+
)
|
|
1934
1210
|
|
|
1935
1211
|
if requirement:
|
|
1936
1212
|
agent.run(requirement, prefix=prefix, suffix=suffix)
|
|
1213
|
+
if agent.non_interactive:
|
|
1214
|
+
raise typer.Exit(code=0)
|
|
1937
1215
|
else:
|
|
1938
1216
|
while True:
|
|
1939
1217
|
user_input = get_multiline_input("请输入你的需求(输入空行退出):")
|
|
1940
1218
|
if not user_input:
|
|
1941
1219
|
raise typer.Exit(code=0)
|
|
1942
1220
|
agent.run(user_input, prefix=prefix, suffix=suffix)
|
|
1221
|
+
if agent.non_interactive:
|
|
1222
|
+
raise typer.Exit(code=0)
|
|
1943
1223
|
|
|
1944
1224
|
except typer.Exit:
|
|
1945
1225
|
raise
|
|
1946
1226
|
except RuntimeError as e:
|
|
1947
|
-
|
|
1227
|
+
PrettyOutput.auto_print(f"❌ 错误: {str(e)}")
|
|
1948
1228
|
sys.exit(1)
|
|
1949
1229
|
|
|
1950
1230
|
|
|
1231
|
+
def _print_available_rules(
|
|
1232
|
+
rules_manager: RulesManager, rule_names: Optional[str] = None
|
|
1233
|
+
) -> None:
|
|
1234
|
+
"""打印可用的规则信息
|
|
1235
|
+
|
|
1236
|
+
参数:
|
|
1237
|
+
rules_manager: 规则管理器实例
|
|
1238
|
+
rule_names: 用户指定的规则名称列表(逗号分隔)
|
|
1239
|
+
"""
|
|
1240
|
+
try:
|
|
1241
|
+
from rich.console import Console
|
|
1242
|
+
from rich.panel import Panel
|
|
1243
|
+
from rich.text import Text
|
|
1244
|
+
|
|
1245
|
+
console = Console()
|
|
1246
|
+
|
|
1247
|
+
# 获取所有可用规则
|
|
1248
|
+
all_rules = rules_manager.get_all_available_rule_names()
|
|
1249
|
+
builtin_rules = all_rules.get("builtin", [])
|
|
1250
|
+
file_rules = all_rules.get("files", [])
|
|
1251
|
+
yaml_rules = all_rules.get("yaml", [])
|
|
1252
|
+
|
|
1253
|
+
# 获取已加载的规则
|
|
1254
|
+
loaded_rules = []
|
|
1255
|
+
if rule_names:
|
|
1256
|
+
rule_list = [name.strip() for name in rule_names.split(",") if name.strip()]
|
|
1257
|
+
for rule_name in rule_list:
|
|
1258
|
+
if rules_manager.get_named_rule(rule_name):
|
|
1259
|
+
loaded_rules.append(rule_name)
|
|
1260
|
+
|
|
1261
|
+
# 检查项目规则和全局规则
|
|
1262
|
+
has_project_rule = rules_manager.read_project_rule() is not None
|
|
1263
|
+
has_global_rule = rules_manager.read_global_rules() is not None
|
|
1264
|
+
|
|
1265
|
+
# 构建规则信息内容
|
|
1266
|
+
content_parts = []
|
|
1267
|
+
|
|
1268
|
+
# 显示所有规则(按来源分类)
|
|
1269
|
+
has_any_rules = False
|
|
1270
|
+
|
|
1271
|
+
# 内置规则
|
|
1272
|
+
if builtin_rules:
|
|
1273
|
+
has_any_rules = True
|
|
1274
|
+
builtin_text = Text()
|
|
1275
|
+
builtin_text.append("📚 内置规则 ", style="bold cyan")
|
|
1276
|
+
builtin_text.append(f"({len(builtin_rules)} 个): ", style="dim")
|
|
1277
|
+
for i, rule in enumerate(builtin_rules):
|
|
1278
|
+
if i > 0:
|
|
1279
|
+
builtin_text.append(", ", style="dim")
|
|
1280
|
+
builtin_text.append(rule, style="yellow")
|
|
1281
|
+
content_parts.append(builtin_text)
|
|
1282
|
+
|
|
1283
|
+
# 用户自定义规则
|
|
1284
|
+
user_custom_rules = file_rules + yaml_rules
|
|
1285
|
+
if user_custom_rules:
|
|
1286
|
+
has_any_rules = True
|
|
1287
|
+
user_text = Text()
|
|
1288
|
+
user_text.append("👤 用户自定义规则 ", style="bold green")
|
|
1289
|
+
user_text.append(f"({len(user_custom_rules)} 个): ", style="dim")
|
|
1290
|
+
|
|
1291
|
+
# 分别显示文件规则和YAML规则
|
|
1292
|
+
custom_rules_parts = []
|
|
1293
|
+
if file_rules:
|
|
1294
|
+
file_part = Text()
|
|
1295
|
+
file_part.append("文件规则: ", style="blue")
|
|
1296
|
+
for i, rule in enumerate(file_rules):
|
|
1297
|
+
if i > 0:
|
|
1298
|
+
file_part.append(", ", style="dim")
|
|
1299
|
+
file_part.append(rule, style="cyan")
|
|
1300
|
+
custom_rules_parts.append(file_part)
|
|
1301
|
+
|
|
1302
|
+
if yaml_rules:
|
|
1303
|
+
yaml_part = Text()
|
|
1304
|
+
yaml_part.append("YAML规则: ", style="magenta")
|
|
1305
|
+
for i, rule in enumerate(yaml_rules):
|
|
1306
|
+
if i > 0:
|
|
1307
|
+
yaml_part.append(", ", style="dim")
|
|
1308
|
+
yaml_part.append(rule, style="magenta")
|
|
1309
|
+
custom_rules_parts.append(yaml_part)
|
|
1310
|
+
|
|
1311
|
+
# 合并显示自定义规则
|
|
1312
|
+
for i, part in enumerate(custom_rules_parts):
|
|
1313
|
+
if i > 0:
|
|
1314
|
+
user_text.append(" | ", style="dim")
|
|
1315
|
+
user_text.append(part)
|
|
1316
|
+
|
|
1317
|
+
content_parts.append(user_text)
|
|
1318
|
+
|
|
1319
|
+
# 分别显示详细的文件规则和YAML规则(保留原有详细信息)
|
|
1320
|
+
if file_rules:
|
|
1321
|
+
has_any_rules = True
|
|
1322
|
+
file_text = Text()
|
|
1323
|
+
file_text.append("📄 详细文件规则 ", style="bold blue")
|
|
1324
|
+
file_text.append(f"({len(file_rules)} 个): ", style="dim")
|
|
1325
|
+
for i, rule in enumerate(file_rules):
|
|
1326
|
+
if i > 0:
|
|
1327
|
+
file_text.append(", ", style="dim")
|
|
1328
|
+
file_text.append(rule, style="cyan")
|
|
1329
|
+
content_parts.append(file_text)
|
|
1330
|
+
|
|
1331
|
+
if yaml_rules:
|
|
1332
|
+
has_any_rules = True
|
|
1333
|
+
yaml_text = Text()
|
|
1334
|
+
yaml_text.append("📝 详细YAML规则 ", style="bold magenta")
|
|
1335
|
+
yaml_text.append(f"({len(yaml_rules)} 个): ", style="dim")
|
|
1336
|
+
for i, rule in enumerate(yaml_rules):
|
|
1337
|
+
if i > 0:
|
|
1338
|
+
yaml_text.append(", ", style="dim")
|
|
1339
|
+
yaml_text.append(rule, style="magenta")
|
|
1340
|
+
content_parts.append(yaml_text)
|
|
1341
|
+
|
|
1342
|
+
# 如果没有规则,显示提示
|
|
1343
|
+
if not has_any_rules:
|
|
1344
|
+
no_rules_text = Text()
|
|
1345
|
+
no_rules_text.append("ℹ️ 当前没有可用的规则", style="dim")
|
|
1346
|
+
content_parts.append(no_rules_text)
|
|
1347
|
+
|
|
1348
|
+
# 提示信息
|
|
1349
|
+
if has_any_rules:
|
|
1350
|
+
tip_text = Text()
|
|
1351
|
+
tip_text.append("💡 提示: ", style="bold green")
|
|
1352
|
+
tip_text.append("使用 ", style="dim")
|
|
1353
|
+
tip_text.append("--rule-names", style="bold yellow")
|
|
1354
|
+
tip_text.append(" 参数加载规则,例如: ", style="dim")
|
|
1355
|
+
tip_text.append("--rule-names tdd,clean_code", style="bold yellow")
|
|
1356
|
+
tip_text.append("\n 或使用 ", style="dim")
|
|
1357
|
+
tip_text.append("@", style="bold yellow")
|
|
1358
|
+
tip_text.append(" 触发规则加载,例如: ", style="dim")
|
|
1359
|
+
tip_text.append("@tdd @clean_code", style="bold yellow")
|
|
1360
|
+
content_parts.append(tip_text)
|
|
1361
|
+
|
|
1362
|
+
# 显示已加载的规则
|
|
1363
|
+
if loaded_rules:
|
|
1364
|
+
loaded_text = Text()
|
|
1365
|
+
loaded_text.append("✅ 已加载规则: ", style="bold green")
|
|
1366
|
+
for i, rule in enumerate(loaded_rules):
|
|
1367
|
+
if i > 0:
|
|
1368
|
+
loaded_text.append(", ", style="dim")
|
|
1369
|
+
loaded_text.append(rule, style="bold yellow")
|
|
1370
|
+
content_parts.append(loaded_text)
|
|
1371
|
+
|
|
1372
|
+
# 显示项目规则和全局规则
|
|
1373
|
+
if has_project_rule or has_global_rule:
|
|
1374
|
+
rule_files_text = Text()
|
|
1375
|
+
if has_project_rule:
|
|
1376
|
+
rule_files_text.append("📁 项目规则: ", style="bold blue")
|
|
1377
|
+
rule_files_text.append(".jarvis/rule", style="dim")
|
|
1378
|
+
if has_global_rule:
|
|
1379
|
+
rule_files_text.append(" | ", style="dim")
|
|
1380
|
+
if has_global_rule:
|
|
1381
|
+
rule_files_text.append("🌐 全局规则: ", style="bold magenta")
|
|
1382
|
+
rule_files_text.append("~/.jarvis/rule", style="dim")
|
|
1383
|
+
content_parts.append(rule_files_text)
|
|
1384
|
+
|
|
1385
|
+
# 如果有规则信息,使用 Panel 打印
|
|
1386
|
+
if content_parts:
|
|
1387
|
+
from rich.console import Group
|
|
1388
|
+
|
|
1389
|
+
# 创建内容组
|
|
1390
|
+
content_group = Group(*content_parts)
|
|
1391
|
+
|
|
1392
|
+
# 创建 Panel
|
|
1393
|
+
panel = Panel(
|
|
1394
|
+
content_group,
|
|
1395
|
+
title="📋 规则信息",
|
|
1396
|
+
title_align="center",
|
|
1397
|
+
border_style="cyan",
|
|
1398
|
+
padding=(0, 1),
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1401
|
+
console.print(panel)
|
|
1402
|
+
except Exception as e:
|
|
1403
|
+
# 显示错误信息而不是静默失败
|
|
1404
|
+
PrettyOutput.auto_print(f"⚠️ 规则信息显示失败: {e}")
|
|
1405
|
+
import traceback
|
|
1406
|
+
|
|
1407
|
+
traceback.print_exc()
|
|
1408
|
+
|
|
1409
|
+
|
|
1951
1410
|
def main() -> None:
|
|
1952
1411
|
"""Application entry point."""
|
|
1953
1412
|
app()
|