jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.0__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.
Files changed (162) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +1143 -245
  3. jarvis/jarvis_agent/agent_manager.py +97 -0
  4. jarvis/jarvis_agent/builtin_input_handler.py +12 -10
  5. jarvis/jarvis_agent/config_editor.py +57 -0
  6. jarvis/jarvis_agent/edit_file_handler.py +392 -99
  7. jarvis/jarvis_agent/event_bus.py +48 -0
  8. jarvis/jarvis_agent/events.py +157 -0
  9. jarvis/jarvis_agent/file_context_handler.py +79 -0
  10. jarvis/jarvis_agent/file_methodology_manager.py +117 -0
  11. jarvis/jarvis_agent/jarvis.py +1117 -147
  12. jarvis/jarvis_agent/main.py +78 -34
  13. jarvis/jarvis_agent/memory_manager.py +195 -0
  14. jarvis/jarvis_agent/methodology_share_manager.py +174 -0
  15. jarvis/jarvis_agent/prompt_manager.py +82 -0
  16. jarvis/jarvis_agent/prompts.py +46 -9
  17. jarvis/jarvis_agent/protocols.py +4 -1
  18. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  19. jarvis/jarvis_agent/run_loop.py +146 -0
  20. jarvis/jarvis_agent/session_manager.py +9 -9
  21. jarvis/jarvis_agent/share_manager.py +228 -0
  22. jarvis/jarvis_agent/shell_input_handler.py +23 -3
  23. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  24. jarvis/jarvis_agent/task_analyzer.py +212 -0
  25. jarvis/jarvis_agent/task_manager.py +154 -0
  26. jarvis/jarvis_agent/task_planner.py +496 -0
  27. jarvis/jarvis_agent/tool_executor.py +8 -4
  28. jarvis/jarvis_agent/tool_share_manager.py +139 -0
  29. jarvis/jarvis_agent/user_interaction.py +42 -0
  30. jarvis/jarvis_agent/utils.py +54 -0
  31. jarvis/jarvis_agent/web_bridge.py +189 -0
  32. jarvis/jarvis_agent/web_output_sink.py +53 -0
  33. jarvis/jarvis_agent/web_server.py +751 -0
  34. jarvis/jarvis_c2rust/__init__.py +26 -0
  35. jarvis/jarvis_c2rust/cli.py +613 -0
  36. jarvis/jarvis_c2rust/collector.py +258 -0
  37. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  38. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  39. jarvis/jarvis_c2rust/optimizer.py +960 -0
  40. jarvis/jarvis_c2rust/scanner.py +1681 -0
  41. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  42. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  43. jarvis/jarvis_code_agent/code_agent.py +1605 -178
  44. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  45. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  46. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  47. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  48. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  61. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  62. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  63. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  64. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  65. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  66. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  68. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  69. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  70. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  71. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  72. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  73. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  74. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  75. jarvis/jarvis_code_agent/lint.py +275 -13
  76. jarvis/jarvis_code_agent/utils.py +142 -0
  77. jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
  78. jarvis/jarvis_code_analysis/code_review.py +583 -548
  79. jarvis/jarvis_data/config_schema.json +339 -28
  80. jarvis/jarvis_git_squash/main.py +22 -13
  81. jarvis/jarvis_git_utils/git_commiter.py +171 -55
  82. jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
  83. jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
  84. jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
  85. jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
  86. jarvis/jarvis_methodology/main.py +48 -63
  87. jarvis/jarvis_multi_agent/__init__.py +302 -43
  88. jarvis/jarvis_multi_agent/main.py +70 -24
  89. jarvis/jarvis_platform/ai8.py +40 -23
  90. jarvis/jarvis_platform/base.py +210 -49
  91. jarvis/jarvis_platform/human.py +11 -1
  92. jarvis/jarvis_platform/kimi.py +82 -76
  93. jarvis/jarvis_platform/openai.py +73 -1
  94. jarvis/jarvis_platform/registry.py +8 -15
  95. jarvis/jarvis_platform/tongyi.py +115 -101
  96. jarvis/jarvis_platform/yuanbao.py +89 -63
  97. jarvis/jarvis_platform_manager/main.py +194 -132
  98. jarvis/jarvis_platform_manager/service.py +122 -86
  99. jarvis/jarvis_rag/cli.py +156 -53
  100. jarvis/jarvis_rag/embedding_manager.py +155 -12
  101. jarvis/jarvis_rag/llm_interface.py +10 -13
  102. jarvis/jarvis_rag/query_rewriter.py +63 -12
  103. jarvis/jarvis_rag/rag_pipeline.py +222 -40
  104. jarvis/jarvis_rag/reranker.py +26 -3
  105. jarvis/jarvis_rag/retriever.py +270 -14
  106. jarvis/jarvis_sec/__init__.py +3605 -0
  107. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  108. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  109. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  110. jarvis/jarvis_sec/cli.py +116 -0
  111. jarvis/jarvis_sec/report.py +257 -0
  112. jarvis/jarvis_sec/status.py +264 -0
  113. jarvis/jarvis_sec/types.py +20 -0
  114. jarvis/jarvis_sec/workflow.py +219 -0
  115. jarvis/jarvis_smart_shell/main.py +405 -137
  116. jarvis/jarvis_stats/__init__.py +13 -0
  117. jarvis/jarvis_stats/cli.py +387 -0
  118. jarvis/jarvis_stats/stats.py +711 -0
  119. jarvis/jarvis_stats/storage.py +612 -0
  120. jarvis/jarvis_stats/visualizer.py +282 -0
  121. jarvis/jarvis_tools/ask_user.py +1 -0
  122. jarvis/jarvis_tools/base.py +18 -2
  123. jarvis/jarvis_tools/clear_memory.py +239 -0
  124. jarvis/jarvis_tools/cli/main.py +220 -144
  125. jarvis/jarvis_tools/execute_script.py +52 -12
  126. jarvis/jarvis_tools/file_analyzer.py +17 -12
  127. jarvis/jarvis_tools/generate_new_tool.py +46 -24
  128. jarvis/jarvis_tools/read_code.py +277 -18
  129. jarvis/jarvis_tools/read_symbols.py +141 -0
  130. jarvis/jarvis_tools/read_webpage.py +86 -13
  131. jarvis/jarvis_tools/registry.py +294 -90
  132. jarvis/jarvis_tools/retrieve_memory.py +227 -0
  133. jarvis/jarvis_tools/save_memory.py +194 -0
  134. jarvis/jarvis_tools/search_web.py +62 -28
  135. jarvis/jarvis_tools/sub_agent.py +205 -0
  136. jarvis/jarvis_tools/sub_code_agent.py +217 -0
  137. jarvis/jarvis_tools/virtual_tty.py +330 -62
  138. jarvis/jarvis_utils/builtin_replace_map.py +4 -5
  139. jarvis/jarvis_utils/clipboard.py +90 -0
  140. jarvis/jarvis_utils/config.py +607 -50
  141. jarvis/jarvis_utils/embedding.py +3 -0
  142. jarvis/jarvis_utils/fzf.py +57 -0
  143. jarvis/jarvis_utils/git_utils.py +251 -29
  144. jarvis/jarvis_utils/globals.py +174 -17
  145. jarvis/jarvis_utils/http.py +58 -79
  146. jarvis/jarvis_utils/input.py +899 -153
  147. jarvis/jarvis_utils/methodology.py +210 -83
  148. jarvis/jarvis_utils/output.py +220 -137
  149. jarvis/jarvis_utils/utils.py +1906 -135
  150. jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
  151. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  152. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
  153. jarvis/jarvis_git_details/main.py +0 -265
  154. jarvis/jarvis_platform/oyi.py +0 -357
  155. jarvis/jarvis_tools/edit_file.py +0 -255
  156. jarvis/jarvis_tools/rewrite_file.py +0 -195
  157. jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
  158. jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
  159. /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
  160. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  161. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  162. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -12,6 +12,9 @@ class generate_new_tool:
12
12
  生成并注册新的Jarvis工具。该工具会在用户数据目录下创建新的工具文件,
13
13
  并自动注册到当前的工具注册表中。适用场景:1. 需要创建新的自定义工具;
14
14
  2. 扩展Jarvis功能;3. 自动化重复性操作;4. 封装特定领域的功能。
15
+ 重要提示:
16
+ 1. `tool_name` 参数必须与 `tool_code` 中定义的 `name` 属性完全一致。
17
+ 2. 在编写工具代码时,应尽量将工具执行的过程和结果打印出来,方便追踪工具的执行状态。
15
18
  """
16
19
 
17
20
  parameters = {
@@ -74,6 +77,27 @@ class generate_new_tool:
74
77
  "stderr": f"工具名称 '{tool_name}' 不是有效的Python标识符",
75
78
  }
76
79
 
80
+ # 验证工具代码中的名称是否与tool_name一致
81
+ import re
82
+
83
+ match = re.search(
84
+ r"^\s*name\s*=\s*[\"'](.+?)[\"']", tool_code, re.MULTILINE
85
+ )
86
+ if not match:
87
+ return {
88
+ "success": False,
89
+ "stdout": "",
90
+ "stderr": "无法在工具代码中找到 'name' 属性。请确保工具类中包含 'name = \"your_tool_name\"'。",
91
+ }
92
+
93
+ code_name = match.group(1)
94
+ if tool_name != code_name:
95
+ return {
96
+ "success": False,
97
+ "stdout": "",
98
+ "stderr": f"工具名称不一致:参数 'tool_name' ('{tool_name}') 与代码中的 'name' 属性 ('{code_name}') 必须相同。",
99
+ }
100
+
77
101
  # 准备工具目录
78
102
  tools_dir = Path(get_data_dir()) / "tools"
79
103
  tools_dir.mkdir(parents=True, exist_ok=True)
@@ -96,17 +120,13 @@ class generate_new_tool:
96
120
  # 注册新工具到当前的工具注册表
97
121
  success_message = f"工具 '{tool_name}' 已成功生成在 {tool_file_path}"
98
122
 
99
- registration_successful = False
100
123
  if agent:
101
124
  tool_registry = agent.get_tool_registry()
102
125
  if tool_registry:
103
126
  # 尝试加载并注册新工具
104
- PrettyOutput.print(
105
- f"正在注册工具 '{tool_name}'...", OutputType.INFO
106
- )
127
+
107
128
  if tool_registry.register_tool_by_file(str(tool_file_path)):
108
- success_message += f"\n已成功注册到当前会话的工具注册表中"
109
- registration_successful = True
129
+ success_message += "\n已成功注册到当前会话的工具注册表中"
110
130
  else:
111
131
  # 注册失败,删除已创建的文件
112
132
  PrettyOutput.print(
@@ -118,20 +138,13 @@ class generate_new_tool:
118
138
  return {
119
139
  "success": False,
120
140
  "stdout": "",
121
- "stderr": f"工具文件已生成,但注册失败。文件已被删除。",
141
+ "stderr": "工具文件已生成,但注册失败。文件已被删除。",
122
142
  }
123
143
  else:
124
144
  PrettyOutput.print(
125
145
  "未找到工具注册表,无法自动注册工具", OutputType.WARNING
126
146
  )
127
- success_message += f"\n注册到当前会话失败,可能需要重新启动Jarvis"
128
-
129
- PrettyOutput.print(
130
- f"工具 '{tool_name}' 创建"
131
- + ("并注册" if registration_successful else "")
132
- + "成功!",
133
- OutputType.SUCCESS,
134
- )
147
+ success_message += "\n注册到当前会话失败,可能需要重新启动Jarvis"
135
148
 
136
149
  # 检查并安装缺失的依赖
137
150
  try:
@@ -159,13 +172,24 @@ class generate_new_tool:
159
172
  try:
160
173
  __import__(pkg)
161
174
  except ImportError:
162
- PrettyOutput.print(
163
- f"检测到缺失依赖: {pkg}, 正在尝试安装...", OutputType.INFO
164
- )
175
+
165
176
  import subprocess
177
+ import sys
178
+ import os
179
+ from shutil import which as _which
180
+ # 优先使用 uv 安装(先查 venv 内 uv,再查 PATH 中 uv),否则回退到 python -m pip
181
+ if sys.platform == "win32":
182
+ venv_uv = os.path.join(sys.prefix, "Scripts", "uv.exe")
183
+ else:
184
+ venv_uv = os.path.join(sys.prefix, "bin", "uv")
185
+ uv_executable = venv_uv if os.path.exists(venv_uv) else (_which("uv") or None)
186
+ if uv_executable:
187
+ install_cmd = [uv_executable, "pip", "install", pkg]
188
+ else:
189
+ install_cmd = [sys.executable, "-m", "pip", "install", pkg]
190
+ subprocess.run(install_cmd, check=True)
191
+
166
192
 
167
- subprocess.run(["pip", "install", pkg], check=True)
168
- PrettyOutput.print(f"成功安装依赖: {pkg}", OutputType.SUCCESS)
169
193
  except Exception as e:
170
194
  PrettyOutput.print(f"依赖检查/安装失败: {str(e)}", OutputType.WARNING)
171
195
 
@@ -179,11 +203,9 @@ class generate_new_tool:
179
203
  # 删除已创建的文件
180
204
  if tool_file_path and tool_file_path.exists():
181
205
  try:
182
- PrettyOutput.print(
183
- f"正在删除已创建的文件 {tool_file_path}...", OutputType.INFO
184
- )
206
+
185
207
  tool_file_path.unlink()
186
- PrettyOutput.print(f"文件已删除", OutputType.SUCCESS)
208
+
187
209
  except Exception as delete_error:
188
210
  PrettyOutput.print(
189
211
  f"删除文件失败: {str(delete_error)}", OutputType.ERROR
@@ -2,6 +2,8 @@
2
2
  import os
3
3
  from typing import Any, Dict
4
4
 
5
+ from jarvis.jarvis_utils.config import get_max_input_token_count
6
+ from jarvis.jarvis_utils.embedding import get_context_token_count
5
7
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
6
8
 
7
9
 
@@ -28,6 +30,29 @@ class ReadCodeTool:
28
30
  },
29
31
  "required": ["files"],
30
32
  }
33
+
34
+ def _get_max_token_limit(self, agent: Any = None) -> int:
35
+ """获取基于最大窗口数量的token限制
36
+
37
+ Args:
38
+ agent: Agent实例,用于获取模型组配置
39
+
40
+ Returns:
41
+ int: 允许的最大token数(2/3最大窗口)
42
+ """
43
+ try:
44
+ # 尝试从agent获取模型组
45
+ model_group = None
46
+ if agent:
47
+ model_group = getattr(agent, "model_group", None)
48
+
49
+ max_input_tokens = get_max_input_token_count(model_group)
50
+ # 计算2/3限制的token数
51
+ limit_tokens = int(max_input_tokens * 2 / 3)
52
+ return limit_tokens
53
+ except Exception:
54
+ # 如果获取失败,使用默认值(假设32000 token,2/3是21333)
55
+ return 21333
31
56
 
32
57
  def _handle_single_file(
33
58
  self, filepath: str, start_line: int = 1, end_line: int = -1, agent: Any = None
@@ -38,13 +63,14 @@ class ReadCodeTool:
38
63
  filepath (str): 文件路径
39
64
  start_line (int): 起始行号,默认为1
40
65
  end_line (int): 结束行号,默认为-1表示文件末尾
66
+ agent: Agent实例,用于获取上下文管理器
41
67
 
42
68
  Returns:
43
69
  Dict[str, Any]: 包含成功状态、输出内容和错误信息的字典
44
70
  """
45
71
  try:
46
72
  abs_path = os.path.abspath(filepath)
47
- print(f"📖 正在读取文件: {abs_path}...")
73
+
48
74
  # 文件存在性检查
49
75
  if not os.path.exists(abs_path):
50
76
  return {
@@ -62,14 +88,12 @@ class ReadCodeTool:
62
88
  }
63
89
 
64
90
  # 读取文件内容
91
+ # 第一遍流式读取,仅统计总行数,避免一次性读入内存
65
92
  with open(abs_path, "r", encoding="utf-8", errors="ignore") as f:
66
- lines = f.readlines()
67
-
68
- total_lines = len(lines)
93
+ total_lines = sum(1 for _ in f)
69
94
 
70
95
  # 处理空文件情况
71
96
  if total_lines == 0:
72
- print(f"✅ 文件读取完成: {abs_path}")
73
97
  return {
74
98
  "success": True,
75
99
  "stdout": f"\n🔍 文件: {abs_path}\n📄 文件为空 (0行)\n",
@@ -93,23 +117,48 @@ class ReadCodeTool:
93
117
  )
94
118
 
95
119
  if start_line > end_line:
96
- print(
97
- f"❌ 无效的行范围 [{start_line}-{end_line}] (总行数: {total_lines})"
98
- )
120
+
99
121
  return {
100
122
  "success": False,
101
123
  "stdout": "",
102
124
  "stderr": f"无效的行范围 [{start_line}-{end_line}] (总行数: {total_lines})",
103
125
  }
104
126
 
105
- # 添加行号并构建输出内容
106
- selected_lines = lines[start_line - 1 : end_line]
107
- numbered_content = "".join(
108
- [
109
- f"{i:4d}:{line}"
110
- for i, line in enumerate(selected_lines, start=start_line)
111
- ]
112
- )
127
+ # 读取要读取的行范围内容,计算实际token数
128
+ selected_content_lines = []
129
+ with open(abs_path, "r", encoding="utf-8", errors="ignore") as f:
130
+ for i, line in enumerate(f, start=1):
131
+ if i < start_line:
132
+ continue
133
+ if i > end_line:
134
+ break
135
+ selected_content_lines.append(line)
136
+
137
+ # 构建带行号的内容用于token计算(与实际输出格式一致)
138
+ numbered_content = "".join(f"{i:4d}:{line}" for i, line in enumerate(selected_content_lines, start=start_line))
139
+
140
+ # 计算实际token数
141
+ content_tokens = get_context_token_count(numbered_content)
142
+ max_token_limit = self._get_max_token_limit(agent)
143
+
144
+ # 检查单文件读取token数是否超过2/3限制
145
+ if content_tokens > max_token_limit:
146
+ read_lines = end_line - start_line + 1
147
+ return {
148
+ "success": False,
149
+ "stdout": "",
150
+ "stderr": (
151
+ f"⚠️ 读取范围过大: 请求读取内容约 {content_tokens} tokens,超过限制 ({max_token_limit} tokens,约2/3最大窗口)\n"
152
+ f"📊 读取范围: {read_lines} 行 (第 {start_line}-{end_line} 行,文件总行数 {total_lines})\n"
153
+ f"💡 建议:\n"
154
+ f" 1. 分批读取:将范围分成多个较小的批次,每批内容不超过 {max_token_limit} tokens\n"
155
+ f" 2. 先定位:使用搜索或分析工具定位大致位置,再读取具体范围\n"
156
+ f" 3. 缩小范围:为文件指定更精确的行号范围"
157
+ ),
158
+ }
159
+
160
+ # 使用已读取的内容构建输出(避免重复读取)
161
+ numbered_content = "".join(f"{i:4d}:{line}" for i, line in enumerate(selected_content_lines, start=start_line))
113
162
 
114
163
  # 构建输出格式
115
164
  output = (
@@ -117,7 +166,11 @@ class ReadCodeTool:
117
166
  f"📄 原始行号: {start_line}-{end_line} (共{total_lines}行) \n\n"
118
167
  f"{numbered_content}\n\n"
119
168
  )
120
- print(f"✅ 文件读取完成: {abs_path}")
169
+
170
+ # 尝试获取并附加上下文信息
171
+ context_info = self._get_file_context(abs_path, start_line, end_line, agent)
172
+ if context_info:
173
+ output += context_info
121
174
 
122
175
  if agent:
123
176
  files = agent.get_user_data("files")
@@ -133,6 +186,105 @@ class ReadCodeTool:
133
186
  PrettyOutput.print(str(e), OutputType.ERROR)
134
187
  return {"success": False, "stdout": "", "stderr": f"文件读取失败: {str(e)}"}
135
188
 
189
+ def _get_file_context(
190
+ self, filepath: str, start_line: int, end_line: int, agent: Any = None
191
+ ) -> str:
192
+ """获取文件的上下文信息
193
+
194
+ Args:
195
+ filepath: 文件路径
196
+ start_line: 起始行号
197
+ end_line: 结束行号
198
+ agent: Agent实例
199
+
200
+ Returns:
201
+ 格式化的上下文信息字符串,如果无法获取则返回空字符串
202
+ """
203
+ try:
204
+ # 尝试从Agent获取CodeAgent实例
205
+ if not agent:
206
+ return ""
207
+
208
+ # 通过agent获取CodeAgent实例
209
+ # CodeAgent在初始化时会将自身关联到agent
210
+ code_agent = getattr(agent, "_code_agent", None)
211
+ if not code_agent:
212
+ return ""
213
+
214
+ # 获取上下文管理器
215
+ context_manager = getattr(code_agent, "context_manager", None)
216
+ if not context_manager:
217
+ return ""
218
+
219
+ # 输出上下文感知日志
220
+ file_name = os.path.basename(filepath)
221
+ if start_line == end_line:
222
+ line_info = f"第{start_line}行"
223
+ else:
224
+ line_info = f"第{start_line}-{end_line}行"
225
+ PrettyOutput.print(f"🧠 正在分析代码上下文 ({file_name}, {line_info})...", OutputType.INFO)
226
+
227
+ # 确保文件已更新到上下文管理器
228
+ # 如果文件内容已缓存,直接使用;否则读取并更新
229
+ if not hasattr(context_manager, "_file_cache") or filepath not in context_manager._file_cache:
230
+ try:
231
+ with open(filepath, "r", encoding="utf-8", errors="replace") as f:
232
+ content = f.read()
233
+ context_manager.update_context_for_file(filepath, content)
234
+ except Exception:
235
+ # 如果读取失败,尝试获取已有上下文
236
+ pass
237
+
238
+ # 获取编辑上下文
239
+ edit_context = context_manager.get_edit_context(filepath, start_line, end_line)
240
+
241
+ # 构建上下文信息
242
+ if not edit_context.context_summary or edit_context.context_summary == "No context available":
243
+ return ""
244
+
245
+ # 格式化上下文信息
246
+ context_lines = ["\n📋 代码上下文信息:"]
247
+ context_lines.append("─" * 60)
248
+
249
+ if edit_context.current_scope:
250
+ scope_info = f"📍 当前作用域: {edit_context.current_scope.kind} `{edit_context.current_scope.name}`"
251
+ if edit_context.current_scope.signature:
252
+ scope_info += f"\n └─ 签名: {edit_context.current_scope.signature}"
253
+ context_lines.append(scope_info)
254
+
255
+ if edit_context.used_symbols:
256
+ symbol_names = [s.name for s in edit_context.used_symbols[:10]]
257
+ symbols_str = ", ".join(f"`{name}`" for name in symbol_names)
258
+ more = len(edit_context.used_symbols) - 10
259
+ if more > 0:
260
+ symbols_str += f" (还有{more}个)"
261
+ context_lines.append(f"🔗 使用的符号: {symbols_str}")
262
+
263
+ if edit_context.imported_symbols:
264
+ import_names = [s.name for s in edit_context.imported_symbols[:10]]
265
+ imports_str = ", ".join(f"`{name}`" for name in import_names)
266
+ more = len(edit_context.imported_symbols) - 10
267
+ if more > 0:
268
+ imports_str += f" (还有{more}个)"
269
+ context_lines.append(f"📦 导入的符号: {imports_str}")
270
+
271
+ if edit_context.relevant_files:
272
+ rel_files = edit_context.relevant_files[:10]
273
+ files_str = "\n ".join(f"• {os.path.relpath(f, context_manager.project_root)}" for f in rel_files)
274
+ more = len(edit_context.relevant_files) - 10
275
+ if more > 0:
276
+ files_str += f"\n ... 还有{more}个相关文件"
277
+ context_lines.append(f"📁 相关文件 ({len(edit_context.relevant_files)}个):\n {files_str}")
278
+
279
+ context_lines.append("─" * 60)
280
+ context_lines.append("") # 空行
281
+
282
+ return "\n".join(context_lines)
283
+
284
+ except Exception:
285
+ # 静默失败,不影响文件读取
286
+ return ""
287
+
136
288
  def execute(self, args: Dict) -> Dict[str, Any]:
137
289
  """执行代码读取操作
138
290
 
@@ -153,7 +305,105 @@ class ReadCodeTool:
153
305
 
154
306
  all_outputs = []
155
307
  overall_success = True
308
+ status_lines = []
309
+ total_tokens = 0 # 累计读取的token数
310
+ max_token_limit = self._get_max_token_limit(agent)
311
+
312
+ # 第一遍:检查所有文件的累计token数是否超过限制
313
+ file_read_info = [] # 存储每个文件要读取的信息
314
+ for file_info in args["files"]:
315
+ if not isinstance(file_info, dict) or "path" not in file_info:
316
+ continue
317
+
318
+ filepath = file_info["path"].strip()
319
+ start_line = file_info.get("start_line", 1)
320
+ end_line = file_info.get("end_line", -1)
321
+
322
+ # 检查文件是否存在并计算要读取的token数
323
+ abs_path = os.path.abspath(filepath)
324
+ if not os.path.exists(abs_path):
325
+ continue
326
+
327
+ try:
328
+ # 统计总行数
329
+ with open(abs_path, "r", encoding="utf-8", errors="ignore") as f:
330
+ total_lines = sum(1 for _ in f)
331
+
332
+ if total_lines == 0:
333
+ continue
334
+
335
+ # 计算实际要读取的行范围
336
+ if end_line == -1:
337
+ actual_end_line = total_lines
338
+ else:
339
+ actual_end_line = (
340
+ max(1, min(end_line, total_lines))
341
+ if end_line >= 0
342
+ else total_lines + end_line + 1
343
+ )
344
+
345
+ actual_start_line = (
346
+ max(1, min(start_line, total_lines))
347
+ if start_line >= 0
348
+ else total_lines + start_line + 1
349
+ )
350
+
351
+ if actual_start_line <= actual_end_line:
352
+ # 读取要读取的行范围内容
353
+ selected_content_lines = []
354
+ with open(abs_path, "r", encoding="utf-8", errors="ignore") as f:
355
+ for i, line in enumerate(f, start=1):
356
+ if i < actual_start_line:
357
+ continue
358
+ if i > actual_end_line:
359
+ break
360
+ selected_content_lines.append(line)
361
+
362
+ # 构建带行号的内容用于token计算(与实际输出格式一致)
363
+ numbered_content = "".join(
364
+ f"{i:4d}:{line}"
365
+ for i, line in enumerate(selected_content_lines, start=actual_start_line)
366
+ )
367
+
368
+ # 计算实际token数
369
+ content_tokens = get_context_token_count(numbered_content)
370
+
371
+ file_read_info.append({
372
+ "filepath": filepath,
373
+ "start_line": actual_start_line,
374
+ "end_line": actual_end_line,
375
+ "read_lines": actual_end_line - actual_start_line + 1,
376
+ "tokens": content_tokens,
377
+ "file_info": file_info,
378
+ })
379
+ total_tokens += content_tokens
380
+ except Exception:
381
+ continue
382
+
383
+ # 检查累计token数是否超过限制
384
+ if total_tokens > max_token_limit:
385
+ file_list = "\n ".join(
386
+ f"• {info['filepath']}: {info['tokens']} tokens ({info['read_lines']} 行, 范围: {info['start_line']}-{info['end_line']})"
387
+ for info in file_read_info[:10]
388
+ )
389
+ more_files = len(file_read_info) - 10
390
+ if more_files > 0:
391
+ file_list += f"\n ... 还有 {more_files} 个文件"
392
+
393
+ return {
394
+ "success": False,
395
+ "stdout": "",
396
+ "stderr": (
397
+ f"⚠️ 累计读取范围过大: 请求累计读取内容约 {total_tokens} tokens,超过限制 ({max_token_limit} tokens,约2/3最大窗口)\n"
398
+ f"📋 文件列表 ({len(file_read_info)} 个文件):\n {file_list}\n"
399
+ f"💡 建议:\n"
400
+ f" 1. 分批读取:将文件分成多个批次,每批累计内容不超过 {max_token_limit} tokens\n"
401
+ f" 2. 先定位:使用搜索或分析工具定位关键代码位置,再读取具体范围\n"
402
+ f" 3. 缩小范围:为每个文件指定更精确的行号范围"
403
+ ),
404
+ }
156
405
 
406
+ # 第二遍:实际读取文件
157
407
  for file_info in args["files"]:
158
408
  if not isinstance(file_info, dict) or "path" not in file_info:
159
409
  continue
@@ -167,13 +417,22 @@ class ReadCodeTool:
167
417
 
168
418
  if result["success"]:
169
419
  all_outputs.append(result["stdout"])
420
+ status_lines.append(f"✅ {file_info['path']} 文件读取成功")
170
421
  else:
171
422
  all_outputs.append(f"❌ {file_info['path']}: {result['stderr']}")
423
+ status_lines.append(f"❌ {file_info['path']} 文件读取失败")
172
424
  overall_success = False
173
425
 
426
+ stdout_text = "\n".join(all_outputs)
427
+ # 仅打印每个文件的读取状态,不打印具体内容
428
+ try:
429
+ if status_lines:
430
+ print("\n".join(status_lines), end="\n")
431
+ except Exception:
432
+ pass
174
433
  return {
175
434
  "success": overall_success,
176
- "stdout": "\n".join(all_outputs),
435
+ "stdout": stdout_text,
177
436
  "stderr": "",
178
437
  }
179
438
 
@@ -0,0 +1,141 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 按需读取 symbols.jsonl 的工具。
4
+
5
+ 用途:
6
+ - 避免Agent直接完整读取体积较大的符号表文件;
7
+ - 通过提供符号表路径与符号名称列表,仅返回匹配的符号记录。
8
+
9
+ 参数:
10
+ - symbols_file (str): 符号表文件路径(.jsonl),或项目根目录/包含 .jarvis/c2rust 的目录
11
+ - symbols (List[str]): 需要读取的符号名称列表(支持 name 与 qualified_name 匹配)
12
+
13
+ 返回:
14
+ - success (bool)
15
+ - stdout (str): JSON文本,包含查询结果
16
+ - stderr (str)
17
+ """
18
+ import json
19
+ import os
20
+ from pathlib import Path
21
+ from typing import Any, Dict, List
22
+
23
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
24
+
25
+
26
+ class ReadSymbolsTool:
27
+ # 文件名必须与工具名一致,便于注册表自动加载
28
+ name = "read_symbols"
29
+ description = "从symbols.jsonl按需读取指定符号的记录,避免完整加载大文件。参数包含符号表路径和符号名列表(匹配 name 与 qualified_name)(C2Rust工具专用)"
30
+ parameters = {
31
+ "type": "object",
32
+ "properties": {
33
+ "symbols_file": {
34
+ "type": "string",
35
+ "description": "符号表文件路径(.jsonl)。若为目录,则解析为 <dir>/.jarvis/c2rust/symbols.jsonl",
36
+ },
37
+ "symbols": {
38
+ "type": "array",
39
+ "items": {"type": "string"},
40
+ "description": "要检索的符号名称列表(支持 name 或 qualified_name 完全匹配)",
41
+ },
42
+ },
43
+ "required": ["symbols_file", "symbols"],
44
+ }
45
+
46
+ @staticmethod
47
+ def check() -> bool:
48
+ """
49
+ 检查工具是否可用。
50
+ 仅在 c2rust 环境中启用(通过环境变量 JARVIS_C2RUST_ENABLED 判断)。
51
+ """
52
+ return os.environ.get("JARVIS_C2RUST_ENABLED") == "1"
53
+
54
+ @staticmethod
55
+ def _resolve_symbols_jsonl_path(path_hint: str) -> Path:
56
+ """
57
+ 解析符号表路径:
58
+ - 若为目录,返回 <dir>/.jarvis/c2rust/symbols.jsonl
59
+ - 若为文件,直接返回
60
+ """
61
+ p = Path(os.path.abspath(os.path.expanduser(path_hint)))
62
+ if p.is_dir():
63
+ candidate = p / ".jarvis" / "c2rust" / "symbols.jsonl"
64
+ return candidate
65
+ return p
66
+
67
+ def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
68
+ try:
69
+ symbols_file_arg = args.get("symbols_file")
70
+ symbols_arg = args.get("symbols")
71
+
72
+ if not isinstance(symbols_file_arg, str) or not symbols_file_arg.strip():
73
+ return {"success": False, "stdout": "", "stderr": "缺少或无效的 symbols_file 参数"}
74
+
75
+ if not isinstance(symbols_arg, list) or not all(isinstance(s, str) for s in symbols_arg):
76
+ return {"success": False, "stdout": "", "stderr": "symbols 参数必须是字符串列表"}
77
+
78
+ symbols_path = self._resolve_symbols_jsonl_path(symbols_file_arg)
79
+ print(f"[read_symbols] Resolved symbols file path: {symbols_path}")
80
+ if not symbols_path.exists():
81
+ return {"success": False, "stdout": "", "stderr": f"符号表文件不存在: {symbols_path}"}
82
+ if not symbols_path.is_file():
83
+ return {"success": False, "stdout": "", "stderr": f"符号表路径不是文件: {symbols_path}"}
84
+
85
+ # 使用集合提升匹配效率;保持原请求顺序以便输出
86
+ requested: List[str] = [s.strip() for s in symbols_arg if s and s.strip()]
87
+ wanted_set = set(requested)
88
+ print(f"[read_symbols] Requested {len(wanted_set)} unique symbols.")
89
+
90
+ results: Dict[str, List[Dict[str, Any]]] = {s: [] for s in requested}
91
+
92
+ # 流式读取,避免载入整个大文件
93
+ with open(symbols_path, "r", encoding="utf-8") as f:
94
+ for line in f:
95
+ line = line.strip()
96
+ if not line:
97
+ continue
98
+ try:
99
+ obj = json.loads(line)
100
+ except Exception:
101
+ continue
102
+
103
+ name = obj.get("name") or ""
104
+ qname = obj.get("qualified_name") or ""
105
+
106
+ # 仅当命中请求的符号时才记录
107
+ if name in wanted_set:
108
+ results[name].append(obj)
109
+ if qname in wanted_set and qname != name:
110
+ results[qname].append(obj)
111
+
112
+ not_found = [s for s in requested if not results.get(s)]
113
+ if not_found:
114
+ print(f"[read_symbols] Symbols not found: {not_found}")
115
+ found_counts = {s: len(results.get(s, [])) for s in requested}
116
+
117
+ out_obj: Dict[str, Any] = {
118
+ "symbols_file": str(symbols_path),
119
+ "requested": requested,
120
+ "found_counts": found_counts,
121
+ "not_found": not_found,
122
+ "items": results,
123
+ }
124
+
125
+ stdout = json.dumps(out_obj, ensure_ascii=False, indent=2)
126
+ # 简要状态打印(不包含具体内容)
127
+ try:
128
+ status_lines = []
129
+ for s in requested:
130
+ cnt = found_counts.get(s, 0)
131
+ status_lines.append(f"[read_symbols] {s}: {cnt} 条匹配")
132
+ if status_lines:
133
+ print("\n".join(status_lines), end="\n")
134
+ except Exception:
135
+ pass
136
+
137
+ return {"success": True, "stdout": stdout, "stderr": ""}
138
+
139
+ except Exception as e:
140
+ PrettyOutput.print(str(e), OutputType.ERROR)
141
+ return {"success": False, "stdout": "", "stderr": f"读取符号表失败: {str(e)}"}