jarvis-ai-assistant 0.7.0__py3-none-any.whl → 0.7.8__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 (159) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +243 -139
  3. jarvis/jarvis_agent/agent_manager.py +5 -10
  4. jarvis/jarvis_agent/builtin_input_handler.py +2 -6
  5. jarvis/jarvis_agent/config_editor.py +2 -7
  6. jarvis/jarvis_agent/event_bus.py +82 -12
  7. jarvis/jarvis_agent/file_context_handler.py +265 -15
  8. jarvis/jarvis_agent/file_methodology_manager.py +3 -4
  9. jarvis/jarvis_agent/jarvis.py +113 -98
  10. jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
  11. jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
  12. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
  13. jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
  14. jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
  15. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
  16. jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
  17. jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
  18. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
  19. jarvis/jarvis_agent/language_support_info.py +486 -0
  20. jarvis/jarvis_agent/main.py +6 -12
  21. jarvis/jarvis_agent/memory_manager.py +7 -16
  22. jarvis/jarvis_agent/methodology_share_manager.py +10 -16
  23. jarvis/jarvis_agent/prompt_manager.py +1 -1
  24. jarvis/jarvis_agent/prompts.py +193 -171
  25. jarvis/jarvis_agent/protocols.py +8 -12
  26. jarvis/jarvis_agent/run_loop.py +77 -14
  27. jarvis/jarvis_agent/session_manager.py +2 -3
  28. jarvis/jarvis_agent/share_manager.py +12 -21
  29. jarvis/jarvis_agent/shell_input_handler.py +1 -2
  30. jarvis/jarvis_agent/task_analyzer.py +26 -4
  31. jarvis/jarvis_agent/task_manager.py +11 -27
  32. jarvis/jarvis_agent/tool_executor.py +2 -3
  33. jarvis/jarvis_agent/tool_share_manager.py +12 -24
  34. jarvis/jarvis_agent/web_server.py +55 -20
  35. jarvis/jarvis_c2rust/__init__.py +5 -5
  36. jarvis/jarvis_c2rust/cli.py +461 -499
  37. jarvis/jarvis_c2rust/collector.py +45 -53
  38. jarvis/jarvis_c2rust/constants.py +26 -0
  39. jarvis/jarvis_c2rust/library_replacer.py +264 -132
  40. jarvis/jarvis_c2rust/llm_module_agent.py +162 -190
  41. jarvis/jarvis_c2rust/loaders.py +207 -0
  42. jarvis/jarvis_c2rust/models.py +28 -0
  43. jarvis/jarvis_c2rust/optimizer.py +1592 -395
  44. jarvis/jarvis_c2rust/transpiler.py +1722 -1064
  45. jarvis/jarvis_c2rust/utils.py +385 -0
  46. jarvis/jarvis_code_agent/build_validation_config.py +2 -3
  47. jarvis/jarvis_code_agent/code_agent.py +394 -320
  48. jarvis/jarvis_code_agent/code_analyzer/__init__.py +3 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +4 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +17 -2
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +3 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +36 -4
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +9 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +9 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +12 -1
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +22 -5
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +57 -32
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +62 -6
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +8 -9
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +290 -5
  61. jarvis/jarvis_code_agent/code_analyzer/language_support.py +21 -0
  62. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +21 -3
  63. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +72 -4
  64. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +35 -3
  65. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
  66. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +52 -2
  68. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +73 -1
  69. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
  70. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +306 -152
  71. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
  72. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +193 -18
  73. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +18 -8
  74. jarvis/jarvis_code_agent/lint.py +258 -27
  75. jarvis/jarvis_code_agent/utils.py +0 -1
  76. jarvis/jarvis_code_analysis/code_review.py +19 -24
  77. jarvis/jarvis_data/config_schema.json +53 -26
  78. jarvis/jarvis_git_squash/main.py +4 -5
  79. jarvis/jarvis_git_utils/git_commiter.py +44 -49
  80. jarvis/jarvis_mcp/sse_mcp_client.py +20 -27
  81. jarvis/jarvis_mcp/stdio_mcp_client.py +11 -12
  82. jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
  83. jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
  84. jarvis/jarvis_methodology/main.py +32 -48
  85. jarvis/jarvis_multi_agent/__init__.py +79 -61
  86. jarvis/jarvis_multi_agent/main.py +3 -7
  87. jarvis/jarvis_platform/base.py +469 -199
  88. jarvis/jarvis_platform/human.py +7 -8
  89. jarvis/jarvis_platform/kimi.py +30 -36
  90. jarvis/jarvis_platform/openai.py +65 -27
  91. jarvis/jarvis_platform/registry.py +26 -10
  92. jarvis/jarvis_platform/tongyi.py +24 -25
  93. jarvis/jarvis_platform/yuanbao.py +31 -42
  94. jarvis/jarvis_platform_manager/main.py +66 -77
  95. jarvis/jarvis_platform_manager/service.py +8 -13
  96. jarvis/jarvis_rag/cli.py +49 -51
  97. jarvis/jarvis_rag/embedding_manager.py +13 -18
  98. jarvis/jarvis_rag/llm_interface.py +8 -9
  99. jarvis/jarvis_rag/query_rewriter.py +10 -21
  100. jarvis/jarvis_rag/rag_pipeline.py +24 -27
  101. jarvis/jarvis_rag/reranker.py +4 -5
  102. jarvis/jarvis_rag/retriever.py +28 -30
  103. jarvis/jarvis_sec/__init__.py +220 -3520
  104. jarvis/jarvis_sec/agents.py +143 -0
  105. jarvis/jarvis_sec/analysis.py +276 -0
  106. jarvis/jarvis_sec/cli.py +29 -6
  107. jarvis/jarvis_sec/clustering.py +1439 -0
  108. jarvis/jarvis_sec/file_manager.py +427 -0
  109. jarvis/jarvis_sec/parsers.py +73 -0
  110. jarvis/jarvis_sec/prompts.py +268 -0
  111. jarvis/jarvis_sec/report.py +83 -4
  112. jarvis/jarvis_sec/review.py +453 -0
  113. jarvis/jarvis_sec/utils.py +499 -0
  114. jarvis/jarvis_sec/verification.py +848 -0
  115. jarvis/jarvis_sec/workflow.py +7 -0
  116. jarvis/jarvis_smart_shell/main.py +38 -87
  117. jarvis/jarvis_stats/cli.py +1 -1
  118. jarvis/jarvis_stats/stats.py +7 -7
  119. jarvis/jarvis_stats/storage.py +15 -21
  120. jarvis/jarvis_tools/clear_memory.py +3 -20
  121. jarvis/jarvis_tools/cli/main.py +20 -23
  122. jarvis/jarvis_tools/edit_file.py +1066 -0
  123. jarvis/jarvis_tools/execute_script.py +42 -21
  124. jarvis/jarvis_tools/file_analyzer.py +6 -9
  125. jarvis/jarvis_tools/generate_new_tool.py +11 -20
  126. jarvis/jarvis_tools/lsp_client.py +1552 -0
  127. jarvis/jarvis_tools/methodology.py +2 -3
  128. jarvis/jarvis_tools/read_code.py +1525 -87
  129. jarvis/jarvis_tools/read_symbols.py +2 -3
  130. jarvis/jarvis_tools/read_webpage.py +7 -10
  131. jarvis/jarvis_tools/registry.py +370 -181
  132. jarvis/jarvis_tools/retrieve_memory.py +20 -19
  133. jarvis/jarvis_tools/rewrite_file.py +105 -0
  134. jarvis/jarvis_tools/save_memory.py +3 -15
  135. jarvis/jarvis_tools/search_web.py +3 -7
  136. jarvis/jarvis_tools/sub_agent.py +17 -6
  137. jarvis/jarvis_tools/sub_code_agent.py +14 -16
  138. jarvis/jarvis_tools/virtual_tty.py +54 -32
  139. jarvis/jarvis_utils/clipboard.py +7 -10
  140. jarvis/jarvis_utils/config.py +98 -63
  141. jarvis/jarvis_utils/embedding.py +5 -5
  142. jarvis/jarvis_utils/fzf.py +8 -8
  143. jarvis/jarvis_utils/git_utils.py +81 -67
  144. jarvis/jarvis_utils/input.py +24 -49
  145. jarvis/jarvis_utils/jsonnet_compat.py +465 -0
  146. jarvis/jarvis_utils/methodology.py +33 -35
  147. jarvis/jarvis_utils/utils.py +245 -202
  148. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.8.dist-info}/METADATA +205 -70
  149. jarvis_ai_assistant-0.7.8.dist-info/RECORD +218 -0
  150. jarvis/jarvis_agent/edit_file_handler.py +0 -584
  151. jarvis/jarvis_agent/rewrite_file_handler.py +0 -141
  152. jarvis/jarvis_agent/task_planner.py +0 -496
  153. jarvis/jarvis_platform/ai8.py +0 -332
  154. jarvis/jarvis_tools/ask_user.py +0 -54
  155. jarvis_ai_assistant-0.7.0.dist-info/RECORD +0 -192
  156. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.8.dist-info}/WHEEL +0 -0
  157. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.8.dist-info}/entry_points.txt +0 -0
  158. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.8.dist-info}/licenses/LICENSE +0 -0
  159. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,499 @@
1
+ # -*- coding: utf-8 -*-
2
+ """工具函数模块"""
3
+
4
+ from typing import Dict, List, Optional
5
+ from pathlib import Path
6
+ import json
7
+ import typer
8
+
9
+ from jarvis.jarvis_sec.workflow import direct_scan
10
+
11
+
12
+ def git_restore_if_dirty(repo_root: str) -> int:
13
+ """
14
+ 若 repo_root 为 git 仓库:检测工作区是否有变更;如有则使用 'git checkout -- .' 恢复。
15
+ 返回估算的变更文件数(基于 git status --porcelain 的行数)。
16
+ """
17
+ try:
18
+ import subprocess as _sub
19
+ root = Path(repo_root)
20
+ if not (root / ".git").exists():
21
+ return 0
22
+ proc = _sub.run(["git", "status", "--porcelain"], cwd=str(root), capture_output=True, text=True)
23
+ if proc.returncode != 0:
24
+ return 0
25
+ lines = [line for line in proc.stdout.splitlines() if line.strip()]
26
+ if lines:
27
+ _sub.run(["git", "checkout", "--", "."], cwd=str(root), capture_output=True, text=True)
28
+ return len(lines)
29
+ except Exception:
30
+ pass
31
+ return 0
32
+
33
+
34
+ def get_sec_dir(base_path: str) -> Path:
35
+ """获取 .jarvis/sec 目录路径,支持 base_path 是项目根目录或已经是 .jarvis/sec 目录"""
36
+ base = Path(base_path)
37
+ # 检查 base_path 是否已经是 .jarvis/sec 目录
38
+ if base.name == "sec" and base.parent.name == ".jarvis":
39
+ return base
40
+ # 否则,假设 base_path 是项目根目录
41
+ return base / ".jarvis" / "sec"
42
+
43
+
44
+ def initialize_analysis_context(
45
+ entry_path: str,
46
+ status_mgr,
47
+ ) -> tuple:
48
+ """
49
+ 初始化分析上下文,包括状态管理、进度文件、目录等。
50
+
51
+ 返回: (sec_dir, progress_path, _progress_append)
52
+ """
53
+ # 获取 .jarvis/sec 目录
54
+ sec_dir = get_sec_dir(entry_path)
55
+ progress_path = None # 不再使用 progress.jsonl
56
+
57
+ # 进度追加函数(空函数,不再记录)
58
+ def _progress_append(rec: Dict) -> None:
59
+ pass # 不再记录进度日志
60
+
61
+ return sec_dir, progress_path, _progress_append
62
+
63
+
64
+ def load_or_run_heuristic_scan(
65
+ entry_path: str,
66
+ langs: List[str],
67
+ exclude_dirs: Optional[List[str]],
68
+ sec_dir: Path,
69
+ status_mgr,
70
+ _progress_append,
71
+ ) -> tuple[List[Dict], Dict]:
72
+ """
73
+ 加载或运行启发式扫描。
74
+
75
+ 优先从新的 candidates.jsonl 文件加载,如果不存在则回退到旧的 heuristic_issues.jsonl。
76
+
77
+ 返回: (candidates, summary)
78
+ """
79
+ candidates: List[Dict] = []
80
+ summary: Dict = {}
81
+
82
+ # 优先使用新的 candidates.jsonl 文件
83
+ from jarvis.jarvis_sec.file_manager import load_candidates, get_candidates_file
84
+ candidates = load_candidates(sec_dir)
85
+
86
+ if candidates:
87
+ try:
88
+ typer.secho(f"[jarvis-sec] 从 {get_candidates_file(sec_dir)} 恢复启发式扫描", fg=typer.colors.BLUE)
89
+ _progress_append({
90
+ "event": "pre_scan_resumed",
91
+ "path": str(get_candidates_file(sec_dir)),
92
+ "issues_found": len(candidates)
93
+ })
94
+ except Exception:
95
+ pass
96
+ else:
97
+ # 回退到旧的 heuristic_issues.jsonl 文件(向后兼容)
98
+ _heuristic_path = sec_dir / "heuristic_issues.jsonl"
99
+ if _heuristic_path.exists():
100
+ try:
101
+ typer.secho(f"[jarvis-sec] 从 {_heuristic_path} 恢复启发式扫描(旧格式)", fg=typer.colors.BLUE)
102
+ with _heuristic_path.open("r", encoding="utf-8") as f:
103
+ for line in f:
104
+ if line.strip():
105
+ candidates.append(json.loads(line))
106
+ _progress_append({
107
+ "event": "pre_scan_resumed",
108
+ "path": str(_heuristic_path),
109
+ "issues_found": len(candidates)
110
+ })
111
+ except Exception as e:
112
+ typer.secho(f"[jarvis-sec] 恢复启发式扫描失败,执行完整扫描: {e}", fg=typer.colors.YELLOW)
113
+ candidates = [] # 重置以便执行完整扫描
114
+
115
+ if not candidates:
116
+ _progress_append({"event": "pre_scan_start", "entry_path": entry_path, "languages": langs})
117
+ status_mgr.update_pre_scan(message="开始启发式扫描...")
118
+ pre_scan = direct_scan(entry_path, languages=langs, exclude_dirs=exclude_dirs)
119
+ candidates = pre_scan.get("issues", [])
120
+ summary = pre_scan.get("summary", {})
121
+ scanned_files = summary.get("scanned_files", 0)
122
+ status_mgr.update_pre_scan(
123
+ current_files=scanned_files,
124
+ total_files=scanned_files,
125
+ issues_found=len(candidates),
126
+ message=f"启发式扫描完成,发现 {len(candidates)} 个候选问题"
127
+ )
128
+ _progress_append({
129
+ "event": "pre_scan_done",
130
+ "entry_path": entry_path,
131
+ "languages": langs,
132
+ "scanned_files": scanned_files,
133
+ "issues_found": len(candidates)
134
+ })
135
+ # 持久化
136
+ try:
137
+ _heuristic_path.parent.mkdir(parents=True, exist_ok=True)
138
+ with _heuristic_path.open("w", encoding="utf-8") as f:
139
+ for item in candidates:
140
+ f.write(json.dumps(item, ensure_ascii=False) + "\n")
141
+ _progress_append({
142
+ "event": "heuristic_report_written",
143
+ "path": str(_heuristic_path),
144
+ "issues_count": len(candidates),
145
+ })
146
+ typer.secho(f"[jarvis-sec] 已将 {len(candidates)} 个启发式扫描问题写入 {_heuristic_path}", fg=typer.colors.GREEN)
147
+ except Exception:
148
+ pass
149
+ else:
150
+ # 从断点恢复启发式扫描结果
151
+ status_mgr.update_pre_scan(
152
+ issues_found=len(candidates),
153
+ message=f"从断点恢复,已发现 {len(candidates)} 个候选问题"
154
+ )
155
+
156
+ return candidates, summary
157
+
158
+
159
+ def compact_candidate(it: Dict) -> Dict:
160
+ """精简候选问题,只保留必要字段"""
161
+ result = {
162
+ "language": it.get("language"),
163
+ "category": it.get("category"),
164
+ "pattern": it.get("pattern"),
165
+ "file": it.get("file"),
166
+ "line": it.get("line"),
167
+ "evidence": it.get("evidence"),
168
+ "confidence": it.get("confidence"),
169
+ "severity": it.get("severity", "medium"),
170
+ }
171
+ # 如果候选已经有gid,保留它(用于断点恢复)
172
+ if "gid" in it:
173
+ try:
174
+ gid_val = int(it.get("gid", 0))
175
+ if gid_val >= 1:
176
+ result["gid"] = gid_val
177
+ except Exception:
178
+ pass
179
+ return result
180
+
181
+
182
+ def prepare_candidates(candidates: List[Dict]) -> List[Dict]:
183
+ """
184
+ 将候选问题精简为子任务清单,控制上下文长度,并分配全局唯一ID。
185
+
186
+ 返回: compact_candidates (已分配gid的候选列表)
187
+ """
188
+ compact_candidates = [compact_candidate(it) for it in candidates]
189
+
190
+ # 检查是否所有候选都已经有gid(从heuristic_issues.jsonl恢复时)
191
+ all_have_gid = all("gid" in it and isinstance(it.get("gid"), int) and it.get("gid", 0) >= 1 for it in compact_candidates)
192
+
193
+ if not all_have_gid:
194
+ # 如果有候选没有gid,需要分配
195
+ # 优先保留已有的gid,为没有gid的候选分配新的gid
196
+ existing_gids = set()
197
+ for it in compact_candidates:
198
+ try:
199
+ gid_val = int(it.get("gid", 0))
200
+ if gid_val >= 1:
201
+ existing_gids.add(gid_val)
202
+ except Exception:
203
+ pass
204
+
205
+ # 为没有gid的候选分配新的gid
206
+ next_gid = 1
207
+ for it in compact_candidates:
208
+ if "gid" not in it or not isinstance(it.get("gid"), int) or it.get("gid", 0) < 1:
209
+ # 找到一个未使用的gid
210
+ while next_gid in existing_gids:
211
+ next_gid += 1
212
+ try:
213
+ it["gid"] = next_gid
214
+ existing_gids.add(next_gid)
215
+ next_gid += 1
216
+ except Exception:
217
+ pass
218
+
219
+ return compact_candidates
220
+
221
+
222
+ def group_candidates_by_file(candidates: List[Dict]) -> Dict[str, List[Dict]]:
223
+ """按文件分组候选问题"""
224
+ from collections import defaultdict
225
+ groups: Dict[str, List[Dict]] = defaultdict(list)
226
+ for it in candidates:
227
+ groups[str(it.get("file") or "")].append(it)
228
+ return groups
229
+
230
+
231
+ def create_report_writer(sec_dir: Path, report_file: Optional[str]):
232
+ """创建报告写入函数"""
233
+ from jarvis.jarvis_sec.file_manager import save_analysis_result, load_clusters
234
+
235
+ def _append_report(items, source: str, task_id: str, cand: Dict):
236
+ """
237
+ 将当前子任务的检测结果追加写入 analysis.jsonl 文件。
238
+
239
+ 参数:
240
+ - items: 验证通过的问题列表(has_risk: true)
241
+ - source: 来源("analysis_only" 或 "verified")
242
+ - task_id: 任务ID(如 "JARVIS-SEC-Batch-1")
243
+ - cand: 候选信息,包含 batch 和 candidates
244
+ """
245
+ if not items:
246
+ return
247
+
248
+ try:
249
+ # 从批次中提取信息
250
+ batch = cand.get("batch", False)
251
+ candidates = cand.get("candidates", [])
252
+
253
+ if not batch or not candidates:
254
+ # 如果没有批次信息,回退到旧格式(向后兼容)
255
+ path = Path(report_file) if report_file else sec_dir / "agent_issues.jsonl"
256
+ path.parent.mkdir(parents=True, exist_ok=True)
257
+ with path.open("a", encoding="utf-8") as f:
258
+ for item in items:
259
+ line = json.dumps(item, ensure_ascii=False)
260
+ f.write(line + "\n")
261
+ try:
262
+ typer.secho(f"[jarvis-sec] 已将 {len(items)} 个问题写入 {path}(旧格式)", fg=typer.colors.GREEN)
263
+ except Exception:
264
+ pass
265
+ return
266
+
267
+ # 从批次中提取 file 和 gids
268
+ batch_file = candidates[0].get("file") if candidates else ""
269
+ batch_gids = []
270
+ for c in candidates:
271
+ try:
272
+ gid = int(c.get("gid", 0))
273
+ if gid >= 1:
274
+ batch_gids.append(gid)
275
+ except Exception:
276
+ pass
277
+
278
+ # 从 clusters.jsonl 中查找对应的 cluster_id
279
+ clusters = load_clusters(sec_dir)
280
+ cluster_id = None
281
+ batch_index = None
282
+ cluster_index = None
283
+
284
+ # 尝试从 task_id 中提取 batch_index(格式:JARVIS-SEC-Batch-1)
285
+ try:
286
+ if "Batch-" in task_id:
287
+ batch_index = int(task_id.split("Batch-")[1])
288
+ except Exception:
289
+ pass
290
+
291
+ # 查找匹配的聚类(通过 file 和 gids)
292
+ for cluster in clusters:
293
+ cluster_file = str(cluster.get("file", ""))
294
+ cluster_gids = cluster.get("gids", [])
295
+
296
+ if cluster_file == batch_file and set(cluster_gids) == set(batch_gids):
297
+ cluster_id = cluster.get("cluster_id", "")
298
+ if not cluster_id:
299
+ # 如果没有 cluster_id,生成一个
300
+ cluster_id = f"{cluster_file}|{cluster.get('batch_index', batch_index or 0)}|{cluster.get('cluster_index', 0)}"
301
+ batch_index = cluster.get("batch_index", batch_index or 0)
302
+ cluster_index = cluster.get("cluster_index", 0)
303
+ break
304
+
305
+ # 如果找不到匹配的聚类,生成一个临时的 cluster_id
306
+ if not cluster_id:
307
+ cluster_id = f"{batch_file}|{batch_index or 0}|0"
308
+ batch_index = batch_index or 0
309
+ cluster_index = 0
310
+
311
+ # 分离验证为问题的gid和误报的gid
312
+ verified_gids = []
313
+ false_positive_gids = []
314
+ issues = []
315
+
316
+ # 从 items 中提取已验证的问题
317
+ for item in items:
318
+ try:
319
+ gid = int(item.get("gid", 0))
320
+ if gid >= 1:
321
+ has_risk = item.get("has_risk", False)
322
+ if has_risk:
323
+ verified_gids.append(gid)
324
+ issues.append(item)
325
+ else:
326
+ false_positive_gids.append(gid)
327
+ except Exception:
328
+ pass
329
+
330
+ # 从 candidates 中提取所有未在 items 中的 gid(这些可能是误报)
331
+ for c in candidates:
332
+ try:
333
+ gid = int(c.get("gid", 0))
334
+ if gid >= 1 and gid not in verified_gids and gid not in false_positive_gids:
335
+ # 如果这个 gid 不在已验证的问题中,可能是误报
336
+ false_positive_gids.append(gid)
337
+ except Exception:
338
+ pass
339
+
340
+ # 构建分析结果记录
341
+ analysis_result = {
342
+ "cluster_id": cluster_id,
343
+ "file": batch_file,
344
+ "batch_index": batch_index,
345
+ "cluster_index": cluster_index,
346
+ "gids": batch_gids,
347
+ "verified_gids": verified_gids,
348
+ "false_positive_gids": false_positive_gids,
349
+ "issues": issues,
350
+ }
351
+
352
+ # 保存到 analysis.jsonl
353
+ save_analysis_result(sec_dir, analysis_result)
354
+
355
+ try:
356
+ typer.secho(f"[jarvis-sec] 已将批次 {batch_index} 的分析结果写入 analysis.jsonl(问题: {len(verified_gids)}, 误报: {len(false_positive_gids)})", fg=typer.colors.GREEN)
357
+ except Exception:
358
+ pass
359
+ except Exception as e:
360
+ # 报告写入失败不影响主流程
361
+ try:
362
+ typer.secho(f"[jarvis-sec] 警告:保存分析结果失败: {e}", fg=typer.colors.YELLOW)
363
+ except Exception:
364
+ pass
365
+
366
+ return _append_report
367
+
368
+
369
+ def sig_of(c: Dict) -> str:
370
+ """生成候选问题的签名"""
371
+ return f"{c.get('language','')}|{c.get('file','')}|{c.get('line','')}|{c.get('pattern','')}"
372
+
373
+
374
+ def load_processed_gids_from_issues(sec_dir: Path) -> set:
375
+ """从 agent_issues.jsonl 中读取已处理的 gid"""
376
+ processed_gids = set()
377
+ try:
378
+ _agent_issues_path = sec_dir / "agent_issues.jsonl"
379
+ if _agent_issues_path.exists():
380
+ with _agent_issues_path.open("r", encoding="utf-8", errors="ignore") as f:
381
+ for line in f:
382
+ line = line.strip()
383
+ if not line:
384
+ continue
385
+ try:
386
+ issue_obj = json.loads(line)
387
+ _gid = int(issue_obj.get("gid", 0))
388
+ if _gid >= 1:
389
+ processed_gids.add(_gid)
390
+ except Exception:
391
+ pass
392
+ if processed_gids:
393
+ try:
394
+ typer.secho(f"[jarvis-sec] 断点恢复:从 agent_issues.jsonl 读取到 {len(processed_gids)} 个已处理的 gid", fg=typer.colors.BLUE)
395
+ except Exception:
396
+ pass
397
+ except Exception:
398
+ pass
399
+ return processed_gids
400
+
401
+
402
+ def count_issues_from_file(sec_dir: Path) -> int:
403
+ """从 analysis.jsonl 读取问题数量"""
404
+ from jarvis.jarvis_sec.file_manager import get_verified_issue_gids
405
+ verified_gids = get_verified_issue_gids(sec_dir)
406
+ return len(verified_gids)
407
+
408
+
409
+ def count_issues_from_file_old(sec_dir: Path) -> int:
410
+ """从 agent_issues.jsonl 中读取当前问题总数(用于状态显示)"""
411
+ count = 0
412
+ try:
413
+ _agent_issues_path = sec_dir / "agent_issues.jsonl"
414
+ if _agent_issues_path.exists():
415
+ saved_gids = set()
416
+ with _agent_issues_path.open("r", encoding="utf-8", errors="ignore") as f:
417
+ for line in f:
418
+ line = line.strip()
419
+ if not line:
420
+ continue
421
+ try:
422
+ item = json.loads(line)
423
+ gid = item.get("gid", 0)
424
+ if gid >= 1 and gid not in saved_gids:
425
+ # 只统计验证通过的告警(has_risk: true 且有 verification_notes)
426
+ if item.get("has_risk") is True and "verification_notes" in item:
427
+ count += 1
428
+ saved_gids.add(gid)
429
+ except Exception:
430
+ pass
431
+ except Exception:
432
+ pass
433
+ return count
434
+
435
+
436
+ def load_all_issues_from_file(sec_dir: Path) -> List[Dict]:
437
+ """从 agent_issues.jsonl 读取所有已保存的告警"""
438
+ all_issues: List[Dict] = []
439
+ try:
440
+ _agent_issues_path = sec_dir / "agent_issues.jsonl"
441
+ if _agent_issues_path.exists():
442
+ saved_gids_from_file = set()
443
+ with _agent_issues_path.open("r", encoding="utf-8", errors="ignore") as f:
444
+ for line in f:
445
+ line = line.strip()
446
+ if not line:
447
+ continue
448
+ try:
449
+ item = json.loads(line)
450
+ gid = item.get("gid", 0)
451
+ if gid >= 1 and gid not in saved_gids_from_file:
452
+ # 只保留验证通过的告警(has_risk: true 且有 verification_notes)
453
+ if item.get("has_risk") is True and "verification_notes" in item:
454
+ all_issues.append(item)
455
+ saved_gids_from_file.add(gid)
456
+ except Exception:
457
+ pass
458
+
459
+ if all_issues:
460
+ try:
461
+ typer.secho(f"[jarvis-sec] 从 agent_issues.jsonl 加载了 {len(all_issues)} 个已保存的告警", fg=typer.colors.BLUE)
462
+ except Exception:
463
+ pass
464
+ else:
465
+ try:
466
+ typer.secho("[jarvis-sec] agent_issues.jsonl 不存在,当前运行未发现任何问题", fg=typer.colors.BLUE)
467
+ except Exception:
468
+ pass
469
+ except Exception as e:
470
+ # 加载失败不影响主流程
471
+ try:
472
+ typer.secho(f"[jarvis-sec] 警告:从 agent_issues.jsonl 加载告警失败: {e}", fg=typer.colors.YELLOW)
473
+ except Exception:
474
+ pass
475
+ return all_issues
476
+
477
+
478
+ def load_processed_gids_from_agent_issues(sec_dir: Path) -> set:
479
+ """从 agent_issues.jsonl 读取已处理的 gid"""
480
+ processed_gids = set()
481
+ try:
482
+ _agent_issues_path = sec_dir / "agent_issues.jsonl"
483
+ if _agent_issues_path.exists():
484
+ with _agent_issues_path.open("r", encoding="utf-8", errors="ignore") as f:
485
+ for line in f:
486
+ line = line.strip()
487
+ if not line:
488
+ continue
489
+ try:
490
+ issue_obj = json.loads(line)
491
+ _gid = int(issue_obj.get("gid", 0))
492
+ if _gid >= 1:
493
+ processed_gids.add(_gid)
494
+ except Exception:
495
+ pass
496
+ except Exception:
497
+ pass
498
+ return processed_gids
499
+