jarvis-ai-assistant 0.3.20__py3-none-any.whl → 0.3.21__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 (57) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +24 -3
  3. jarvis/jarvis_agent/config_editor.py +5 -1
  4. jarvis/jarvis_agent/edit_file_handler.py +15 -9
  5. jarvis/jarvis_agent/jarvis.py +99 -3
  6. jarvis/jarvis_agent/memory_manager.py +3 -3
  7. jarvis/jarvis_agent/share_manager.py +3 -1
  8. jarvis/jarvis_agent/task_analyzer.py +0 -1
  9. jarvis/jarvis_agent/task_manager.py +15 -5
  10. jarvis/jarvis_agent/tool_executor.py +2 -2
  11. jarvis/jarvis_code_agent/code_agent.py +39 -16
  12. jarvis/jarvis_git_utils/git_commiter.py +3 -6
  13. jarvis/jarvis_mcp/sse_mcp_client.py +9 -3
  14. jarvis/jarvis_mcp/streamable_mcp_client.py +15 -5
  15. jarvis/jarvis_memory_organizer/memory_organizer.py +1 -1
  16. jarvis/jarvis_methodology/main.py +4 -4
  17. jarvis/jarvis_multi_agent/__init__.py +3 -3
  18. jarvis/jarvis_platform/base.py +10 -5
  19. jarvis/jarvis_platform/kimi.py +18 -6
  20. jarvis/jarvis_platform/tongyi.py +18 -5
  21. jarvis/jarvis_platform/yuanbao.py +10 -3
  22. jarvis/jarvis_platform_manager/main.py +21 -7
  23. jarvis/jarvis_platform_manager/service.py +4 -3
  24. jarvis/jarvis_rag/cli.py +61 -22
  25. jarvis/jarvis_rag/embedding_manager.py +10 -3
  26. jarvis/jarvis_rag/llm_interface.py +4 -1
  27. jarvis/jarvis_rag/query_rewriter.py +3 -1
  28. jarvis/jarvis_rag/rag_pipeline.py +11 -3
  29. jarvis/jarvis_rag/retriever.py +151 -2
  30. jarvis/jarvis_smart_shell/main.py +59 -18
  31. jarvis/jarvis_stats/cli.py +11 -9
  32. jarvis/jarvis_stats/stats.py +14 -8
  33. jarvis/jarvis_stats/storage.py +23 -6
  34. jarvis/jarvis_tools/cli/main.py +63 -29
  35. jarvis/jarvis_tools/edit_file.py +3 -4
  36. jarvis/jarvis_tools/file_analyzer.py +0 -1
  37. jarvis/jarvis_tools/generate_new_tool.py +3 -3
  38. jarvis/jarvis_tools/read_code.py +0 -1
  39. jarvis/jarvis_tools/read_webpage.py +14 -4
  40. jarvis/jarvis_tools/registry.py +0 -3
  41. jarvis/jarvis_tools/retrieve_memory.py +0 -1
  42. jarvis/jarvis_tools/save_memory.py +0 -1
  43. jarvis/jarvis_tools/search_web.py +0 -2
  44. jarvis/jarvis_tools/sub_agent.py +197 -0
  45. jarvis/jarvis_tools/sub_code_agent.py +194 -0
  46. jarvis/jarvis_tools/virtual_tty.py +21 -13
  47. jarvis/jarvis_utils/config.py +35 -5
  48. jarvis/jarvis_utils/input.py +297 -56
  49. jarvis/jarvis_utils/methodology.py +3 -1
  50. jarvis/jarvis_utils/output.py +5 -2
  51. jarvis/jarvis_utils/utils.py +480 -170
  52. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/METADATA +10 -2
  53. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/RECORD +57 -55
  54. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/WHEEL +0 -0
  55. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/entry_points.txt +0 -0
  56. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/licenses/LICENSE +0 -0
  57. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,8 @@
1
1
  import os
2
2
  import pickle
3
- from typing import Any, Dict, List, cast
3
+ import json
4
+ import hashlib
5
+ from typing import Any, Dict, List, Optional, cast
4
6
 
5
7
  import chromadb
6
8
  from langchain.docstore.document import Document
@@ -48,6 +50,10 @@ class ChromaRetriever:
48
50
  # BM25索引设置
49
51
  self.bm25_index_path = os.path.join(self.db_path, f"{collection_name}_bm25.pkl")
50
52
  self._load_or_initialize_bm25()
53
+ # 清单文件用于检测源文件的变更/删除
54
+ self.manifest_path = os.path.join(
55
+ self.db_path, f"{collection_name}_manifest.json"
56
+ )
51
57
 
52
58
  def _load_or_initialize_bm25(self):
53
59
  """从磁盘加载BM25索引或初始化一个新索引。"""
@@ -59,7 +65,9 @@ class ChromaRetriever:
59
65
  self.bm25_index = BM25Okapi(self.bm25_corpus)
60
66
  PrettyOutput.print("BM25 索引加载成功。", OutputType.SUCCESS)
61
67
  else:
62
- PrettyOutput.print("未找到 BM25 索引,将初始化一个新的。", OutputType.WARNING)
68
+ PrettyOutput.print(
69
+ "未找到 BM25 索引,将初始化一个新的。", OutputType.WARNING
70
+ )
63
71
  self.bm25_corpus = []
64
72
  self.bm25_index = None
65
73
 
@@ -71,6 +79,138 @@ class ChromaRetriever:
71
79
  pickle.dump({"corpus": self.bm25_corpus, "index": self.bm25_index}, f)
72
80
  PrettyOutput.print("BM25 索引保存成功。", OutputType.SUCCESS)
73
81
 
82
+ def _load_manifest(self) -> Dict[str, Dict[str, Any]]:
83
+ """加载已索引文件清单,用于变更检测。"""
84
+ if os.path.exists(self.manifest_path):
85
+ try:
86
+ with open(self.manifest_path, "r", encoding="utf-8") as f:
87
+ data = json.load(f)
88
+ if isinstance(data, dict):
89
+ return data # type: ignore[return-value]
90
+ except Exception:
91
+ pass
92
+ return {}
93
+
94
+ def _save_manifest(self, manifest: Dict[str, Dict[str, Any]]) -> None:
95
+ """保存已索引文件清单。"""
96
+ try:
97
+ with open(self.manifest_path, "w", encoding="utf-8") as f:
98
+ json.dump(manifest, f, ensure_ascii=False, indent=2)
99
+ except Exception as e:
100
+ PrettyOutput.print(f"保存索引清单失败: {e}", OutputType.WARNING)
101
+
102
+ def _compute_md5(
103
+ self, file_path: str, chunk_size: int = 1024 * 1024
104
+ ) -> Optional[str]:
105
+ """流式计算文件的MD5,避免占用过多内存。失败时返回None。"""
106
+ try:
107
+ md5 = hashlib.md5()
108
+ with open(file_path, "rb") as f:
109
+ while True:
110
+ data = f.read(chunk_size)
111
+ if not data:
112
+ break
113
+ md5.update(data)
114
+ return md5.hexdigest()
115
+ except Exception:
116
+ return None
117
+
118
+ def _update_manifest_with_sources(self, sources: List[str]) -> None:
119
+ """根据本次新增文档的来源,更新索引清单(记录mtime与size)。"""
120
+ manifest = self._load_manifest()
121
+ updated = 0
122
+ for src in set(sources):
123
+ try:
124
+ if isinstance(src, str) and os.path.exists(src):
125
+ st = os.stat(src)
126
+ entry: Dict[str, Any] = {
127
+ "mtime": float(st.st_mtime),
128
+ "size": int(st.st_size),
129
+ }
130
+ md5sum = self._compute_md5(src)
131
+ if md5sum:
132
+ entry["md5"] = md5sum
133
+ manifest[src] = entry # type: ignore[dict-item]
134
+ updated += 1
135
+ except Exception:
136
+ continue
137
+ if updated > 0:
138
+ self._save_manifest(manifest)
139
+ PrettyOutput.print(
140
+ f"已更新索引清单,记录 {updated} 个源文件状态。", OutputType.INFO
141
+ )
142
+
143
+ def _detect_changed_or_deleted(self) -> Dict[str, List[str]]:
144
+ """检测已记录的源文件是否发生变化或被删除。"""
145
+ manifest = self._load_manifest()
146
+ changed: List[str] = []
147
+ deleted: List[str] = []
148
+ for src, info in manifest.items():
149
+ try:
150
+ if not os.path.exists(src):
151
+ deleted.append(src)
152
+ continue
153
+ st = os.stat(src)
154
+ size_changed = int(info.get("size", -1)) != int(st.st_size)
155
+ if size_changed:
156
+ changed.append(src)
157
+ continue
158
+ md5_old = info.get("md5")
159
+ if md5_old:
160
+ # 仅在mtime变化时计算md5以降低开销
161
+ mtime_changed = (
162
+ abs(float(info.get("mtime", 0.0)) - float(st.st_mtime)) >= 1e-6
163
+ )
164
+ if mtime_changed:
165
+ md5_new = self._compute_md5(src)
166
+ if not md5_new or md5_new != md5_old:
167
+ changed.append(src)
168
+ else:
169
+ # 没有记录md5,回退使用mtime判断
170
+ mtime_changed = (
171
+ abs(float(info.get("mtime", 0.0)) - float(st.st_mtime)) >= 1e-6
172
+ )
173
+ if mtime_changed:
174
+ changed.append(src)
175
+ except Exception:
176
+ # 无法读取文件状态,视为发生变化
177
+ changed.append(src)
178
+ return {"changed": changed, "deleted": deleted}
179
+
180
+ def _warn_if_sources_changed(self) -> None:
181
+ """如发现已索引文件变化或删除,给出提醒。"""
182
+ result = self._detect_changed_or_deleted()
183
+ changed = result["changed"]
184
+ deleted = result["deleted"]
185
+ if not changed and not deleted:
186
+ return
187
+ if changed:
188
+ PrettyOutput.print(
189
+ f"检测到 {len(changed)} 个已索引文件发生变化,建议重新索引以保证检索准确性。",
190
+ OutputType.WARNING,
191
+ )
192
+ for p in changed[:5]:
193
+ PrettyOutput.print(f" 变更: {p}", OutputType.WARNING)
194
+ if len(changed) > 5:
195
+ PrettyOutput.print(
196
+ f" ... 以及另外 {len(changed) - 5} 个文件", OutputType.WARNING
197
+ )
198
+ if deleted:
199
+ PrettyOutput.print(
200
+ f"检测到 {len(deleted)} 个已索引文件已被删除,建议清理并重新索引。",
201
+ OutputType.WARNING,
202
+ )
203
+ for p in deleted[:5]:
204
+ PrettyOutput.print(f" 删除: {p}", OutputType.WARNING)
205
+ if len(deleted) > 5:
206
+ PrettyOutput.print(
207
+ f" ... 以及另外 {len(deleted) - 5} 个文件", OutputType.WARNING
208
+ )
209
+ PrettyOutput.print(
210
+ "提示:请使用 'jarvis-rag add <路径>' 重新索引相关文件,以更新向量库与BM25索引。",
211
+ OutputType.INFO,
212
+ )
213
+
74
214
  def add_documents(
75
215
  self, documents: List[Document], chunk_size=1000, chunk_overlap=100
76
216
  ):
@@ -114,6 +254,13 @@ class ChromaRetriever:
114
254
  self.bm25_corpus.extend(tokenized_chunks)
115
255
  self.bm25_index = BM25Okapi(self.bm25_corpus)
116
256
  self._save_bm25_index()
257
+ # 更新索引清单(用于检测源文件变更/删除)
258
+ source_list = [
259
+ md.get("source")
260
+ for md in metadatas
261
+ if md and isinstance(md.get("source"), str)
262
+ ]
263
+ self._update_manifest_with_sources(cast(List[str], source_list))
117
264
 
118
265
  def retrieve(
119
266
  self, query: str, n_results: int = 5, use_bm25: bool = True
@@ -122,6 +269,8 @@ class ChromaRetriever:
122
269
  使用向量搜索和BM25执行混合检索,然后使用倒数排序融合(RRF)
123
270
  对结果进行融合。
124
271
  """
272
+ # 在检索前检查源文件变更/删除并提醒
273
+ self._warn_if_sources_changed()
125
274
  # 1. 向量搜索 (ChromaDB)
126
275
  query_embedding = self.embedding_manager.embed_query(query)
127
276
  vector_results = self.collection.query(
@@ -125,7 +125,9 @@ def install_jss_completion(
125
125
  ) -> None:
126
126
  """为指定的shell安装'命令未找到'处理器,实现自然语言命令建议"""
127
127
  if shell not in ("fish", "bash", "zsh"):
128
- PrettyOutput.print(f"错误: 不支持的shell类型: {shell}, 仅支持fish, bash, zsh", OutputType.ERROR)
128
+ PrettyOutput.print(
129
+ f"错误: 不支持的shell类型: {shell}, 仅支持fish, bash, zsh", OutputType.ERROR
130
+ )
129
131
  raise typer.Exit(code=1)
130
132
 
131
133
  if shell == "fish":
@@ -142,7 +144,10 @@ def install_jss_completion(
142
144
  content = f.read()
143
145
 
144
146
  if start_marker in content:
145
- PrettyOutput.print("JSS fish completion 已安装,请执行: source ~/.config/fish/config.fish", OutputType.SUCCESS)
147
+ PrettyOutput.print(
148
+ "JSS fish completion 已安装,请执行: source ~/.config/fish/config.fish",
149
+ OutputType.SUCCESS,
150
+ )
146
151
  return
147
152
 
148
153
  with open(config_file, "a") as f:
@@ -162,7 +167,10 @@ end
162
167
  {end_marker}
163
168
  """
164
169
  )
165
- PrettyOutput.print("JSS fish completion 已安装,请执行: source ~/.config/fish/config.fish", OutputType.SUCCESS)
170
+ PrettyOutput.print(
171
+ "JSS fish completion 已安装,请执行: source ~/.config/fish/config.fish",
172
+ OutputType.SUCCESS,
173
+ )
166
174
  elif shell == "bash":
167
175
  config_file = _get_bash_config_file()
168
176
  start_marker, end_marker = _get_bash_markers()
@@ -177,7 +185,10 @@ end
177
185
  content = f.read()
178
186
 
179
187
  if start_marker in content:
180
- PrettyOutput.print("JSS bash completion 已安装,请执行: source ~/.bashrc", OutputType.SUCCESS)
188
+ PrettyOutput.print(
189
+ "JSS bash completion 已安装,请执行: source ~/.bashrc",
190
+ OutputType.SUCCESS,
191
+ )
181
192
  return
182
193
  else:
183
194
  with open(config_file, "a") as f:
@@ -220,7 +231,10 @@ command_not_found_handle() {{
220
231
  {end_marker}
221
232
  """
222
233
  )
223
- PrettyOutput.print("JSS bash completion 已安装,请执行: source ~/.bashrc", OutputType.SUCCESS)
234
+ PrettyOutput.print(
235
+ "JSS bash completion 已安装,请执行: source ~/.bashrc",
236
+ OutputType.SUCCESS,
237
+ )
224
238
  elif shell == "zsh":
225
239
  config_file = _get_zsh_config_file()
226
240
  start_marker, end_marker = _get_zsh_markers()
@@ -235,7 +249,9 @@ command_not_found_handle() {{
235
249
  content = f.read()
236
250
 
237
251
  if start_marker in content:
238
- PrettyOutput.print("JSS zsh completion 已安装,请执行: source ~/.zshrc", OutputType.SUCCESS)
252
+ PrettyOutput.print(
253
+ "JSS zsh completion 已安装,请执行: source ~/.zshrc", OutputType.SUCCESS
254
+ )
239
255
  return
240
256
 
241
257
  with open(config_file, "a") as f:
@@ -282,7 +298,9 @@ command_not_found_handler() {{
282
298
  {end_marker}
283
299
  """
284
300
  )
285
- PrettyOutput.print("JSS zsh completion 已安装,请执行: source ~/.zshrc", OutputType.SUCCESS)
301
+ PrettyOutput.print(
302
+ "JSS zsh completion 已安装,请执行: source ~/.zshrc", OutputType.SUCCESS
303
+ )
286
304
  return
287
305
 
288
306
  with open(config_file, "a") as f:
@@ -325,7 +343,9 @@ command_not_found_handle() {{
325
343
  {end_marker}
326
344
  """
327
345
  )
328
- PrettyOutput.print("JSS bash completion 已安装,请执行: source ~/.bashrc", OutputType.SUCCESS)
346
+ PrettyOutput.print(
347
+ "JSS bash completion 已安装,请执行: source ~/.bashrc", OutputType.SUCCESS
348
+ )
329
349
 
330
350
 
331
351
  @app.command("uninstall")
@@ -334,7 +354,9 @@ def uninstall_jss_completion(
334
354
  ) -> None:
335
355
  """卸载JSS shell'命令未找到'处理器"""
336
356
  if shell not in ("fish", "bash", "zsh"):
337
- PrettyOutput.print(f"错误: 不支持的shell类型: {shell}, 仅支持fish, bash, zsh", OutputType.ERROR)
357
+ PrettyOutput.print(
358
+ f"错误: 不支持的shell类型: {shell}, 仅支持fish, bash, zsh", OutputType.ERROR
359
+ )
338
360
  raise typer.Exit(code=1)
339
361
 
340
362
  if shell == "fish":
@@ -342,14 +364,18 @@ def uninstall_jss_completion(
342
364
  start_marker, end_marker = _get_markers()
343
365
 
344
366
  if not os.path.exists(config_file):
345
- PrettyOutput.print("未找到 JSS fish completion 配置,无需卸载", OutputType.INFO)
367
+ PrettyOutput.print(
368
+ "未找到 JSS fish completion 配置,无需卸载", OutputType.INFO
369
+ )
346
370
  return
347
371
 
348
372
  with open(config_file, "r") as f:
349
373
  content = f.read()
350
374
 
351
375
  if start_marker not in content:
352
- PrettyOutput.print("未找到 JSS fish completion 配置,无需卸载", OutputType.INFO)
376
+ PrettyOutput.print(
377
+ "未找到 JSS fish completion 配置,无需卸载", OutputType.INFO
378
+ )
353
379
  return
354
380
 
355
381
  new_content = content.split(start_marker)[0] + content.split(end_marker)[-1]
@@ -357,20 +383,27 @@ def uninstall_jss_completion(
357
383
  with open(config_file, "w") as f:
358
384
  f.write(new_content)
359
385
 
360
- PrettyOutput.print("JSS fish completion 已卸载,请执行: source ~/.config/fish/config.fish", OutputType.SUCCESS)
386
+ PrettyOutput.print(
387
+ "JSS fish completion 已卸载,请执行: source ~/.config/fish/config.fish",
388
+ OutputType.SUCCESS,
389
+ )
361
390
  elif shell == "bash":
362
391
  config_file = _get_bash_config_file()
363
392
  start_marker, end_marker = _get_bash_markers()
364
393
 
365
394
  if not os.path.exists(config_file):
366
- PrettyOutput.print("未找到 JSS bash completion 配置,无需卸载", OutputType.INFO)
395
+ PrettyOutput.print(
396
+ "未找到 JSS bash completion 配置,无需卸载", OutputType.INFO
397
+ )
367
398
  return
368
399
 
369
400
  with open(config_file, "r") as f:
370
401
  content = f.read()
371
402
 
372
403
  if start_marker not in content:
373
- PrettyOutput.print("未找到 JSS bash completion 配置,无需卸载", OutputType.INFO)
404
+ PrettyOutput.print(
405
+ "未找到 JSS bash completion 配置,无需卸载", OutputType.INFO
406
+ )
374
407
  return
375
408
 
376
409
  new_content = content.split(start_marker)[0] + content.split(end_marker)[-1]
@@ -378,20 +411,26 @@ def uninstall_jss_completion(
378
411
  with open(config_file, "w") as f:
379
412
  f.write(new_content)
380
413
 
381
- PrettyOutput.print("JSS bash completion 已卸载,请执行: source ~/.bashrc", OutputType.SUCCESS)
414
+ PrettyOutput.print(
415
+ "JSS bash completion 已卸载,请执行: source ~/.bashrc", OutputType.SUCCESS
416
+ )
382
417
  elif shell == "zsh":
383
418
  config_file = _get_zsh_config_file()
384
419
  start_marker, end_marker = _get_zsh_markers()
385
420
 
386
421
  if not os.path.exists(config_file):
387
- PrettyOutput.print("未找到 JSS zsh completion 配置,无需卸载", OutputType.INFO)
422
+ PrettyOutput.print(
423
+ "未找到 JSS zsh completion 配置,无需卸载", OutputType.INFO
424
+ )
388
425
  return
389
426
 
390
427
  with open(config_file, "r") as f:
391
428
  content = f.read()
392
429
 
393
430
  if start_marker not in content:
394
- PrettyOutput.print("未找到 JSS zsh completion 配置,无需卸载", OutputType.INFO)
431
+ PrettyOutput.print(
432
+ "未找到 JSS zsh completion 配置,无需卸载", OutputType.INFO
433
+ )
395
434
  return
396
435
 
397
436
  new_content = content.split(start_marker)[0] + content.split(end_marker)[-1]
@@ -399,7 +438,9 @@ def uninstall_jss_completion(
399
438
  with open(config_file, "w") as f:
400
439
  f.write(new_content)
401
440
 
402
- PrettyOutput.print("JSS zsh completion 已卸载,请执行: source ~/.zshrc", OutputType.SUCCESS)
441
+ PrettyOutput.print(
442
+ "JSS zsh completion 已卸载,请执行: source ~/.zshrc", OutputType.SUCCESS
443
+ )
403
444
 
404
445
 
405
446
  def process_request(request: str) -> Optional[str]:
@@ -208,7 +208,7 @@ def list():
208
208
  # 获取数据点数和标签
209
209
  records = stats._get_storage().get_metrics(metric, start_time, end_time)
210
210
  count = len(records)
211
-
211
+
212
212
  # 收集所有唯一的标签
213
213
  all_tags = {}
214
214
  for record in records:
@@ -217,7 +217,7 @@ def list():
217
217
  if k not in all_tags:
218
218
  all_tags[k] = set()
219
219
  all_tags[k].add(v)
220
-
220
+
221
221
  # 格式化标签显示
222
222
  tag_str = ""
223
223
  if all_tags:
@@ -292,7 +292,9 @@ def export(
292
292
 
293
293
  if output == "json":
294
294
  # JSON格式输出
295
- PrettyOutput.print(json.dumps(data, indent=2, ensure_ascii=False), OutputType.CODE, lang="json")
295
+ PrettyOutput.print(
296
+ json.dumps(data, indent=2, ensure_ascii=False), OutputType.CODE, lang="json"
297
+ )
296
298
  else:
297
299
  # CSV格式输出
298
300
  records = data.get("records", [])
@@ -316,30 +318,30 @@ def remove(
316
318
  # 显示指标信息供用户确认
317
319
  stats = StatsManager(_get_stats_dir())
318
320
  metrics = stats.list_metrics()
319
-
321
+
320
322
  if metric not in metrics:
321
323
  rprint(f"[red]错误:指标 '{metric}' 不存在[/red]")
322
324
  return
323
-
325
+
324
326
  # 获取指标的基本信息
325
327
  info = stats._get_storage().get_metric_info(metric)
326
328
  if info:
327
329
  unit = info.get("unit", "-")
328
330
  last_updated = info.get("last_updated", "-")
329
-
331
+
330
332
  rprint(f"\n[yellow]准备删除指标:[/yellow]")
331
333
  rprint(f" 名称: {metric}")
332
334
  rprint(f" 单位: {unit}")
333
335
  rprint(f" 最后更新: {last_updated}")
334
-
336
+
335
337
  confirm = typer.confirm(f"\n确定要删除指标 '{metric}' 及其所有数据吗?")
336
338
  if not confirm:
337
339
  rprint("[yellow]已取消操作[/yellow]")
338
340
  return
339
-
341
+
340
342
  stats = StatsManager(_get_stats_dir())
341
343
  success = stats.remove_metric(metric)
342
-
344
+
343
345
  if success:
344
346
  rprint(f"[green]✓[/green] 已成功删除指标: {metric}")
345
347
  else:
@@ -116,7 +116,9 @@ class StatsManager:
116
116
  info = storage.get_metric_info(metric_name)
117
117
  if not info or not info.get("group"):
118
118
  try:
119
- grp = storage.resolve_metric_group(metric_name) # 触发一次分组解析与回填
119
+ grp = storage.resolve_metric_group(
120
+ metric_name
121
+ ) # 触发一次分组解析与回填
120
122
  if grp:
121
123
  info = storage.get_metric_info(metric_name)
122
124
  except Exception:
@@ -313,10 +315,10 @@ class StatsManager:
313
315
  def remove_metric(metric_name: str) -> bool:
314
316
  """
315
317
  删除指定的指标及其所有数据
316
-
318
+
317
319
  Args:
318
320
  metric_name: 要删除的指标名称
319
-
321
+
320
322
  Returns:
321
323
  True 如果成功删除,False 如果指标不存在
322
324
  """
@@ -359,15 +361,15 @@ class StatsManager:
359
361
  for metric in metrics:
360
362
  # 获取该指标的记录
361
363
  records = storage.get_metrics(metric, start_time, end_time, tags)
362
-
364
+
363
365
  # 如果指定了标签过滤,但没有匹配的记录,跳过该指标
364
366
  if tags and len(records) == 0:
365
367
  continue
366
-
368
+
367
369
  info = storage.get_metric_info(metric)
368
370
  unit = "-"
369
371
  last_updated = "-"
370
-
372
+
371
373
  if info:
372
374
  unit = info.get("unit", "-")
373
375
  last_updated = info.get("last_updated", "-")
@@ -440,7 +442,9 @@ class StatsManager:
440
442
  )
441
443
 
442
444
  if not aggregated:
443
- PrettyOutput.print(f"没有找到指标 '{metric_name}' 的数据", OutputType.WARNING)
445
+ PrettyOutput.print(
446
+ f"没有找到指标 '{metric_name}' 的数据", OutputType.WARNING
447
+ )
444
448
  return
445
449
 
446
450
  # 获取指标信息
@@ -550,7 +554,9 @@ class StatsManager:
550
554
  )
551
555
 
552
556
  if not aggregated:
553
- PrettyOutput.print(f"没有找到指标 '{metric_name}' 的数据", OutputType.WARNING)
557
+ PrettyOutput.print(
558
+ f"没有找到指标 '{metric_name}' 的数据", OutputType.WARNING
559
+ )
554
560
  return
555
561
 
556
562
  # 获取指标信息
@@ -347,15 +347,24 @@ class StatsStorage:
347
347
  info = meta["metrics"].get(metric_name)
348
348
  now_iso = datetime.now().isoformat()
349
349
  if info is None:
350
- info = {"unit": None, "created_at": now_iso, "last_updated": now_iso}
350
+ info = {
351
+ "unit": None,
352
+ "created_at": now_iso,
353
+ "last_updated": now_iso,
354
+ }
351
355
  meta["metrics"][metric_name] = info
352
356
  if not info.get("group"):
353
357
  inferred_group = None
354
358
  if group_counts:
355
- inferred_group = max(group_counts.items(), key=lambda kv: kv[1])[0]
359
+ inferred_group = max(
360
+ group_counts.items(), key=lambda kv: kv[1]
361
+ )[0]
356
362
  # 名称启发式作为补充
357
363
  if not inferred_group:
358
- if metric_name.startswith("code_lines_") or "commit" in metric_name:
364
+ if (
365
+ metric_name.startswith("code_lines_")
366
+ or "commit" in metric_name
367
+ ):
359
368
  inferred_group = "code_agent"
360
369
  if inferred_group:
361
370
  info["group"] = inferred_group
@@ -381,7 +390,9 @@ class StatsStorage:
381
390
  try:
382
391
  # 优先从元数据读取
383
392
  meta = self._load_json(self.meta_file)
384
- metrics_meta = meta.get("metrics", {}) if isinstance(meta.get("metrics"), dict) else {}
393
+ metrics_meta = (
394
+ meta.get("metrics", {}) if isinstance(meta.get("metrics"), dict) else {}
395
+ )
385
396
  info = metrics_meta.get(metric_name)
386
397
  if info and isinstance(info, dict):
387
398
  grp = info.get("group")
@@ -420,7 +431,11 @@ class StatsStorage:
420
431
  metrics_meta = meta["metrics"]
421
432
  if info is None:
422
433
  now_iso = datetime.now().isoformat()
423
- info = {"unit": None, "created_at": now_iso, "last_updated": now_iso}
434
+ info = {
435
+ "unit": None,
436
+ "created_at": now_iso,
437
+ "last_updated": now_iso,
438
+ }
424
439
  metrics_meta[metric_name] = info
425
440
  info["group"] = inferred_group
426
441
  self._save_json(self.meta_file, meta)
@@ -461,7 +476,9 @@ class StatsStorage:
461
476
  pass
462
477
 
463
478
  # 合并三个来源的指标并返回排序后的列表
464
- all_metrics = metrics_from_meta.union(metrics_from_data).union(metrics_from_totals)
479
+ all_metrics = metrics_from_meta.union(metrics_from_data).union(
480
+ metrics_from_totals
481
+ )
465
482
  return sorted(list(all_metrics))
466
483
 
467
484
  def aggregate_metrics(