jarvis-ai-assistant 0.3.20__py3-none-any.whl → 0.3.22__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 +24 -3
- 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/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 +42 -18
- 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/base.py +10 -5
- 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 +47 -3
- jarvis/jarvis_rag/retriever.py +240 -2
- jarvis/jarvis_smart_shell/main.py +59 -18
- jarvis/jarvis_stats/cli.py +11 -9
- jarvis/jarvis_stats/stats.py +14 -8
- jarvis/jarvis_stats/storage.py +23 -6
- jarvis/jarvis_tools/cli/main.py +63 -29
- jarvis/jarvis_tools/edit_file.py +17 -90
- 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 +16 -9
- 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/config.py +35 -5
- jarvis/jarvis_utils/input.py +297 -56
- jarvis/jarvis_utils/methodology.py +3 -1
- jarvis/jarvis_utils/output.py +5 -2
- jarvis/jarvis_utils/utils.py +483 -170
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/METADATA +10 -2
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/RECORD +57 -55
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.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,227 @@ 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
|
+
|
214
|
+
def detect_index_changes(self) -> Dict[str, List[str]]:
|
215
|
+
"""
|
216
|
+
公共方法:检测索引变更(变更与删除)。
|
217
|
+
返回:
|
218
|
+
{'changed': List[str], 'deleted': List[str]}
|
219
|
+
"""
|
220
|
+
return self._detect_changed_or_deleted()
|
221
|
+
|
222
|
+
def _remove_sources_from_manifest(self, sources: List[str]) -> None:
|
223
|
+
"""从manifest中移除指定源文件记录并保存。"""
|
224
|
+
if not sources:
|
225
|
+
return
|
226
|
+
manifest = self._load_manifest()
|
227
|
+
removed = 0
|
228
|
+
for src in set(sources):
|
229
|
+
if src in manifest:
|
230
|
+
manifest.pop(src, None)
|
231
|
+
removed += 1
|
232
|
+
if removed > 0:
|
233
|
+
self._save_manifest(manifest)
|
234
|
+
PrettyOutput.print(f"已从索引清单中移除 {removed} 个已删除的源文件记录。", OutputType.INFO)
|
235
|
+
|
236
|
+
def update_index_for_changes(self, changed: List[str], deleted: List[str]) -> None:
|
237
|
+
"""
|
238
|
+
公共方法:根据变更与删除列表更新索引。
|
239
|
+
- 对 deleted: 从向量库按 metadata.source 删除
|
240
|
+
- 对 changed: 先删除旧条目,再从源文件重建并添加
|
241
|
+
- 最后:从集合重建BM25索引,更新manifest
|
242
|
+
"""
|
243
|
+
changed = list(dict.fromkeys([p for p in (changed or []) if isinstance(p, str)]))
|
244
|
+
deleted = list(dict.fromkeys([p for p in (deleted or []) if isinstance(p, str)]))
|
245
|
+
|
246
|
+
if not changed and not deleted:
|
247
|
+
return
|
248
|
+
|
249
|
+
# 先处理删除
|
250
|
+
for src in deleted:
|
251
|
+
try:
|
252
|
+
self.collection.delete(where={"source": src}) # type: ignore[arg-type]
|
253
|
+
except Exception as e:
|
254
|
+
PrettyOutput.print(f"删除源 '{src}' 时出错: {e}", OutputType.WARNING)
|
255
|
+
|
256
|
+
# 再处理变更(重建)
|
257
|
+
docs_to_add: List[Document] = []
|
258
|
+
for src in changed:
|
259
|
+
try:
|
260
|
+
# 删除旧条目
|
261
|
+
try:
|
262
|
+
self.collection.delete(where={"source": src}) # type: ignore[arg-type]
|
263
|
+
except Exception:
|
264
|
+
pass
|
265
|
+
# 读取源文件内容(作为单文档载入,由 add_documents 进行拆分与嵌入)
|
266
|
+
with open(src, "r", encoding="utf-8", errors="ignore") as f:
|
267
|
+
content = f.read()
|
268
|
+
docs_to_add.append(Document(page_content=content, metadata={"source": src}))
|
269
|
+
except Exception as e:
|
270
|
+
PrettyOutput.print(f"重建源 '{src}' 内容时出错: {e}", OutputType.WARNING)
|
271
|
+
|
272
|
+
if docs_to_add:
|
273
|
+
try:
|
274
|
+
# 复用现有拆分与嵌入逻辑
|
275
|
+
self.add_documents(docs_to_add)
|
276
|
+
except Exception as e:
|
277
|
+
PrettyOutput.print(f"添加变更文档到索引时出错: {e}", OutputType.ERROR)
|
278
|
+
|
279
|
+
# 重建BM25索引,确保删除后的语料被清理
|
280
|
+
try:
|
281
|
+
all_docs_in_collection = self.collection.get()
|
282
|
+
all_documents = all_docs_in_collection.get("documents") or []
|
283
|
+
self.bm25_corpus = [str(text).split() for text in all_documents if text]
|
284
|
+
self.bm25_index = BM25Okapi(self.bm25_corpus) if self.bm25_corpus else None
|
285
|
+
self._save_bm25_index()
|
286
|
+
except Exception as e:
|
287
|
+
PrettyOutput.print(f"重建BM25索引失败: {e}", OutputType.WARNING)
|
288
|
+
|
289
|
+
# 更新manifest:变更文件更新状态;删除文件从清单中移除
|
290
|
+
try:
|
291
|
+
if changed:
|
292
|
+
self._update_manifest_with_sources(changed)
|
293
|
+
if deleted:
|
294
|
+
self._remove_sources_from_manifest(deleted)
|
295
|
+
except Exception as e:
|
296
|
+
PrettyOutput.print(f"更新索引清单时出错: {e}", OutputType.WARNING)
|
297
|
+
|
298
|
+
PrettyOutput.print(
|
299
|
+
f"索引已更新:变更 {len(changed)} 个,删除 {len(deleted)} 个。",
|
300
|
+
OutputType.SUCCESS,
|
301
|
+
)
|
302
|
+
|
74
303
|
def add_documents(
|
75
304
|
self, documents: List[Document], chunk_size=1000, chunk_overlap=100
|
76
305
|
):
|
@@ -114,6 +343,13 @@ class ChromaRetriever:
|
|
114
343
|
self.bm25_corpus.extend(tokenized_chunks)
|
115
344
|
self.bm25_index = BM25Okapi(self.bm25_corpus)
|
116
345
|
self._save_bm25_index()
|
346
|
+
# 更新索引清单(用于检测源文件变更/删除)
|
347
|
+
source_list = [
|
348
|
+
md.get("source")
|
349
|
+
for md in metadatas
|
350
|
+
if md and isinstance(md.get("source"), str)
|
351
|
+
]
|
352
|
+
self._update_manifest_with_sources(cast(List[str], source_list))
|
117
353
|
|
118
354
|
def retrieve(
|
119
355
|
self, query: str, n_results: int = 5, use_bm25: bool = True
|
@@ -122,6 +358,8 @@ class ChromaRetriever:
|
|
122
358
|
使用向量搜索和BM25执行混合检索,然后使用倒数排序融合(RRF)
|
123
359
|
对结果进行融合。
|
124
360
|
"""
|
361
|
+
# 在检索前检查源文件变更/删除并提醒
|
362
|
+
self._warn_if_sources_changed()
|
125
363
|
# 1. 向量搜索 (ChromaDB)
|
126
364
|
query_embedding = self.embedding_manager.embed_query(query)
|
127
365
|
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(
|
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
@@ -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(
|
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:
|
jarvis/jarvis_stats/stats.py
CHANGED
@@ -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(
|
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(
|
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(
|
557
|
+
PrettyOutput.print(
|
558
|
+
f"没有找到指标 '{metric_name}' 的数据", OutputType.WARNING
|
559
|
+
)
|
554
560
|
return
|
555
561
|
|
556
562
|
# 获取指标信息
|
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(
|