jarvis-ai-assistant 0.3.30__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 (115) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +289 -87
  3. jarvis/jarvis_agent/agent_manager.py +17 -8
  4. jarvis/jarvis_agent/edit_file_handler.py +374 -86
  5. jarvis/jarvis_agent/event_bus.py +1 -1
  6. jarvis/jarvis_agent/file_context_handler.py +79 -0
  7. jarvis/jarvis_agent/jarvis.py +601 -43
  8. jarvis/jarvis_agent/main.py +32 -2
  9. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  10. jarvis/jarvis_agent/run_loop.py +38 -5
  11. jarvis/jarvis_agent/share_manager.py +8 -1
  12. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  13. jarvis/jarvis_agent/task_analyzer.py +5 -2
  14. jarvis/jarvis_agent/task_planner.py +496 -0
  15. jarvis/jarvis_agent/utils.py +5 -1
  16. jarvis/jarvis_agent/web_bridge.py +189 -0
  17. jarvis/jarvis_agent/web_output_sink.py +53 -0
  18. jarvis/jarvis_agent/web_server.py +751 -0
  19. jarvis/jarvis_c2rust/__init__.py +26 -0
  20. jarvis/jarvis_c2rust/cli.py +613 -0
  21. jarvis/jarvis_c2rust/collector.py +258 -0
  22. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  23. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  24. jarvis/jarvis_c2rust/optimizer.py +960 -0
  25. jarvis/jarvis_c2rust/scanner.py +1681 -0
  26. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  27. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  28. jarvis/jarvis_code_agent/code_agent.py +1171 -94
  29. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  30. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  31. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  32. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  33. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  34. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  35. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  36. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  37. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  38. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  39. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  40. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  41. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  42. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  43. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  44. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  45. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  46. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  47. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  48. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  49. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  50. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  51. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  52. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  53. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  54. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  55. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  56. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  57. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  58. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  59. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  60. jarvis/jarvis_code_agent/lint.py +270 -8
  61. jarvis/jarvis_code_agent/utils.py +142 -0
  62. jarvis/jarvis_code_analysis/code_review.py +483 -569
  63. jarvis/jarvis_data/config_schema.json +97 -8
  64. jarvis/jarvis_git_utils/git_commiter.py +38 -26
  65. jarvis/jarvis_mcp/sse_mcp_client.py +2 -2
  66. jarvis/jarvis_mcp/stdio_mcp_client.py +1 -1
  67. jarvis/jarvis_memory_organizer/memory_organizer.py +1 -1
  68. jarvis/jarvis_multi_agent/__init__.py +239 -25
  69. jarvis/jarvis_multi_agent/main.py +37 -1
  70. jarvis/jarvis_platform/base.py +103 -51
  71. jarvis/jarvis_platform/openai.py +26 -1
  72. jarvis/jarvis_platform/yuanbao.py +1 -1
  73. jarvis/jarvis_platform_manager/service.py +2 -2
  74. jarvis/jarvis_rag/cli.py +4 -4
  75. jarvis/jarvis_sec/__init__.py +3605 -0
  76. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  77. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  78. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  79. jarvis/jarvis_sec/cli.py +116 -0
  80. jarvis/jarvis_sec/report.py +257 -0
  81. jarvis/jarvis_sec/status.py +264 -0
  82. jarvis/jarvis_sec/types.py +20 -0
  83. jarvis/jarvis_sec/workflow.py +219 -0
  84. jarvis/jarvis_stats/cli.py +1 -1
  85. jarvis/jarvis_stats/stats.py +1 -1
  86. jarvis/jarvis_stats/visualizer.py +1 -1
  87. jarvis/jarvis_tools/cli/main.py +1 -0
  88. jarvis/jarvis_tools/execute_script.py +46 -9
  89. jarvis/jarvis_tools/generate_new_tool.py +3 -1
  90. jarvis/jarvis_tools/read_code.py +275 -12
  91. jarvis/jarvis_tools/read_symbols.py +141 -0
  92. jarvis/jarvis_tools/read_webpage.py +5 -3
  93. jarvis/jarvis_tools/registry.py +73 -35
  94. jarvis/jarvis_tools/search_web.py +15 -11
  95. jarvis/jarvis_tools/sub_agent.py +24 -42
  96. jarvis/jarvis_tools/sub_code_agent.py +14 -13
  97. jarvis/jarvis_tools/virtual_tty.py +1 -1
  98. jarvis/jarvis_utils/config.py +187 -35
  99. jarvis/jarvis_utils/embedding.py +3 -0
  100. jarvis/jarvis_utils/git_utils.py +181 -6
  101. jarvis/jarvis_utils/globals.py +3 -3
  102. jarvis/jarvis_utils/http.py +1 -1
  103. jarvis/jarvis_utils/input.py +78 -2
  104. jarvis/jarvis_utils/methodology.py +25 -19
  105. jarvis/jarvis_utils/utils.py +644 -359
  106. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/METADATA +85 -1
  107. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  108. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +4 -0
  109. jarvis/jarvis_agent/config.py +0 -92
  110. jarvis/jarvis_tools/edit_file.py +0 -179
  111. jarvis/jarvis_tools/rewrite_file.py +0 -191
  112. jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
  113. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  114. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  115. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -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,6 +63,7 @@ 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]: 包含成功状态、输出内容和错误信息的字典
@@ -62,10 +88,9 @@ 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:
@@ -99,14 +124,41 @@ class ReadCodeTool:
99
124
  "stderr": f"无效的行范围 [{start_line}-{end_line}] (总行数: {total_lines})",
100
125
  }
101
126
 
102
- # 添加行号并构建输出内容
103
- selected_lines = lines[start_line - 1 : end_line]
104
- numbered_content = "".join(
105
- [
106
- f"{i:4d}:{line}"
107
- for i, line in enumerate(selected_lines, start=start_line)
108
- ]
109
- )
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))
110
162
 
111
163
  # 构建输出格式
112
164
  output = (
@@ -115,6 +167,11 @@ class ReadCodeTool:
115
167
  f"{numbered_content}\n\n"
116
168
  )
117
169
 
170
+ # 尝试获取并附加上下文信息
171
+ context_info = self._get_file_context(abs_path, start_line, end_line, agent)
172
+ if context_info:
173
+ output += context_info
174
+
118
175
  if agent:
119
176
  files = agent.get_user_data("files")
120
177
  if files:
@@ -129,6 +186,105 @@ class ReadCodeTool:
129
186
  PrettyOutput.print(str(e), OutputType.ERROR)
130
187
  return {"success": False, "stdout": "", "stderr": f"文件读取失败: {str(e)}"}
131
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
+
132
288
  def execute(self, args: Dict) -> Dict[str, Any]:
133
289
  """执行代码读取操作
134
290
 
@@ -149,7 +305,105 @@ class ReadCodeTool:
149
305
 
150
306
  all_outputs = []
151
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
+ }
152
405
 
406
+ # 第二遍:实际读取文件
153
407
  for file_info in args["files"]:
154
408
  if not isinstance(file_info, dict) or "path" not in file_info:
155
409
  continue
@@ -163,13 +417,22 @@ class ReadCodeTool:
163
417
 
164
418
  if result["success"]:
165
419
  all_outputs.append(result["stdout"])
420
+ status_lines.append(f"✅ {file_info['path']} 文件读取成功")
166
421
  else:
167
422
  all_outputs.append(f"❌ {file_info['path']}: {result['stderr']}")
423
+ status_lines.append(f"❌ {file_info['path']} 文件读取失败")
168
424
  overall_success = False
169
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
170
433
  return {
171
434
  "success": overall_success,
172
- "stdout": "\n".join(all_outputs),
435
+ "stdout": stdout_text,
173
436
  "stderr": "",
174
437
  }
175
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)}"}
@@ -9,7 +9,7 @@ from jarvis.jarvis_utils.config import (
9
9
  )
10
10
  from jarvis.jarvis_utils.http import get as http_get
11
11
  from markdownify import markdownify as md # type: ignore
12
- import requests
12
+ import requests # type: ignore[import-untyped]
13
13
 
14
14
 
15
15
  class WebpageTool:
@@ -58,7 +58,8 @@ class WebpageTool:
58
58
  2. 包含网页标题
59
59
  3. 根据用户需求提供准确、完整的信息"""
60
60
  response = model.chat_until_success(prompt) # type: ignore
61
- return {"success": True, "stdout": response, "stderr": ""}
61
+ if response and response.strip():
62
+ return {"success": True, "stdout": response, "stderr": ""}
62
63
 
63
64
  # 2) 然后尝试使用默认平台(normal)的 web 能力
64
65
  model = PlatformRegistry().get_normal_platform()
@@ -73,7 +74,8 @@ class WebpageTool:
73
74
  2. 包含网页标题
74
75
  3. 根据用户需求提供准确、完整的信息"""
75
76
  response = model.chat_until_success(prompt) # type: ignore
76
- return {"success": True, "stdout": response, "stderr": ""}
77
+ if response and response.strip():
78
+ return {"success": True, "stdout": response, "stderr": ""}
77
79
 
78
80
  # 3) 回退:使用 requests 抓取网页,再用模型分析
79
81