jarvis-ai-assistant 0.3.19__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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +33 -5
- jarvis/jarvis_agent/config_editor.py +5 -1
- jarvis/jarvis_agent/edit_file_handler.py +15 -9
- jarvis/jarvis_agent/jarvis.py +99 -3
- jarvis/jarvis_agent/memory_manager.py +3 -3
- jarvis/jarvis_agent/share_manager.py +3 -1
- jarvis/jarvis_agent/shell_input_handler.py +17 -2
- jarvis/jarvis_agent/task_analyzer.py +0 -1
- jarvis/jarvis_agent/task_manager.py +15 -5
- jarvis/jarvis_agent/tool_executor.py +2 -2
- jarvis/jarvis_code_agent/code_agent.py +39 -16
- jarvis/jarvis_git_utils/git_commiter.py +3 -6
- jarvis/jarvis_mcp/sse_mcp_client.py +9 -3
- jarvis/jarvis_mcp/streamable_mcp_client.py +15 -5
- jarvis/jarvis_memory_organizer/memory_organizer.py +1 -1
- jarvis/jarvis_methodology/main.py +4 -4
- jarvis/jarvis_multi_agent/__init__.py +3 -3
- jarvis/jarvis_platform/ai8.py +0 -4
- jarvis/jarvis_platform/base.py +12 -7
- jarvis/jarvis_platform/kimi.py +18 -6
- jarvis/jarvis_platform/tongyi.py +18 -5
- jarvis/jarvis_platform/yuanbao.py +10 -3
- jarvis/jarvis_platform_manager/main.py +21 -7
- jarvis/jarvis_platform_manager/service.py +4 -3
- jarvis/jarvis_rag/cli.py +61 -22
- jarvis/jarvis_rag/embedding_manager.py +10 -3
- jarvis/jarvis_rag/llm_interface.py +4 -1
- jarvis/jarvis_rag/query_rewriter.py +3 -1
- jarvis/jarvis_rag/rag_pipeline.py +11 -3
- jarvis/jarvis_rag/retriever.py +151 -2
- jarvis/jarvis_smart_shell/main.py +60 -19
- jarvis/jarvis_stats/cli.py +12 -9
- jarvis/jarvis_stats/stats.py +17 -11
- jarvis/jarvis_stats/storage.py +23 -6
- jarvis/jarvis_tools/cli/main.py +63 -29
- jarvis/jarvis_tools/edit_file.py +3 -4
- jarvis/jarvis_tools/file_analyzer.py +0 -1
- jarvis/jarvis_tools/generate_new_tool.py +3 -3
- jarvis/jarvis_tools/read_code.py +0 -1
- jarvis/jarvis_tools/read_webpage.py +14 -4
- jarvis/jarvis_tools/registry.py +0 -3
- jarvis/jarvis_tools/retrieve_memory.py +0 -1
- jarvis/jarvis_tools/save_memory.py +0 -1
- jarvis/jarvis_tools/search_web.py +0 -2
- jarvis/jarvis_tools/sub_agent.py +197 -0
- jarvis/jarvis_tools/sub_code_agent.py +194 -0
- jarvis/jarvis_tools/virtual_tty.py +21 -13
- jarvis/jarvis_utils/clipboard.py +1 -1
- jarvis/jarvis_utils/config.py +35 -5
- jarvis/jarvis_utils/input.py +528 -41
- jarvis/jarvis_utils/methodology.py +3 -1
- jarvis/jarvis_utils/output.py +218 -129
- jarvis/jarvis_utils/utils.py +480 -170
- {jarvis_ai_assistant-0.3.19.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/METADATA +10 -2
- {jarvis_ai_assistant-0.3.19.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/RECORD +60 -58
- {jarvis_ai_assistant-0.3.19.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.19.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.3.19.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.19.dist-info → jarvis_ai_assistant-0.3.21.dist-info}/top_level.txt +0 -0
jarvis/jarvis_rag/retriever.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import os
|
2
2
|
import pickle
|
3
|
-
|
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(
|
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(
|
@@ -24,7 +24,7 @@ Example:
|
|
24
24
|
|
25
25
|
def execute_command(command: str, should_run: bool) -> None:
|
26
26
|
"""Print command without execution"""
|
27
|
-
print(command)
|
27
|
+
PrettyOutput.print(command, OutputType.CODE, lang="bash")
|
28
28
|
if should_run:
|
29
29
|
os.system(command)
|
30
30
|
|
@@ -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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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]:
|
jarvis/jarvis_stats/cli.py
CHANGED
@@ -16,6 +16,7 @@ from pathlib import Path
|
|
16
16
|
from .stats import StatsManager
|
17
17
|
from jarvis.jarvis_utils.utils import init_env
|
18
18
|
from jarvis.jarvis_utils.config import get_data_dir
|
19
|
+
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
19
20
|
|
20
21
|
app = typer.Typer(help="Jarvis 统计模块命令行工具")
|
21
22
|
console = Console()
|
@@ -207,7 +208,7 @@ def list():
|
|
207
208
|
# 获取数据点数和标签
|
208
209
|
records = stats._get_storage().get_metrics(metric, start_time, end_time)
|
209
210
|
count = len(records)
|
210
|
-
|
211
|
+
|
211
212
|
# 收集所有唯一的标签
|
212
213
|
all_tags = {}
|
213
214
|
for record in records:
|
@@ -216,7 +217,7 @@ def list():
|
|
216
217
|
if k not in all_tags:
|
217
218
|
all_tags[k] = set()
|
218
219
|
all_tags[k].add(v)
|
219
|
-
|
220
|
+
|
220
221
|
# 格式化标签显示
|
221
222
|
tag_str = ""
|
222
223
|
if all_tags:
|
@@ -291,7 +292,9 @@ def export(
|
|
291
292
|
|
292
293
|
if output == "json":
|
293
294
|
# JSON格式输出
|
294
|
-
print(
|
295
|
+
PrettyOutput.print(
|
296
|
+
json.dumps(data, indent=2, ensure_ascii=False), OutputType.CODE, lang="json"
|
297
|
+
)
|
295
298
|
else:
|
296
299
|
# CSV格式输出
|
297
300
|
records = data.get("records", [])
|
@@ -315,30 +318,30 @@ def remove(
|
|
315
318
|
# 显示指标信息供用户确认
|
316
319
|
stats = StatsManager(_get_stats_dir())
|
317
320
|
metrics = stats.list_metrics()
|
318
|
-
|
321
|
+
|
319
322
|
if metric not in metrics:
|
320
323
|
rprint(f"[red]错误:指标 '{metric}' 不存在[/red]")
|
321
324
|
return
|
322
|
-
|
325
|
+
|
323
326
|
# 获取指标的基本信息
|
324
327
|
info = stats._get_storage().get_metric_info(metric)
|
325
328
|
if info:
|
326
329
|
unit = info.get("unit", "-")
|
327
330
|
last_updated = info.get("last_updated", "-")
|
328
|
-
|
331
|
+
|
329
332
|
rprint(f"\n[yellow]准备删除指标:[/yellow]")
|
330
333
|
rprint(f" 名称: {metric}")
|
331
334
|
rprint(f" 单位: {unit}")
|
332
335
|
rprint(f" 最后更新: {last_updated}")
|
333
|
-
|
336
|
+
|
334
337
|
confirm = typer.confirm(f"\n确定要删除指标 '{metric}' 及其所有数据吗?")
|
335
338
|
if not confirm:
|
336
339
|
rprint("[yellow]已取消操作[/yellow]")
|
337
340
|
return
|
338
|
-
|
341
|
+
|
339
342
|
stats = StatsManager(_get_stats_dir())
|
340
343
|
success = stats.remove_metric(metric)
|
341
|
-
|
344
|
+
|
342
345
|
if success:
|
343
346
|
rprint(f"[green]✓[/green] 已成功删除指标: {metric}")
|
344
347
|
else:
|
jarvis/jarvis_stats/stats.py
CHANGED
@@ -9,6 +9,7 @@ from typing import Dict, List, Optional, Union, Any
|
|
9
9
|
|
10
10
|
from jarvis.jarvis_stats.storage import StatsStorage
|
11
11
|
from jarvis.jarvis_stats.visualizer import StatsVisualizer
|
12
|
+
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
12
13
|
|
13
14
|
|
14
15
|
class StatsManager:
|
@@ -115,7 +116,9 @@ class StatsManager:
|
|
115
116
|
info = storage.get_metric_info(metric_name)
|
116
117
|
if not info or not info.get("group"):
|
117
118
|
try:
|
118
|
-
grp = storage.resolve_metric_group(
|
119
|
+
grp = storage.resolve_metric_group(
|
120
|
+
metric_name
|
121
|
+
) # 触发一次分组解析与回填
|
119
122
|
if grp:
|
120
123
|
info = storage.get_metric_info(metric_name)
|
121
124
|
except Exception:
|
@@ -307,16 +310,15 @@ class StatsManager:
|
|
307
310
|
"""
|
308
311
|
storage = StatsManager._get_storage()
|
309
312
|
storage.delete_old_data(days_to_keep)
|
310
|
-
print(f"已清理 {days_to_keep} 天前的数据")
|
311
313
|
|
312
314
|
@staticmethod
|
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
|
-
print(
|
445
|
+
PrettyOutput.print(
|
446
|
+
f"没有找到指标 '{metric_name}' 的数据", OutputType.WARNING
|
447
|
+
)
|
444
448
|
return
|
445
449
|
|
446
450
|
# 获取指标信息
|
@@ -474,7 +478,7 @@ class StatsManager:
|
|
474
478
|
show_values=True,
|
475
479
|
)
|
476
480
|
|
477
|
-
print(chart)
|
481
|
+
PrettyOutput.print(chart, OutputType.CODE, lang="text")
|
478
482
|
|
479
483
|
# 显示时间范围
|
480
484
|
from rich.panel import Panel
|
@@ -550,7 +554,9 @@ class StatsManager:
|
|
550
554
|
)
|
551
555
|
|
552
556
|
if not aggregated:
|
553
|
-
print(
|
557
|
+
PrettyOutput.print(
|
558
|
+
f"没有找到指标 '{metric_name}' 的数据", OutputType.WARNING
|
559
|
+
)
|
554
560
|
return
|
555
561
|
|
556
562
|
# 获取指标信息
|
@@ -560,7 +566,7 @@ class StatsManager:
|
|
560
566
|
# 显示汇总
|
561
567
|
summary = visualizer.show_summary(aggregated, metric_name, unit, tags)
|
562
568
|
if summary: # 如果返回了内容才打印(兼容性)
|
563
|
-
print(summary)
|
569
|
+
PrettyOutput.print(summary, OutputType.INFO)
|
564
570
|
|
565
571
|
# 显示时间范围
|
566
572
|
from rich.panel import Panel
|
jarvis/jarvis_stats/storage.py
CHANGED
@@ -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 = {
|
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(
|
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
|
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 =
|
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 = {
|
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(
|
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(
|