jarvis-ai-assistant 0.3.18__py3-none-any.whl → 0.3.19__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 (49) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +21 -10
  3. jarvis/jarvis_agent/edit_file_handler.py +8 -13
  4. jarvis/jarvis_agent/memory_manager.py +4 -4
  5. jarvis/jarvis_agent/task_analyzer.py +4 -3
  6. jarvis/jarvis_agent/task_manager.py +6 -6
  7. jarvis/jarvis_agent/tool_executor.py +2 -2
  8. jarvis/jarvis_code_agent/code_agent.py +21 -29
  9. jarvis/jarvis_code_analysis/code_review.py +2 -4
  10. jarvis/jarvis_git_utils/git_commiter.py +17 -18
  11. jarvis/jarvis_methodology/main.py +12 -12
  12. jarvis/jarvis_platform/base.py +14 -13
  13. jarvis/jarvis_platform/kimi.py +13 -13
  14. jarvis/jarvis_platform/tongyi.py +17 -15
  15. jarvis/jarvis_platform/yuanbao.py +11 -11
  16. jarvis/jarvis_rag/cli.py +36 -32
  17. jarvis/jarvis_rag/embedding_manager.py +11 -6
  18. jarvis/jarvis_rag/llm_interface.py +6 -5
  19. jarvis/jarvis_rag/rag_pipeline.py +9 -8
  20. jarvis/jarvis_rag/reranker.py +3 -2
  21. jarvis/jarvis_rag/retriever.py +18 -8
  22. jarvis/jarvis_smart_shell/main.py +306 -46
  23. jarvis/jarvis_stats/stats.py +40 -0
  24. jarvis/jarvis_stats/storage.py +220 -9
  25. jarvis/jarvis_tools/clear_memory.py +0 -11
  26. jarvis/jarvis_tools/cli/main.py +18 -17
  27. jarvis/jarvis_tools/edit_file.py +4 -4
  28. jarvis/jarvis_tools/execute_script.py +5 -1
  29. jarvis/jarvis_tools/file_analyzer.py +6 -6
  30. jarvis/jarvis_tools/generate_new_tool.py +6 -17
  31. jarvis/jarvis_tools/read_code.py +3 -6
  32. jarvis/jarvis_tools/read_webpage.py +4 -4
  33. jarvis/jarvis_tools/registry.py +8 -28
  34. jarvis/jarvis_tools/retrieve_memory.py +5 -16
  35. jarvis/jarvis_tools/rewrite_file.py +0 -4
  36. jarvis/jarvis_tools/save_memory.py +2 -10
  37. jarvis/jarvis_tools/search_web.py +5 -8
  38. jarvis/jarvis_tools/virtual_tty.py +22 -40
  39. jarvis/jarvis_utils/clipboard.py +3 -3
  40. jarvis/jarvis_utils/input.py +67 -27
  41. jarvis/jarvis_utils/methodology.py +3 -3
  42. jarvis/jarvis_utils/output.py +1 -7
  43. jarvis/jarvis_utils/utils.py +35 -58
  44. {jarvis_ai_assistant-0.3.18.dist-info → jarvis_ai_assistant-0.3.19.dist-info}/METADATA +1 -1
  45. {jarvis_ai_assistant-0.3.18.dist-info → jarvis_ai_assistant-0.3.19.dist-info}/RECORD +49 -49
  46. {jarvis_ai_assistant-0.3.18.dist-info → jarvis_ai_assistant-0.3.19.dist-info}/WHEEL +0 -0
  47. {jarvis_ai_assistant-0.3.18.dist-info → jarvis_ai_assistant-0.3.19.dist-info}/entry_points.txt +0 -0
  48. {jarvis_ai_assistant-0.3.18.dist-info → jarvis_ai_assistant-0.3.19.dist-info}/licenses/LICENSE +0 -0
  49. {jarvis_ai_assistant-0.3.18.dist-info → jarvis_ai_assistant-0.3.19.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,7 @@ from .llm_interface import JarvisPlatform_LLM, LLMInterface, ToolAgent_LLM
8
8
  from .query_rewriter import QueryRewriter
9
9
  from .reranker import Reranker
10
10
  from .retriever import ChromaRetriever
11
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
11
12
  from jarvis.jarvis_utils.config import (
12
13
  get_rag_embedding_model,
13
14
  get_rag_rerank_model,
@@ -74,7 +75,7 @@ class JarvisRAGPipeline:
74
75
  self._reranker: Optional[Reranker] = None
75
76
  self._query_rewriter: Optional[QueryRewriter] = None
76
77
 
77
- print("JarvisRAGPipeline 初始化成功 (模型按需加载).")
78
+ PrettyOutput.print("JarvisRAGPipeline 初始化成功 (模型按需加载).", OutputType.SUCCESS)
78
79
 
79
80
  def _get_embedding_manager(self) -> EmbeddingManager:
80
81
  if self._embedding_manager is None:
@@ -193,7 +194,7 @@ class JarvisRAGPipeline:
193
194
  # 2. 为每个重写的查询检索初始候选文档
194
195
  all_candidate_docs = []
195
196
  for q in rewritten_queries:
196
- print(f"🔍 正在为查询变体 '{q}' 进行混合检索...")
197
+ PrettyOutput.print(f"正在为查询变体 '{q}' 进行混合检索...", OutputType.INFO)
197
198
  candidates = self._get_retriever().retrieve(
198
199
  q, n_results=n_results * 2, use_bm25=self.use_bm25
199
200
  )
@@ -208,7 +209,7 @@ class JarvisRAGPipeline:
208
209
 
209
210
  # 3. 根据*原始*查询对统一的候选池进行重排
210
211
  if self.use_rerank:
211
- print(f"🔍 正在对 {len(unique_candidate_docs)} 个候选文档进行重排(基于原始问题)...")
212
+ PrettyOutput.print(f"正在对 {len(unique_candidate_docs)} 个候选文档进行重排(基于原始问题)...", OutputType.INFO)
212
213
  retrieved_docs = self._get_reranker().rerank(
213
214
  query_text, unique_candidate_docs, top_n=n_results
214
215
  )
@@ -229,15 +230,15 @@ class JarvisRAGPipeline:
229
230
  )
230
231
  )
231
232
  if sources:
232
- print(f"📚 根据以下文档回答:")
233
+ PrettyOutput.print("根据以下文档回答:", OutputType.INFO)
233
234
  for source in sources:
234
- print(f" - {source}")
235
+ PrettyOutput.print(f" - {source}", OutputType.INFO)
235
236
 
236
237
  # 4. 创建最终提示并生成答案
237
238
  # 我们使用原始的query_text作为给LLM的最终提示
238
239
  prompt = self._create_prompt(query_text, retrieved_docs)
239
240
 
240
- print("🤖 正在从LLM生成答案...")
241
+ PrettyOutput.print("正在从LLM生成答案...", OutputType.INFO)
241
242
  answer = self.llm.generate(prompt)
242
243
 
243
244
  return answer
@@ -259,7 +260,7 @@ class JarvisRAGPipeline:
259
260
  # 2. 检索候选文档
260
261
  all_candidate_docs = []
261
262
  for q in rewritten_queries:
262
- print(f"🔍 正在为查询变体 '{q}' 进行混合检索...")
263
+ PrettyOutput.print(f"正在为查询变体 '{q}' 进行混合检索...", OutputType.INFO)
263
264
  candidates = self._get_retriever().retrieve(
264
265
  q, n_results=n_results * 2, use_bm25=self.use_bm25
265
266
  )
@@ -273,7 +274,7 @@ class JarvisRAGPipeline:
273
274
 
274
275
  # 3. 重排
275
276
  if self.use_rerank:
276
- print(f"🔍 正在对 {len(unique_candidate_docs)} 个候选文档进行重排...")
277
+ PrettyOutput.print(f"正在对 {len(unique_candidate_docs)} 个候选文档进行重排...", OutputType.INFO)
277
278
  retrieved_docs = self._get_reranker().rerank(
278
279
  query_text, unique_candidate_docs, top_n=n_results
279
280
  )
@@ -4,6 +4,7 @@ from langchain.docstore.document import Document
4
4
  from sentence_transformers.cross_encoder import ( # type: ignore
5
5
  CrossEncoder,
6
6
  )
7
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
7
8
 
8
9
 
9
10
  class Reranker:
@@ -19,9 +20,9 @@ class Reranker:
19
20
  参数:
20
21
  model_name (str): 要使用的Cross-Encoder模型的名称。
21
22
  """
22
- print(f"🔍 正在初始化重排模型: {model_name}...")
23
+ PrettyOutput.print(f"正在初始化重排模型: {model_name}...", OutputType.INFO)
23
24
  self.model = CrossEncoder(model_name)
24
- print("重排模型初始化成功。")
25
+ PrettyOutput.print("重排模型初始化成功。", OutputType.SUCCESS)
25
26
 
26
27
  def rerank(
27
28
  self, query: str, documents: List[Document], top_n: int = 5
@@ -8,6 +8,7 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
8
8
  from rank_bm25 import BM25Okapi # type: ignore
9
9
 
10
10
  from .embedding_manager import EmbeddingManager
11
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
11
12
 
12
13
 
13
14
  class ChromaRetriever:
@@ -39,7 +40,10 @@ class ChromaRetriever:
39
40
  self.collection = self.client.get_or_create_collection(
40
41
  name=self.collection_name
41
42
  )
42
- print(f"✅ ChromaDB 客户端已在 '{db_path}' 初始化,集合为 '{collection_name}'。")
43
+ PrettyOutput.print(
44
+ f"ChromaDB 客户端已在 '{db_path}' 初始化,集合为 '{collection_name}'。",
45
+ OutputType.SUCCESS,
46
+ )
43
47
 
44
48
  # BM25索引设置
45
49
  self.bm25_index_path = os.path.join(self.db_path, f"{collection_name}_bm25.pkl")
@@ -48,24 +52,24 @@ class ChromaRetriever:
48
52
  def _load_or_initialize_bm25(self):
49
53
  """从磁盘加载BM25索引或初始化一个新索引。"""
50
54
  if os.path.exists(self.bm25_index_path):
51
- print("🔍 正在加载现有的 BM25 索引...")
55
+ PrettyOutput.print("正在加载现有的 BM25 索引...", OutputType.INFO)
52
56
  with open(self.bm25_index_path, "rb") as f:
53
57
  data = pickle.load(f)
54
58
  self.bm25_corpus = data["corpus"]
55
59
  self.bm25_index = BM25Okapi(self.bm25_corpus)
56
- print("BM25 索引加载成功。")
60
+ PrettyOutput.print("BM25 索引加载成功。", OutputType.SUCCESS)
57
61
  else:
58
- print("⚠️ 未找到 BM25 索引,将初始化一个新的。")
62
+ PrettyOutput.print("未找到 BM25 索引,将初始化一个新的。", OutputType.WARNING)
59
63
  self.bm25_corpus = []
60
64
  self.bm25_index = None
61
65
 
62
66
  def _save_bm25_index(self):
63
67
  """将BM25索引保存到磁盘。"""
64
68
  if self.bm25_index:
65
- print("💾 正在保存 BM25 索引...")
69
+ PrettyOutput.print("正在保存 BM25 索引...", OutputType.INFO)
66
70
  with open(self.bm25_index_path, "wb") as f:
67
71
  pickle.dump({"corpus": self.bm25_corpus, "index": self.bm25_index}, f)
68
- print("BM25 索引保存成功。")
72
+ PrettyOutput.print("BM25 索引保存成功。", OutputType.SUCCESS)
69
73
 
70
74
  def add_documents(
71
75
  self, documents: List[Document], chunk_size=1000, chunk_overlap=100
@@ -78,7 +82,10 @@ class ChromaRetriever:
78
82
  )
79
83
  chunks = text_splitter.split_documents(documents)
80
84
 
81
- print(f"📄 已将 {len(documents)} 个文档拆分为 {len(chunks)} 个块。")
85
+ PrettyOutput.print(
86
+ f"已将 {len(documents)} 个文档拆分为 {len(chunks)} 个块。",
87
+ OutputType.INFO,
88
+ )
82
89
 
83
90
  if not chunks:
84
91
  return
@@ -97,7 +104,10 @@ class ChromaRetriever:
97
104
  documents=chunk_texts,
98
105
  metadatas=cast(Any, metadatas),
99
106
  )
100
- print(f"✅ 成功将 {len(chunks)} 个块添加到 ChromaDB 集合中。")
107
+ PrettyOutput.print(
108
+ f"成功将 {len(chunks)} 个块添加到 ChromaDB 集合中。",
109
+ OutputType.SUCCESS,
110
+ )
101
111
 
102
112
  # 更新并保存BM25索引
103
113
  tokenized_chunks = [doc.split() for doc in chunk_texts]
@@ -10,6 +10,7 @@ from jarvis.jarvis_platform.registry import PlatformRegistry
10
10
  from jarvis.jarvis_utils.config import get_shell_name, set_config
11
11
  from jarvis.jarvis_utils.input import get_multiline_input
12
12
  from jarvis.jarvis_utils.utils import init_env
13
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
13
14
 
14
15
  app = typer.Typer(
15
16
  help="将自然语言要求转换为shell命令",
@@ -58,38 +59,95 @@ def _get_markers() -> Tuple[str, str]:
58
59
  )
59
60
 
60
61
 
62
+ def _check_bash_shell() -> bool:
63
+ """Check if current shell is bash
64
+
65
+ Returns:
66
+ bool: True if bash shell, False otherwise
67
+ """
68
+ return get_shell_name() == "bash"
69
+
70
+
71
+ def _get_bash_config_file() -> str:
72
+ """Get bash config file path
73
+
74
+ Returns:
75
+ str: Path to bash config file (~/.bashrc)
76
+ """
77
+ return os.path.expanduser("~/.bashrc")
78
+
79
+
80
+ def _get_bash_markers() -> Tuple[str, str]:
81
+ """Get start and end markers for JSS completion in bash
82
+
83
+ Returns:
84
+ Tuple[str, str]: (start_marker, end_marker)
85
+ """
86
+ return (
87
+ "# ===== JARVIS JSS BASH COMPLETION START =====",
88
+ "# ===== JARVIS JSS BASH COMPLETION END =====",
89
+ )
90
+
91
+
92
+ def _check_zsh_shell() -> bool:
93
+ """Check if current shell is zsh
94
+
95
+ Returns:
96
+ bool: True if zsh shell, False otherwise
97
+ """
98
+ return get_shell_name() == "zsh"
99
+
100
+
101
+ def _get_zsh_config_file() -> str:
102
+ """Get zsh config file path
103
+
104
+ Returns:
105
+ str: Path to zsh config file (~/.zshrc)
106
+ """
107
+ return os.path.expanduser("~/.zshrc")
108
+
109
+
110
+ def _get_zsh_markers() -> Tuple[str, str]:
111
+ """Get start and end markers for JSS completion in zsh
112
+
113
+ Returns:
114
+ Tuple[str, str]: (start_marker, end_marker)
115
+ """
116
+ return (
117
+ "# ===== JARVIS JSS ZSH COMPLETION START =====",
118
+ "# ===== JARVIS JSS ZSH COMPLETION END =====",
119
+ )
120
+
121
+
61
122
  @app.command("install")
62
123
  def install_jss_completion(
63
- shell: str = typer.Option("fish", help="指定shell类型(仅支持fish)"),
124
+ shell: str = typer.Option("fish", help="指定shell类型(支持fish, bash, zsh)"),
64
125
  ) -> None:
65
- """为fish shell安装'命令未找到'处理器,实现自然语言命令执行"""
66
- if shell != "fish":
67
- print(f"错误: 不支持的shell类型: {shell}, 仅支持fish")
126
+ """为指定的shell安装'命令未找到'处理器,实现自然语言命令建议"""
127
+ if shell not in ("fish", "bash", "zsh"):
128
+ PrettyOutput.print(f"错误: 不支持的shell类型: {shell}, 仅支持fish, bash, zsh", OutputType.ERROR)
68
129
  raise typer.Exit(code=1)
69
130
 
70
- if not _check_fish_shell():
71
- print("当前不是fish shell,无需安装")
72
- return
131
+ if shell == "fish":
132
+ config_file = _get_config_file()
133
+ start_marker, end_marker = _get_markers()
73
134
 
74
- config_file = _get_config_file()
75
- start_marker, end_marker = _get_markers()
135
+ if not os.path.exists(config_file):
136
+ PrettyOutput.print("未找到 config.fish 文件,将创建新文件", OutputType.INFO)
137
+ os.makedirs(os.path.dirname(config_file), exist_ok=True)
138
+ with open(config_file, "w") as f:
139
+ f.write("")
76
140
 
77
- if not os.path.exists(config_file):
78
- print("未找到config.fish文件,将创建新文件")
79
- os.makedirs(os.path.dirname(config_file), exist_ok=True)
80
- with open(config_file, "w") as f:
81
- f.write("")
141
+ with open(config_file, "r") as f:
142
+ content = f.read()
82
143
 
83
- with open(config_file, "r") as f:
84
- content = f.read()
144
+ if start_marker in content:
145
+ PrettyOutput.print("JSS fish completion 已安装,请执行: source ~/.config/fish/config.fish", OutputType.SUCCESS)
146
+ return
85
147
 
86
- if start_marker in content:
87
- print("JSS fish completion已安装,请执行: source ~/.config/fish/config.fish")
88
- return
89
-
90
- with open(config_file, "a") as f:
91
- f.write(
92
- f"""
148
+ with open(config_file, "a") as f:
149
+ f.write(
150
+ f"""
93
151
  {start_marker}
94
152
  function fish_command_not_found
95
153
  if test (string length "$argv") -lt 10
@@ -103,43 +161,245 @@ function __fish_command_not_found_handler --on-event fish_command_not_found
103
161
  end
104
162
  {end_marker}
105
163
  """
106
- )
107
- print("JSS fish completion已安装,请执行: source ~/.config/fish/config.fish")
164
+ )
165
+ PrettyOutput.print("JSS fish completion 已安装,请执行: source ~/.config/fish/config.fish", OutputType.SUCCESS)
166
+ elif shell == "bash":
167
+ config_file = _get_bash_config_file()
168
+ start_marker, end_marker = _get_bash_markers()
169
+
170
+ if not os.path.exists(config_file):
171
+ PrettyOutput.print("未找到 ~/.bashrc 文件,将创建新文件", OutputType.INFO)
172
+ os.makedirs(os.path.dirname(config_file), exist_ok=True)
173
+ with open(config_file, "w") as f:
174
+ f.write("")
175
+
176
+ with open(config_file, "r") as f:
177
+ content = f.read()
178
+
179
+ if start_marker in content:
180
+ PrettyOutput.print("JSS bash completion 已安装,请执行: source ~/.bashrc", OutputType.SUCCESS)
181
+ return
182
+ else:
183
+ with open(config_file, "a") as f:
184
+ f.write(
185
+ f"""
186
+ {start_marker}
187
+ # Bash 'command not found' handler for JSS
188
+ # 行为:
189
+ # - 生成可编辑的建议命令,用户可直接编辑后回车执行
190
+ # - 非交互模式下仅打印建议
191
+ command_not_found_handle() {{
192
+ local cmd="$1"
193
+ shift || true
194
+ local text="$cmd $*"
195
+
196
+ # 与 fish 行为保持一致:对过短输入不处理
197
+ if [ ${{#text}} -lt 10 ]; then
198
+ return 127
199
+ fi
200
+
201
+ local suggestion edited
202
+ suggestion=$(jss request "$text")
203
+ if [ -n "$suggestion" ]; then
204
+ # 交互式:用 readline 预填命令,用户可直接回车执行或编辑
205
+ if [[ $- == *i* ]]; then
206
+ edited="$suggestion"
207
+ # -e 启用 readline;-i 预填默认值;无提示前缀,使体验更接近 fish 的“替换命令行”
208
+ read -e -i "$edited" edited
209
+ if [ -n "$edited" ]; then
210
+ eval "$edited"
211
+ return $?
212
+ fi
213
+ else
214
+ # 非交互:仅打印建议
215
+ printf '%s\n' "$suggestion"
216
+ fi
217
+ fi
218
+ return 127
219
+ }}
220
+ {end_marker}
221
+ """
222
+ )
223
+ PrettyOutput.print("JSS bash completion 已安装,请执行: source ~/.bashrc", OutputType.SUCCESS)
224
+ elif shell == "zsh":
225
+ config_file = _get_zsh_config_file()
226
+ start_marker, end_marker = _get_zsh_markers()
227
+
228
+ if not os.path.exists(config_file):
229
+ PrettyOutput.print("未找到 ~/.zshrc 文件,将创建新文件", OutputType.INFO)
230
+ os.makedirs(os.path.dirname(config_file), exist_ok=True)
231
+ with open(config_file, "w") as f:
232
+ f.write("")
233
+
234
+ with open(config_file, "r") as f:
235
+ content = f.read()
236
+
237
+ if start_marker in content:
238
+ PrettyOutput.print("JSS zsh completion 已安装,请执行: source ~/.zshrc", OutputType.SUCCESS)
239
+ return
240
+
241
+ with open(config_file, "a") as f:
242
+ f.write(
243
+ f"""
244
+ {start_marker}
245
+ # Zsh 'command not found' handler for JSS
246
+ # 行为:
247
+ # - 生成可编辑的建议命令,用户可直接编辑后回车执行
248
+ # - 非交互模式下仅打印建议
249
+ command_not_found_handler() {{
250
+ local cmd="$1"
251
+ shift || true
252
+ local text="$cmd $*"
253
+
254
+ # 与 fish 行为保持一致:对过短输入不处理
255
+ if [ ${{#text}} -lt 10 ]; then
256
+ return 127
257
+ fi
258
+
259
+ local suggestion edited
260
+ suggestion=$(jss request "$text")
261
+ if [ -n "$suggestion" ]; then
262
+ if [[ -o interactive ]]; then
263
+ local editor="${{VISUAL:-${{EDITOR:-vi}}}}"
264
+ local tmpfile edited
265
+ tmpfile="$(mktemp -t jss-edit-XXXXXX)"
266
+ printf '%s\n' "$suggestion" > "$tmpfile"
267
+ "$editor" "$tmpfile"
268
+ edited="$(sed -n '/./{{p;q;}}' "$tmpfile" | tr -d '\r')"
269
+ rm -f "$tmpfile"
270
+ if [ -z "$edited" ]; then
271
+ edited="$suggestion"
272
+ fi
273
+ eval "$edited"
274
+ return $?
275
+ else
276
+ # 非交互:仅打印建议
277
+ print -r -- "$suggestion"
278
+ fi
279
+ fi
280
+ return 127
281
+ }}
282
+ {end_marker}
283
+ """
284
+ )
285
+ PrettyOutput.print("JSS zsh completion 已安装,请执行: source ~/.zshrc", OutputType.SUCCESS)
286
+ return
287
+
288
+ with open(config_file, "a") as f:
289
+ f.write(
290
+ f"""
291
+ {start_marker}
292
+ # Bash 'command not found' handler for JSS
293
+ # 行为:
294
+ # - 生成可编辑的建议命令,用户可直接编辑后回车执行
295
+ # - 非交互模式下仅打印建议
296
+ command_not_found_handle() {{
297
+ local cmd="$1"
298
+ shift || true
299
+ local text="$cmd $*"
300
+
301
+ # 与 fish 行为保持一致:对过短输入不处理
302
+ if [ ${{#text}} -lt 10 ]; then
303
+ return 127
304
+ fi
305
+
306
+ local suggestion edited
307
+ suggestion=$(jss request "$text")
308
+ if [ -n "$suggestion" ]; then
309
+ # 交互式:用 readline 预填命令,用户可直接回车执行或编辑
310
+ if [[ $- == *i* ]]; then
311
+ edited="$suggestion"
312
+ # -e 启用 readline;-i 预填默认值;无提示前缀,使体验更接近 fish 的“替换命令行”
313
+ read -e -i "$edited" edited
314
+ if [ -n "$edited" ]; then
315
+ eval "$edited"
316
+ return $?
317
+ fi
318
+ else
319
+ # 非交互:仅打印建议
320
+ printf '%s\n' "$suggestion"
321
+ fi
322
+ fi
323
+ return 127
324
+ }}
325
+ {end_marker}
326
+ """
327
+ )
328
+ PrettyOutput.print("JSS bash completion 已安装,请执行: source ~/.bashrc", OutputType.SUCCESS)
108
329
 
109
330
 
110
331
  @app.command("uninstall")
111
332
  def uninstall_jss_completion(
112
- shell: str = typer.Option("fish", help="指定shell类型(仅支持fish)"),
333
+ shell: str = typer.Option("fish", help="指定shell类型(支持fish, bash, zsh)"),
113
334
  ) -> None:
114
- """卸载JSS fish shell'命令未找到'处理器"""
115
- if shell != "fish":
116
- print(f"错误: 不支持的shell类型: {shell}, 仅支持fish")
335
+ """卸载JSS shell'命令未找到'处理器"""
336
+ if shell not in ("fish", "bash", "zsh"):
337
+ PrettyOutput.print(f"错误: 不支持的shell类型: {shell}, 仅支持fish, bash, zsh", OutputType.ERROR)
117
338
  raise typer.Exit(code=1)
118
339
 
119
- if not _check_fish_shell():
120
- print("当前不是fish shell,无需卸载")
121
- return
340
+ if shell == "fish":
341
+ config_file = _get_config_file()
342
+ start_marker, end_marker = _get_markers()
122
343
 
123
- config_file = _get_config_file()
124
- start_marker, end_marker = _get_markers()
344
+ if not os.path.exists(config_file):
345
+ PrettyOutput.print("未找到 JSS fish completion 配置,无需卸载", OutputType.INFO)
346
+ return
125
347
 
126
- if not os.path.exists(config_file):
127
- print("未找到JSS fish completion配置,无需卸载")
128
- return
348
+ with open(config_file, "r") as f:
349
+ content = f.read()
129
350
 
130
- with open(config_file, "r") as f:
131
- content = f.read()
351
+ if start_marker not in content:
352
+ PrettyOutput.print("未找到 JSS fish completion 配置,无需卸载", OutputType.INFO)
353
+ return
132
354
 
133
- if start_marker not in content:
134
- print("未找到JSS fish completion配置,无需卸载")
135
- return
355
+ new_content = content.split(start_marker)[0] + content.split(end_marker)[-1]
356
+
357
+ with open(config_file, "w") as f:
358
+ f.write(new_content)
359
+
360
+ PrettyOutput.print("JSS fish completion 已卸载,请执行: source ~/.config/fish/config.fish", OutputType.SUCCESS)
361
+ elif shell == "bash":
362
+ config_file = _get_bash_config_file()
363
+ start_marker, end_marker = _get_bash_markers()
364
+
365
+ if not os.path.exists(config_file):
366
+ PrettyOutput.print("未找到 JSS bash completion 配置,无需卸载", OutputType.INFO)
367
+ return
368
+
369
+ with open(config_file, "r") as f:
370
+ content = f.read()
371
+
372
+ if start_marker not in content:
373
+ PrettyOutput.print("未找到 JSS bash completion 配置,无需卸载", OutputType.INFO)
374
+ return
136
375
 
137
- new_content = content.split(start_marker)[0] + content.split(end_marker)[-1]
376
+ new_content = content.split(start_marker)[0] + content.split(end_marker)[-1]
138
377
 
139
- with open(config_file, "w") as f:
140
- f.write(new_content)
378
+ with open(config_file, "w") as f:
379
+ f.write(new_content)
380
+
381
+ PrettyOutput.print("JSS bash completion 已卸载,请执行: source ~/.bashrc", OutputType.SUCCESS)
382
+ elif shell == "zsh":
383
+ config_file = _get_zsh_config_file()
384
+ start_marker, end_marker = _get_zsh_markers()
385
+
386
+ if not os.path.exists(config_file):
387
+ PrettyOutput.print("未找到 JSS zsh completion 配置,无需卸载", OutputType.INFO)
388
+ return
389
+
390
+ with open(config_file, "r") as f:
391
+ content = f.read()
392
+
393
+ if start_marker not in content:
394
+ PrettyOutput.print("未找到 JSS zsh completion 配置,无需卸载", OutputType.INFO)
395
+ return
396
+
397
+ new_content = content.split(start_marker)[0] + content.split(end_marker)[-1]
398
+
399
+ with open(config_file, "w") as f:
400
+ f.write(new_content)
141
401
 
142
- print("JSS fish completion已卸载,请执行: source ~/.config/fish/config.fish")
402
+ PrettyOutput.print("JSS zsh completion 已卸载,请执行: source ~/.zshrc", OutputType.SUCCESS)
143
403
 
144
404
 
145
405
  def process_request(request: str) -> Optional[str]:
@@ -93,6 +93,46 @@ class StatsManager:
93
93
  storage = StatsManager._get_storage()
94
94
  return storage.list_metrics()
95
95
 
96
+ @staticmethod
97
+ def get_metric_total(metric_name: str) -> float:
98
+ """
99
+ 获取指标的累计总量(快速路径)
100
+ 优先从总量缓存读取;若不存在则回溯历史数据计算一次后缓存
101
+ """
102
+ storage = StatsManager._get_storage()
103
+ try:
104
+ return float(storage.get_metric_total(metric_name))
105
+ except Exception:
106
+ return 0.0
107
+
108
+ @staticmethod
109
+ def get_metric_info(metric_name: str) -> Optional[Dict[str, Any]]:
110
+ """
111
+ 获取指标元信息(包含unit、created_at、last_updated、group等)
112
+ - 若缺少group,会尝试基于历史记录的tags进行推断并写回,然后返回最新信息
113
+ """
114
+ storage = StatsManager._get_storage()
115
+ info = storage.get_metric_info(metric_name)
116
+ if not info or not info.get("group"):
117
+ try:
118
+ grp = storage.resolve_metric_group(metric_name) # 触发一次分组解析与回填
119
+ if grp:
120
+ info = storage.get_metric_info(metric_name)
121
+ except Exception:
122
+ pass
123
+ return info
124
+
125
+ @staticmethod
126
+ def resolve_metric_group(metric_name: str) -> Optional[str]:
127
+ """
128
+ 主动解析并写回某个指标的分组信息(调用底层存储的推断逻辑)
129
+ """
130
+ storage = StatsManager._get_storage()
131
+ try:
132
+ return storage.resolve_metric_group(metric_name)
133
+ except Exception:
134
+ return None
135
+
96
136
  @staticmethod
97
137
  def show(
98
138
  metric_name: Optional[str] = None,