jarvis-ai-assistant 0.2.3__py3-none-any.whl → 0.2.4__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/edit_file_handler.py +5 -0
- jarvis/jarvis_agent/jarvis.py +22 -25
- jarvis/jarvis_agent/main.py +6 -6
- jarvis/jarvis_code_agent/code_agent.py +279 -11
- jarvis/jarvis_code_analysis/code_review.py +21 -19
- jarvis/jarvis_data/config_schema.json +23 -10
- jarvis/jarvis_git_squash/main.py +3 -3
- jarvis/jarvis_git_utils/git_commiter.py +32 -11
- jarvis/jarvis_mcp/sse_mcp_client.py +4 -6
- jarvis/jarvis_mcp/streamable_mcp_client.py +5 -9
- jarvis/jarvis_rag/retriever.py +1 -1
- jarvis/jarvis_smart_shell/main.py +2 -2
- jarvis/jarvis_stats/__init__.py +13 -0
- jarvis/jarvis_stats/cli.py +337 -0
- jarvis/jarvis_stats/stats.py +433 -0
- jarvis/jarvis_stats/storage.py +329 -0
- jarvis/jarvis_stats/visualizer.py +443 -0
- jarvis/jarvis_tools/cli/main.py +84 -15
- jarvis/jarvis_tools/registry.py +35 -16
- jarvis/jarvis_tools/search_web.py +3 -3
- jarvis/jarvis_tools/virtual_tty.py +315 -26
- jarvis/jarvis_utils/config.py +6 -0
- jarvis/jarvis_utils/git_utils.py +8 -16
- jarvis/jarvis_utils/utils.py +210 -37
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/METADATA +19 -2
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/RECORD +31 -26
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/entry_points.txt +2 -0
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/top_level.txt +0 -0
@@ -139,7 +139,7 @@
|
|
139
139
|
"JARVIS_THINKING_MODEL": {
|
140
140
|
"type": "string",
|
141
141
|
"description": "思考操作模型名称",
|
142
|
-
"default": "
|
142
|
+
"default": "deep_seek_v3"
|
143
143
|
},
|
144
144
|
"JARVIS_LLM_GROUP": {
|
145
145
|
"type": "string",
|
@@ -169,7 +169,7 @@
|
|
169
169
|
},
|
170
170
|
"JARVIS_THINKING_MODEL": {
|
171
171
|
"type": "string",
|
172
|
-
"default": "
|
172
|
+
"default": "deep_seek_v3"
|
173
173
|
},
|
174
174
|
"JARVIS_MAX_TOKEN_COUNT": {
|
175
175
|
"type": "number",
|
@@ -234,6 +234,14 @@
|
|
234
234
|
},
|
235
235
|
"default": []
|
236
236
|
},
|
237
|
+
"JARVIS_METHODOLOGY_DIRS": {
|
238
|
+
"type": "array",
|
239
|
+
"description": "方法论加载目录",
|
240
|
+
"items": {
|
241
|
+
"type": "string"
|
242
|
+
},
|
243
|
+
"default": []
|
244
|
+
},
|
237
245
|
"JARVIS_PRINT_PROMPT": {
|
238
246
|
"type": "boolean",
|
239
247
|
"description": "是否打印提示",
|
@@ -260,11 +268,11 @@
|
|
260
268
|
"properties": {
|
261
269
|
"embedding_model": {
|
262
270
|
"type": "string",
|
263
|
-
"default": "BAAI/bge-
|
271
|
+
"default": "BAAI/bge-m3"
|
264
272
|
},
|
265
273
|
"rerank_model": {
|
266
274
|
"type": "string",
|
267
|
-
"default": "BAAI/bge-reranker-
|
275
|
+
"default": "BAAI/bge-reranker-v2-m3"
|
268
276
|
},
|
269
277
|
"use_bm25": {
|
270
278
|
"type": "boolean",
|
@@ -284,13 +292,13 @@
|
|
284
292
|
"properties": {
|
285
293
|
"embedding_model": {
|
286
294
|
"type": "string",
|
287
|
-
"default": "BAAI/bge-
|
288
|
-
"description": "用于RAG的嵌入模型的名称, 默认为 'BAAI/bge-
|
295
|
+
"default": "BAAI/bge-m3",
|
296
|
+
"description": "用于RAG的嵌入模型的名称, 默认为 'BAAI/bge-m3'"
|
289
297
|
},
|
290
298
|
"rerank_model": {
|
291
299
|
"type": "string",
|
292
|
-
"default": "BAAI/bge-reranker-
|
293
|
-
"description": "用于RAG的rerank模型的名称, 默认为 'BAAI/bge-reranker-
|
300
|
+
"default": "BAAI/bge-reranker-v2-m3",
|
301
|
+
"description": "用于RAG的rerank模型的名称, 默认为 'BAAI/bge-reranker-v2-m3'"
|
294
302
|
},
|
295
303
|
"use_bm25": {
|
296
304
|
"type": "boolean",
|
@@ -304,8 +312,8 @@
|
|
304
312
|
}
|
305
313
|
},
|
306
314
|
"default": {
|
307
|
-
"embedding_model": "BAAI/bge-
|
308
|
-
"rerank_model": "BAAI/bge-reranker-
|
315
|
+
"embedding_model": "BAAI/bge-m3",
|
316
|
+
"rerank_model": "BAAI/bge-reranker-v2-m3",
|
309
317
|
"use_bm25": true,
|
310
318
|
"use_rerank": true
|
311
319
|
}
|
@@ -361,6 +369,11 @@
|
|
361
369
|
"OYI_API_KEY": {
|
362
370
|
"type": "string",
|
363
371
|
"description": "Oyi API Key"
|
372
|
+
},
|
373
|
+
"SHELL": {
|
374
|
+
"type": "string",
|
375
|
+
"description": "系统Shell路径,用于获取当前使用的shell类型",
|
376
|
+
"default": "/bin/bash"
|
364
377
|
}
|
365
378
|
},
|
366
379
|
"additionalProperties": true
|
jarvis/jarvis_git_squash/main.py
CHANGED
@@ -9,7 +9,7 @@ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
9
9
|
from jarvis.jarvis_utils.utils import init_env
|
10
10
|
from jarvis.jarvis_utils.input import user_confirm
|
11
11
|
|
12
|
-
app = typer.Typer(help="Git
|
12
|
+
app = typer.Typer(help="Git压缩工具")
|
13
13
|
|
14
14
|
|
15
15
|
class GitSquashTool:
|
@@ -53,8 +53,8 @@ class GitSquashTool:
|
|
53
53
|
|
54
54
|
@app.command()
|
55
55
|
def cli(
|
56
|
-
commit_hash: str = typer.Argument(..., help="
|
57
|
-
lang: str = typer.Option("Chinese", "--lang", help="
|
56
|
+
commit_hash: str = typer.Argument(..., help="要压缩的基础提交哈希"),
|
57
|
+
lang: str = typer.Option("Chinese", "--lang", help="提交信息的语言"),
|
58
58
|
):
|
59
59
|
init_env("欢迎使用 Jarvis-GitSquash,您的Git压缩助手已准备就绪!")
|
60
60
|
tool = GitSquashTool()
|
@@ -21,7 +21,7 @@ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
21
21
|
from jarvis.jarvis_utils.tag import ct, ot
|
22
22
|
from jarvis.jarvis_utils.utils import init_env, is_context_overflow
|
23
23
|
|
24
|
-
app = typer.Typer(help="Git
|
24
|
+
app = typer.Typer(help="Git提交工具")
|
25
25
|
|
26
26
|
|
27
27
|
class GitCommitTool:
|
@@ -258,17 +258,38 @@ commit信息
|
|
258
258
|
|
259
259
|
# 执行提交
|
260
260
|
print("⚙️ 正在准备提交...")
|
261
|
-
|
261
|
+
# Windows 兼容性:使用 delete=False 避免权限错误
|
262
|
+
tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
263
|
+
tmp_file_path = tmp_file.name
|
264
|
+
try:
|
262
265
|
tmp_file.write(commit_message)
|
263
|
-
tmp_file.
|
266
|
+
tmp_file.close() # Windows 需要先关闭文件才能被其他进程读取
|
267
|
+
|
264
268
|
print("💾 正在执行提交...")
|
265
|
-
commit_cmd = ["git", "commit", "-F",
|
266
|
-
subprocess.Popen(
|
269
|
+
commit_cmd = ["git", "commit", "-F", tmp_file_path]
|
270
|
+
process = subprocess.Popen(
|
267
271
|
commit_cmd,
|
268
|
-
stdout=subprocess.
|
269
|
-
stderr=subprocess.
|
270
|
-
|
272
|
+
stdout=subprocess.PIPE,
|
273
|
+
stderr=subprocess.PIPE,
|
274
|
+
text=True,
|
275
|
+
)
|
276
|
+
stdout, stderr = process.communicate()
|
277
|
+
|
278
|
+
if process.returncode != 0:
|
279
|
+
# 如果提交失败,重置暂存区
|
280
|
+
subprocess.run(["git", "reset", "HEAD"], check=False)
|
281
|
+
error_msg = (
|
282
|
+
stderr.strip() if stderr else "Unknown git commit error"
|
283
|
+
)
|
284
|
+
raise Exception(f"Git commit failed: {error_msg}")
|
285
|
+
|
271
286
|
print("✅ 提交")
|
287
|
+
finally:
|
288
|
+
# 手动删除临时文件
|
289
|
+
try:
|
290
|
+
os.unlink(tmp_file_path)
|
291
|
+
except Exception:
|
292
|
+
pass
|
272
293
|
|
273
294
|
commit_hash = self._get_last_commit_hash()
|
274
295
|
print("✅ 完成提交")
|
@@ -310,17 +331,17 @@ commit信息
|
|
310
331
|
@app.command()
|
311
332
|
def cli(
|
312
333
|
root_dir: str = typer.Option(
|
313
|
-
".", "--root-dir", help="
|
334
|
+
".", "--root-dir", help="Git仓库的根目录路径"
|
314
335
|
),
|
315
336
|
prefix: str = typer.Option(
|
316
337
|
"",
|
317
338
|
"--prefix",
|
318
|
-
help="
|
339
|
+
help="提交信息前缀(用空格分隔)",
|
319
340
|
),
|
320
341
|
suffix: str = typer.Option(
|
321
342
|
"",
|
322
343
|
"--suffix",
|
323
|
-
help="
|
344
|
+
help="提交信息后缀(用换行分隔)",
|
324
345
|
),
|
325
346
|
):
|
326
347
|
init_env("欢迎使用 Jarvis-GitCommitTool,您的Git提交助手已准备就绪!")
|
@@ -50,9 +50,9 @@ class SSEMcpClient(McpClient):
|
|
50
50
|
self.sse_thread: Optional[threading.Thread] = None
|
51
51
|
self.messages_endpoint: Optional[str] = None
|
52
52
|
self.session_id: Optional[str] = None
|
53
|
-
self.pending_requests = {} # 存储等待响应的请求 {id: Event}
|
54
|
-
self.request_results = {} # 存储请求结果 {id: result}
|
55
|
-
self.notification_handlers = {}
|
53
|
+
self.pending_requests: Dict[str, threading.Event] = {} # 存储等待响应的请求 {id: Event}
|
54
|
+
self.request_results: Dict[str, Dict[str, Any]] = {} # 存储请求结果 {id: result}
|
55
|
+
self.notification_handlers: Dict[str, List[Callable]] = {}
|
56
56
|
self.event_lock = threading.Lock()
|
57
57
|
self.request_id_counter = 0
|
58
58
|
|
@@ -95,9 +95,7 @@ class SSEMcpClient(McpClient):
|
|
95
95
|
|
96
96
|
# 验证服务器响应
|
97
97
|
if "result" not in response:
|
98
|
-
raise RuntimeError(
|
99
|
-
f"初始化失败: {response.get('error', 'Unknown error')}"
|
100
|
-
)
|
98
|
+
raise RuntimeError(f"初始化失败: {response.get('error', 'Unknown error')}")
|
101
99
|
|
102
100
|
# 发送initialized通知
|
103
101
|
self._send_notification("notifications/initialized", {})
|
@@ -45,9 +45,9 @@ class StreamableMcpClient(McpClient):
|
|
45
45
|
self.session.headers.update(extra_headers)
|
46
46
|
|
47
47
|
# 请求相关属性
|
48
|
-
self.pending_requests = {} # 存储等待响应的请求 {id: Event}
|
49
|
-
self.request_results = {} # 存储请求结果 {id: result}
|
50
|
-
self.notification_handlers = {}
|
48
|
+
self.pending_requests: Dict[str, threading.Event] = {} # 存储等待响应的请求 {id: Event}
|
49
|
+
self.request_results: Dict[str, Dict[str, Any]] = {} # 存储请求结果 {id: result}
|
50
|
+
self.notification_handlers: Dict[str, List[Callable]] = {}
|
51
51
|
self.event_lock = threading.Lock()
|
52
52
|
self.request_id_counter = 0
|
53
53
|
|
@@ -70,9 +70,7 @@ class StreamableMcpClient(McpClient):
|
|
70
70
|
|
71
71
|
# 验证服务器响应
|
72
72
|
if "result" not in response:
|
73
|
-
raise RuntimeError(
|
74
|
-
f"初始化失败: {response.get('error', 'Unknown error')}"
|
75
|
-
)
|
73
|
+
raise RuntimeError(f"初始化失败: {response.get('error', 'Unknown error')}")
|
76
74
|
|
77
75
|
# 发送initialized通知
|
78
76
|
self._send_notification("notifications/initialized", {})
|
@@ -143,9 +141,7 @@ class StreamableMcpClient(McpClient):
|
|
143
141
|
|
144
142
|
# 发送请求到Streamable HTTP端点
|
145
143
|
mcp_url = urljoin(self.base_url, "mcp")
|
146
|
-
response = self.session.post(
|
147
|
-
mcp_url, json=request, stream=True # 启用流式传输
|
148
|
-
)
|
144
|
+
response = self.session.post(mcp_url, json=request, stream=True) # 启用流式传输
|
149
145
|
response.raise_for_status()
|
150
146
|
|
151
147
|
# 处理流式响应
|
jarvis/jarvis_rag/retriever.py
CHANGED
@@ -144,7 +144,7 @@ class ChromaRetriever:
|
|
144
144
|
]
|
145
145
|
|
146
146
|
# 按分数排序并取最高结果
|
147
|
-
bm25_results_with_docs.sort(key=lambda x: x[2], reverse=True)
|
147
|
+
bm25_results_with_docs.sort(key=lambda x: x[2], reverse=True) # type: ignore
|
148
148
|
|
149
149
|
for doc_text, metadata, _ in bm25_results_with_docs[: n_results * 2]:
|
150
150
|
bm25_docs.append(Document(page_content=doc_text, metadata=metadata))
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""
|
2
|
+
Jarvis统计模块
|
3
|
+
|
4
|
+
提供指标统计、数据持久化、可视化展示等功能
|
5
|
+
"""
|
6
|
+
|
7
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
8
|
+
from jarvis.jarvis_stats.storage import StatsStorage
|
9
|
+
from jarvis.jarvis_stats.visualizer import StatsVisualizer
|
10
|
+
|
11
|
+
__all__ = ["StatsManager", "StatsStorage", "StatsVisualizer"]
|
12
|
+
|
13
|
+
__version__ = "1.0.0"
|
@@ -0,0 +1,337 @@
|
|
1
|
+
"""
|
2
|
+
统计模块命令行接口
|
3
|
+
|
4
|
+
使用 typer 提供友好的命令行交互
|
5
|
+
"""
|
6
|
+
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
from typing import Optional, List
|
9
|
+
import typer
|
10
|
+
from rich.console import Console
|
11
|
+
from rich.table import Table
|
12
|
+
from rich import print as rprint
|
13
|
+
from pathlib import Path
|
14
|
+
|
15
|
+
from .stats import StatsManager
|
16
|
+
from jarvis.jarvis_utils.utils import init_env
|
17
|
+
from jarvis.jarvis_utils.config import get_data_dir
|
18
|
+
|
19
|
+
app = typer.Typer(help="Jarvis 统计模块命令行工具")
|
20
|
+
console = Console()
|
21
|
+
|
22
|
+
# 全局变量,存储是否已初始化
|
23
|
+
_initialized = False
|
24
|
+
_stats_dir = None
|
25
|
+
|
26
|
+
|
27
|
+
def _get_stats_dir():
|
28
|
+
"""获取统计数据目录"""
|
29
|
+
global _initialized, _stats_dir
|
30
|
+
if not _initialized:
|
31
|
+
_stats_dir = Path(get_data_dir()) / "stats"
|
32
|
+
_initialized = True
|
33
|
+
return str(_stats_dir)
|
34
|
+
|
35
|
+
|
36
|
+
@app.command()
|
37
|
+
def add(
|
38
|
+
metric: str = typer.Argument(..., help="指标名称"),
|
39
|
+
value: float = typer.Argument(..., help="指标值"),
|
40
|
+
unit: Optional[str] = typer.Option(None, "--unit", "-u", help="单位"),
|
41
|
+
tags: Optional[List[str]] = typer.Option(
|
42
|
+
None, "--tag", "-t", help="标签,格式: key=value"
|
43
|
+
),
|
44
|
+
):
|
45
|
+
"""添加统计数据"""
|
46
|
+
stats = StatsManager(_get_stats_dir())
|
47
|
+
|
48
|
+
# 解析标签
|
49
|
+
tag_dict = {}
|
50
|
+
if tags:
|
51
|
+
for tag in tags:
|
52
|
+
if "=" in tag:
|
53
|
+
key, val = tag.split("=", 1)
|
54
|
+
tag_dict[key] = val
|
55
|
+
|
56
|
+
stats.increment(
|
57
|
+
metric,
|
58
|
+
amount=value,
|
59
|
+
unit=unit if unit else "count",
|
60
|
+
tags=tag_dict if tag_dict else None,
|
61
|
+
)
|
62
|
+
|
63
|
+
rprint(
|
64
|
+
f"[green]✓[/green] 已添加数据: {metric}={value}" + (f" {unit}" if unit else "")
|
65
|
+
)
|
66
|
+
if tag_dict:
|
67
|
+
rprint(f" 标签: {tag_dict}")
|
68
|
+
|
69
|
+
|
70
|
+
@app.command()
|
71
|
+
def inc(
|
72
|
+
metric: str = typer.Argument(..., help="指标名称"),
|
73
|
+
amount: int = typer.Option(1, "--amount", "-a", help="增加的数量"),
|
74
|
+
tags: Optional[List[str]] = typer.Option(
|
75
|
+
None, "--tag", "-t", help="标签,格式: key=value"
|
76
|
+
),
|
77
|
+
):
|
78
|
+
"""增加计数型指标"""
|
79
|
+
stats = StatsManager(_get_stats_dir())
|
80
|
+
|
81
|
+
# 解析标签
|
82
|
+
tag_dict = {}
|
83
|
+
if tags:
|
84
|
+
for tag in tags:
|
85
|
+
if "=" in tag:
|
86
|
+
key, val = tag.split("=", 1)
|
87
|
+
tag_dict[key] = val
|
88
|
+
|
89
|
+
stats.increment(metric, amount=amount, tags=tag_dict if tag_dict else None)
|
90
|
+
|
91
|
+
rprint(f"[green]✓[/green] 已增加计数: {metric} +{amount}")
|
92
|
+
if tag_dict:
|
93
|
+
rprint(f" 标签: {tag_dict}")
|
94
|
+
|
95
|
+
|
96
|
+
@app.command()
|
97
|
+
def show(
|
98
|
+
metric: Optional[str] = typer.Argument(None, help="指标名称,不指定则显示所有"),
|
99
|
+
last_hours: Optional[int] = typer.Option(None, "--hours", "-h", help="最近N小时"),
|
100
|
+
last_days: Optional[int] = typer.Option(None, "--days", "-d", help="最近N天"),
|
101
|
+
format: str = typer.Option(
|
102
|
+
"table", "--format", "-f", help="显示格式: table/chart/summary"
|
103
|
+
),
|
104
|
+
aggregation: str = typer.Option(
|
105
|
+
"hourly", "--agg", "-a", help="聚合方式: hourly/daily"
|
106
|
+
),
|
107
|
+
tags: Optional[List[str]] = typer.Option(
|
108
|
+
None, "--tag", "-t", help="标签过滤,格式: key=value"
|
109
|
+
),
|
110
|
+
):
|
111
|
+
"""显示统计数据"""
|
112
|
+
stats = StatsManager(_get_stats_dir())
|
113
|
+
|
114
|
+
# 解析标签
|
115
|
+
tag_dict = {}
|
116
|
+
if tags:
|
117
|
+
for tag in tags:
|
118
|
+
if "=" in tag:
|
119
|
+
key, val = tag.split("=", 1)
|
120
|
+
tag_dict[key] = val
|
121
|
+
|
122
|
+
stats.show(
|
123
|
+
metric_name=metric,
|
124
|
+
last_hours=last_hours,
|
125
|
+
last_days=last_days,
|
126
|
+
format=format,
|
127
|
+
aggregation=aggregation,
|
128
|
+
tags=tag_dict if tag_dict else None,
|
129
|
+
)
|
130
|
+
|
131
|
+
|
132
|
+
@app.command()
|
133
|
+
def plot(
|
134
|
+
metric: Optional[str] = typer.Argument(
|
135
|
+
None, help="指标名称(可选,不指定则根据标签过滤所有匹配的指标)"
|
136
|
+
),
|
137
|
+
last_hours: Optional[int] = typer.Option(None, "--hours", "-h", help="最近N小时"),
|
138
|
+
last_days: Optional[int] = typer.Option(None, "--days", "-d", help="最近N天"),
|
139
|
+
aggregation: str = typer.Option(
|
140
|
+
"hourly", "--agg", "-a", help="聚合方式: hourly/daily"
|
141
|
+
),
|
142
|
+
width: Optional[int] = typer.Option(None, "--width", "-w", help="图表宽度"),
|
143
|
+
height: Optional[int] = typer.Option(None, "--height", "-H", help="图表高度"),
|
144
|
+
tags: Optional[List[str]] = typer.Option(
|
145
|
+
None, "--tag", "-t", help="标签过滤,格式: key=value"
|
146
|
+
),
|
147
|
+
):
|
148
|
+
"""绘制指标折线图,支持根据标签过滤显示多个指标"""
|
149
|
+
stats = StatsManager(_get_stats_dir())
|
150
|
+
|
151
|
+
# 解析标签
|
152
|
+
tag_dict = {}
|
153
|
+
if tags:
|
154
|
+
for tag in tags:
|
155
|
+
if "=" in tag:
|
156
|
+
key, val = tag.split("=", 1)
|
157
|
+
tag_dict[key] = val
|
158
|
+
|
159
|
+
stats.plot(
|
160
|
+
metric_name=metric,
|
161
|
+
last_hours=last_hours,
|
162
|
+
last_days=last_days,
|
163
|
+
aggregation=aggregation,
|
164
|
+
width=width,
|
165
|
+
height=height,
|
166
|
+
tags=tag_dict if tag_dict else None,
|
167
|
+
)
|
168
|
+
|
169
|
+
|
170
|
+
@app.command()
|
171
|
+
def list():
|
172
|
+
"""列出所有指标"""
|
173
|
+
stats = StatsManager(_get_stats_dir())
|
174
|
+
metrics = stats.list_metrics()
|
175
|
+
|
176
|
+
if not metrics:
|
177
|
+
rprint("[yellow]没有找到任何指标[/yellow]")
|
178
|
+
return
|
179
|
+
|
180
|
+
# 创建表格
|
181
|
+
table = Table(title="统计指标列表")
|
182
|
+
table.add_column("指标名称", style="cyan")
|
183
|
+
table.add_column("单位", style="green")
|
184
|
+
table.add_column("最后更新", style="yellow")
|
185
|
+
table.add_column("7天数据点", style="magenta")
|
186
|
+
|
187
|
+
# 获取每个指标的信息
|
188
|
+
end_time = datetime.now()
|
189
|
+
start_time = end_time - timedelta(days=7)
|
190
|
+
|
191
|
+
for metric in metrics:
|
192
|
+
info = stats.storage.get_metric_info(metric)
|
193
|
+
if info:
|
194
|
+
unit = info.get("unit", "-")
|
195
|
+
last_updated = info.get("last_updated", "-")
|
196
|
+
|
197
|
+
# 格式化时间
|
198
|
+
if last_updated != "-":
|
199
|
+
try:
|
200
|
+
dt = datetime.fromisoformat(last_updated)
|
201
|
+
last_updated = dt.strftime("%Y-%m-%d %H:%M")
|
202
|
+
except:
|
203
|
+
pass
|
204
|
+
|
205
|
+
# 获取数据点数
|
206
|
+
records = stats.storage.get_metrics(metric, start_time, end_time)
|
207
|
+
count = len(records)
|
208
|
+
|
209
|
+
table.add_row(metric, unit, last_updated, str(count))
|
210
|
+
|
211
|
+
console.print(table)
|
212
|
+
rprint(f"\n[green]总计: {len(metrics)} 个指标[/green]")
|
213
|
+
|
214
|
+
|
215
|
+
@app.command()
|
216
|
+
def clean(
|
217
|
+
days: int = typer.Option(30, "--days", "-d", help="保留最近N天的数据"),
|
218
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="跳过确认"),
|
219
|
+
):
|
220
|
+
"""清理旧数据"""
|
221
|
+
if not yes:
|
222
|
+
confirm = typer.confirm(f"确定要删除 {days} 天前的数据吗?")
|
223
|
+
if not confirm:
|
224
|
+
rprint("[yellow]已取消操作[/yellow]")
|
225
|
+
return
|
226
|
+
|
227
|
+
stats = StatsManager(_get_stats_dir())
|
228
|
+
stats.clean_old_data(days_to_keep=days)
|
229
|
+
rprint(f"[green]✓[/green] 已清理 {days} 天前的数据")
|
230
|
+
|
231
|
+
|
232
|
+
@app.command()
|
233
|
+
def export(
|
234
|
+
metric: str = typer.Argument(..., help="指标名称"),
|
235
|
+
output: str = typer.Option("csv", "--format", "-f", help="输出格式: csv/json"),
|
236
|
+
last_hours: Optional[int] = typer.Option(None, "--hours", "-h", help="最近N小时"),
|
237
|
+
last_days: Optional[int] = typer.Option(None, "--days", "-d", help="最近N天"),
|
238
|
+
tags: Optional[List[str]] = typer.Option(
|
239
|
+
None, "--tag", "-t", help="标签过滤,格式: key=value"
|
240
|
+
),
|
241
|
+
):
|
242
|
+
"""导出统计数据"""
|
243
|
+
import json
|
244
|
+
import csv
|
245
|
+
import sys
|
246
|
+
|
247
|
+
stats = StatsManager(_get_stats_dir())
|
248
|
+
|
249
|
+
# 解析标签
|
250
|
+
tag_dict = {}
|
251
|
+
if tags:
|
252
|
+
for tag in tags:
|
253
|
+
if "=" in tag:
|
254
|
+
key, val = tag.split("=", 1)
|
255
|
+
tag_dict[key] = val
|
256
|
+
|
257
|
+
# 获取数据
|
258
|
+
data = stats.get_stats(
|
259
|
+
metric_name=metric,
|
260
|
+
last_hours=last_hours,
|
261
|
+
last_days=last_days,
|
262
|
+
tags=tag_dict if tag_dict else None,
|
263
|
+
)
|
264
|
+
|
265
|
+
if output == "json":
|
266
|
+
# JSON格式输出
|
267
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
268
|
+
else:
|
269
|
+
# CSV格式输出
|
270
|
+
records = data.get("records", [])
|
271
|
+
if records:
|
272
|
+
writer = csv.writer(sys.stdout)
|
273
|
+
writer.writerow(["timestamp", "value", "tags"])
|
274
|
+
for record in records:
|
275
|
+
tags_str = json.dumps(record.get("tags", {}))
|
276
|
+
writer.writerow([record["timestamp"], record["value"], tags_str])
|
277
|
+
else:
|
278
|
+
rprint("[yellow]没有找到数据[/yellow]", file=sys.stderr)
|
279
|
+
|
280
|
+
|
281
|
+
@app.command()
|
282
|
+
def demo():
|
283
|
+
"""运行演示,展示统计模块的功能"""
|
284
|
+
import random
|
285
|
+
import time
|
286
|
+
|
287
|
+
console.print("[bold cyan]Jarvis 统计模块演示[/bold cyan]\n")
|
288
|
+
|
289
|
+
stats = StatsManager(_get_stats_dir())
|
290
|
+
|
291
|
+
# 添加演示数据
|
292
|
+
with console.status("[bold green]正在生成演示数据...") as status:
|
293
|
+
# API响应时间
|
294
|
+
for i in range(20):
|
295
|
+
response_time = random.uniform(0.1, 2.0)
|
296
|
+
status_code = random.choice(["200", "404", "500"])
|
297
|
+
stats.increment(
|
298
|
+
"demo_response_time",
|
299
|
+
amount=response_time,
|
300
|
+
unit="seconds",
|
301
|
+
tags={"status": status_code},
|
302
|
+
)
|
303
|
+
time.sleep(0.05)
|
304
|
+
|
305
|
+
# 访问计数
|
306
|
+
for i in range(30):
|
307
|
+
endpoint = random.choice(["/api/users", "/api/posts", "/api/admin"])
|
308
|
+
stats.increment("demo_api_calls", tags={"endpoint": endpoint})
|
309
|
+
time.sleep(0.05)
|
310
|
+
|
311
|
+
rprint("[green]✓[/green] 演示数据生成完成\n")
|
312
|
+
|
313
|
+
# 显示数据
|
314
|
+
console.rule("[bold blue]指标列表")
|
315
|
+
stats.show()
|
316
|
+
|
317
|
+
console.rule("[bold blue]响应时间详情")
|
318
|
+
stats.show("demo_response_time", last_hours=1)
|
319
|
+
|
320
|
+
console.rule("[bold blue]API调用折线图")
|
321
|
+
stats.plot("demo_api_calls", last_hours=1, height=10)
|
322
|
+
|
323
|
+
console.rule("[bold blue]响应时间汇总")
|
324
|
+
stats.show("demo_response_time", last_hours=1, format="summary")
|
325
|
+
|
326
|
+
rprint("\n[green]✓[/green] 演示完成!")
|
327
|
+
|
328
|
+
|
329
|
+
def main():
|
330
|
+
"""主入口函数"""
|
331
|
+
# 初始化环境,防止设置初始化太迟
|
332
|
+
init_env("欢迎使用 Jarvis-Stats,您的统计分析工具已准备就绪!", None)
|
333
|
+
app()
|
334
|
+
|
335
|
+
|
336
|
+
if __name__ == "__main__":
|
337
|
+
main()
|