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
@@ -0,0 +1,329 @@
|
|
1
|
+
"""
|
2
|
+
统计数据存储模块
|
3
|
+
|
4
|
+
负责统计数据的持久化存储和读取
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
import os
|
9
|
+
from datetime import datetime, timedelta
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Dict, List, Optional, Any
|
12
|
+
from collections import defaultdict
|
13
|
+
import sys
|
14
|
+
import time
|
15
|
+
|
16
|
+
|
17
|
+
class StatsStorage:
|
18
|
+
"""统计数据存储类"""
|
19
|
+
|
20
|
+
def __init__(self, storage_dir: Optional[str] = None):
|
21
|
+
"""
|
22
|
+
初始化存储
|
23
|
+
|
24
|
+
Args:
|
25
|
+
storage_dir: 存储目录路径,默认为 ~/.jarvis/stats
|
26
|
+
"""
|
27
|
+
if storage_dir is None:
|
28
|
+
storage_dir = os.path.expanduser("~/.jarvis/stats")
|
29
|
+
|
30
|
+
self.storage_dir = Path(storage_dir)
|
31
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
32
|
+
|
33
|
+
# 数据目录路径
|
34
|
+
self.data_dir = self.storage_dir / "data"
|
35
|
+
self.data_dir.mkdir(exist_ok=True)
|
36
|
+
|
37
|
+
# 元数据文件路径
|
38
|
+
self.meta_file = self.storage_dir / "stats_meta.json"
|
39
|
+
|
40
|
+
# 初始化元数据
|
41
|
+
self._init_metadata()
|
42
|
+
|
43
|
+
def _init_metadata(self):
|
44
|
+
"""初始化元数据"""
|
45
|
+
if not self.meta_file.exists():
|
46
|
+
meta = {
|
47
|
+
"version": "1.0.0",
|
48
|
+
"created_at": datetime.now().isoformat(),
|
49
|
+
"metrics": {}, # 存储各个指标的元信息
|
50
|
+
}
|
51
|
+
self._save_json(self.meta_file, meta)
|
52
|
+
|
53
|
+
def _get_data_file(self, date_str: str) -> Path:
|
54
|
+
"""获取指定日期的数据文件路径"""
|
55
|
+
return self.data_dir / f"stats_{date_str}.json"
|
56
|
+
|
57
|
+
def _load_json(self, filepath: Path) -> Dict:
|
58
|
+
"""加载JSON文件"""
|
59
|
+
if not filepath.exists():
|
60
|
+
return {}
|
61
|
+
|
62
|
+
# 重试机制处理并发访问
|
63
|
+
max_retries = 3
|
64
|
+
for attempt in range(max_retries):
|
65
|
+
try:
|
66
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
67
|
+
data = json.load(f)
|
68
|
+
return data
|
69
|
+
except (json.JSONDecodeError, IOError):
|
70
|
+
if attempt < max_retries - 1:
|
71
|
+
time.sleep(0.1 * (attempt + 1)) # 递增延迟
|
72
|
+
continue
|
73
|
+
return {}
|
74
|
+
return {}
|
75
|
+
|
76
|
+
def _save_json(self, filepath: Path, data: Dict):
|
77
|
+
"""保存JSON文件"""
|
78
|
+
# 使用临时文件+重命名的原子操作来避免并发写入问题
|
79
|
+
temp_filepath = filepath.with_suffix(".tmp")
|
80
|
+
max_retries = 3
|
81
|
+
|
82
|
+
for attempt in range(max_retries):
|
83
|
+
try:
|
84
|
+
# 先写入临时文件
|
85
|
+
with open(temp_filepath, "w", encoding="utf-8") as f:
|
86
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
87
|
+
|
88
|
+
# Windows上需要先删除目标文件(如果存在)
|
89
|
+
if sys.platform == "win32" and filepath.exists():
|
90
|
+
filepath.unlink()
|
91
|
+
|
92
|
+
# 原子性重命名
|
93
|
+
temp_filepath.rename(filepath)
|
94
|
+
return
|
95
|
+
|
96
|
+
except Exception as e:
|
97
|
+
if attempt < max_retries - 1:
|
98
|
+
time.sleep(0.1 * (attempt + 1)) # 递增延迟
|
99
|
+
continue
|
100
|
+
# 清理临时文件
|
101
|
+
if temp_filepath.exists():
|
102
|
+
try:
|
103
|
+
temp_filepath.unlink()
|
104
|
+
except OSError:
|
105
|
+
pass
|
106
|
+
raise RuntimeError(f"保存数据失败: {e}") from e
|
107
|
+
|
108
|
+
def add_metric(
|
109
|
+
self,
|
110
|
+
metric_name: str,
|
111
|
+
value: float,
|
112
|
+
unit: Optional[str] = None,
|
113
|
+
timestamp: Optional[datetime] = None,
|
114
|
+
tags: Optional[Dict[str, str]] = None,
|
115
|
+
):
|
116
|
+
"""
|
117
|
+
添加统计数据
|
118
|
+
|
119
|
+
Args:
|
120
|
+
metric_name: 指标名称
|
121
|
+
value: 指标值
|
122
|
+
unit: 单位
|
123
|
+
timestamp: 时间戳,默认为当前时间
|
124
|
+
tags: 标签字典,用于数据分类
|
125
|
+
"""
|
126
|
+
if timestamp is None:
|
127
|
+
timestamp = datetime.now()
|
128
|
+
|
129
|
+
# 更新元数据
|
130
|
+
meta = self._load_json(self.meta_file)
|
131
|
+
if metric_name not in meta["metrics"]:
|
132
|
+
meta["metrics"][metric_name] = {
|
133
|
+
"unit": unit,
|
134
|
+
"created_at": timestamp.isoformat(),
|
135
|
+
"last_updated": timestamp.isoformat(),
|
136
|
+
}
|
137
|
+
else:
|
138
|
+
meta["metrics"][metric_name]["last_updated"] = timestamp.isoformat()
|
139
|
+
if unit and not meta["metrics"][metric_name].get("unit"):
|
140
|
+
meta["metrics"][metric_name]["unit"] = unit
|
141
|
+
self._save_json(self.meta_file, meta)
|
142
|
+
|
143
|
+
# 获取日期对应的数据文件
|
144
|
+
date_key = timestamp.strftime("%Y-%m-%d")
|
145
|
+
hour_key = timestamp.strftime("%H")
|
146
|
+
date_file = self._get_data_file(date_key)
|
147
|
+
|
148
|
+
# 加载日期文件的数据
|
149
|
+
data = self._load_json(date_file)
|
150
|
+
|
151
|
+
# 组织数据结构:metric_name -> hour -> records
|
152
|
+
if metric_name not in data:
|
153
|
+
data[metric_name] = {}
|
154
|
+
|
155
|
+
if hour_key not in data[metric_name]:
|
156
|
+
data[metric_name][hour_key] = []
|
157
|
+
|
158
|
+
# 添加数据记录
|
159
|
+
record = {
|
160
|
+
"timestamp": timestamp.isoformat(),
|
161
|
+
"value": value,
|
162
|
+
"tags": tags or {},
|
163
|
+
}
|
164
|
+
data[metric_name][hour_key].append(record)
|
165
|
+
|
166
|
+
# 保存数据到日期文件
|
167
|
+
self._save_json(date_file, data)
|
168
|
+
|
169
|
+
def get_metrics(
|
170
|
+
self,
|
171
|
+
metric_name: str,
|
172
|
+
start_time: Optional[datetime] = None,
|
173
|
+
end_time: Optional[datetime] = None,
|
174
|
+
tags: Optional[Dict[str, str]] = None,
|
175
|
+
) -> List[Dict]:
|
176
|
+
"""
|
177
|
+
获取指定时间范围的统计数据
|
178
|
+
|
179
|
+
Args:
|
180
|
+
metric_name: 指标名称
|
181
|
+
start_time: 开始时间
|
182
|
+
end_time: 结束时间
|
183
|
+
tags: 过滤标签
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
数据记录列表
|
187
|
+
"""
|
188
|
+
# 默认时间范围
|
189
|
+
if end_time is None:
|
190
|
+
end_time = datetime.now()
|
191
|
+
if start_time is None:
|
192
|
+
start_time = end_time - timedelta(days=7) # 默认最近7天
|
193
|
+
|
194
|
+
results = []
|
195
|
+
|
196
|
+
# 遍历日期
|
197
|
+
current_date = start_time.date()
|
198
|
+
end_date = end_time.date()
|
199
|
+
|
200
|
+
while current_date <= end_date:
|
201
|
+
date_key = current_date.strftime("%Y-%m-%d")
|
202
|
+
date_file = self._get_data_file(date_key)
|
203
|
+
|
204
|
+
# 如果日期文件不存在,跳过
|
205
|
+
if not date_file.exists():
|
206
|
+
current_date += timedelta(days=1)
|
207
|
+
continue
|
208
|
+
|
209
|
+
# 加载日期文件的数据
|
210
|
+
data = self._load_json(date_file)
|
211
|
+
|
212
|
+
if metric_name in data:
|
213
|
+
for hour_key, records in data[metric_name].items():
|
214
|
+
for record in records:
|
215
|
+
record_time = datetime.fromisoformat(record["timestamp"])
|
216
|
+
|
217
|
+
# 检查时间范围
|
218
|
+
if start_time <= record_time <= end_time:
|
219
|
+
# 检查标签过滤
|
220
|
+
if tags:
|
221
|
+
record_tags = record.get("tags", {})
|
222
|
+
if all(
|
223
|
+
record_tags.get(k) == v for k, v in tags.items()
|
224
|
+
):
|
225
|
+
results.append(record)
|
226
|
+
else:
|
227
|
+
results.append(record)
|
228
|
+
|
229
|
+
current_date += timedelta(days=1)
|
230
|
+
|
231
|
+
# 按时间排序
|
232
|
+
results.sort(key=lambda x: x["timestamp"])
|
233
|
+
|
234
|
+
return results
|
235
|
+
|
236
|
+
def get_metric_info(self, metric_name: str) -> Optional[Dict]:
|
237
|
+
"""获取指标元信息"""
|
238
|
+
meta = self._load_json(self.meta_file)
|
239
|
+
return meta.get("metrics", {}).get(metric_name)
|
240
|
+
|
241
|
+
def list_metrics(self) -> List[str]:
|
242
|
+
"""列出所有指标"""
|
243
|
+
meta = self._load_json(self.meta_file)
|
244
|
+
return list(meta.get("metrics", {}).keys())
|
245
|
+
|
246
|
+
def aggregate_metrics(
|
247
|
+
self,
|
248
|
+
metric_name: str,
|
249
|
+
start_time: Optional[datetime] = None,
|
250
|
+
end_time: Optional[datetime] = None,
|
251
|
+
aggregation: str = "hourly",
|
252
|
+
tags: Optional[Dict[str, str]] = None,
|
253
|
+
) -> Dict[str, Dict[str, Any]]:
|
254
|
+
"""
|
255
|
+
聚合统计数据
|
256
|
+
|
257
|
+
Args:
|
258
|
+
metric_name: 指标名称
|
259
|
+
start_time: 开始时间
|
260
|
+
end_time: 结束时间
|
261
|
+
aggregation: 聚合方式 (hourly, daily)
|
262
|
+
tags: 过滤标签
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
聚合后的数据字典
|
266
|
+
"""
|
267
|
+
records = self.get_metrics(metric_name, start_time, end_time, tags)
|
268
|
+
|
269
|
+
if not records:
|
270
|
+
return {}
|
271
|
+
|
272
|
+
# 聚合数据
|
273
|
+
aggregated = defaultdict(
|
274
|
+
lambda: {
|
275
|
+
"count": 0,
|
276
|
+
"sum": 0,
|
277
|
+
"min": float("inf"),
|
278
|
+
"max": float("-inf"),
|
279
|
+
"values": [],
|
280
|
+
}
|
281
|
+
)
|
282
|
+
|
283
|
+
for record in records:
|
284
|
+
timestamp = datetime.fromisoformat(record["timestamp"])
|
285
|
+
value = record["value"]
|
286
|
+
|
287
|
+
if aggregation == "hourly":
|
288
|
+
key = timestamp.strftime("%Y-%m-%d %H:00")
|
289
|
+
elif aggregation == "daily":
|
290
|
+
key = timestamp.strftime("%Y-%m-%d")
|
291
|
+
else:
|
292
|
+
key = timestamp.strftime("%Y-%m-%d %H:00")
|
293
|
+
|
294
|
+
aggregated[key]["count"] += 1
|
295
|
+
aggregated[key]["sum"] += value
|
296
|
+
aggregated[key]["min"] = min(aggregated[key]["min"], value)
|
297
|
+
aggregated[key]["max"] = max(aggregated[key]["max"], value)
|
298
|
+
aggregated[key]["values"].append(value)
|
299
|
+
|
300
|
+
# 计算平均值
|
301
|
+
result = {}
|
302
|
+
for key, stats in aggregated.items():
|
303
|
+
result[key] = {
|
304
|
+
"count": stats["count"],
|
305
|
+
"sum": stats["sum"],
|
306
|
+
"min": stats["min"],
|
307
|
+
"max": stats["max"],
|
308
|
+
"avg": stats["sum"] / stats["count"] if stats["count"] > 0 else 0,
|
309
|
+
}
|
310
|
+
|
311
|
+
return result
|
312
|
+
|
313
|
+
def delete_old_data(self, days_to_keep: int = 30):
|
314
|
+
"""删除旧数据"""
|
315
|
+
cutoff_date = (datetime.now() - timedelta(days=days_to_keep)).date()
|
316
|
+
|
317
|
+
# 遍历数据目录中的所有文件
|
318
|
+
for data_file in self.data_dir.glob("stats_*.json"):
|
319
|
+
try:
|
320
|
+
# 从文件名中提取日期
|
321
|
+
date_str = data_file.stem.replace("stats_", "")
|
322
|
+
file_date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
323
|
+
|
324
|
+
# 如果文件日期早于截止日期,删除文件
|
325
|
+
if file_date < cutoff_date:
|
326
|
+
data_file.unlink()
|
327
|
+
except (ValueError, OSError):
|
328
|
+
# 忽略无法解析或删除的文件
|
329
|
+
continue
|