jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.0__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 +1143 -245
- jarvis/jarvis_agent/agent_manager.py +97 -0
- jarvis/jarvis_agent/builtin_input_handler.py +12 -10
- jarvis/jarvis_agent/config_editor.py +57 -0
- jarvis/jarvis_agent/edit_file_handler.py +392 -99
- jarvis/jarvis_agent/event_bus.py +48 -0
- jarvis/jarvis_agent/events.py +157 -0
- jarvis/jarvis_agent/file_context_handler.py +79 -0
- jarvis/jarvis_agent/file_methodology_manager.py +117 -0
- jarvis/jarvis_agent/jarvis.py +1117 -147
- jarvis/jarvis_agent/main.py +78 -34
- jarvis/jarvis_agent/memory_manager.py +195 -0
- jarvis/jarvis_agent/methodology_share_manager.py +174 -0
- jarvis/jarvis_agent/prompt_manager.py +82 -0
- jarvis/jarvis_agent/prompts.py +46 -9
- jarvis/jarvis_agent/protocols.py +4 -1
- jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
- jarvis/jarvis_agent/run_loop.py +146 -0
- jarvis/jarvis_agent/session_manager.py +9 -9
- jarvis/jarvis_agent/share_manager.py +228 -0
- jarvis/jarvis_agent/shell_input_handler.py +23 -3
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +212 -0
- jarvis/jarvis_agent/task_manager.py +154 -0
- jarvis/jarvis_agent/task_planner.py +496 -0
- jarvis/jarvis_agent/tool_executor.py +8 -4
- jarvis/jarvis_agent/tool_share_manager.py +139 -0
- jarvis/jarvis_agent/user_interaction.py +42 -0
- jarvis/jarvis_agent/utils.py +54 -0
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +751 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +613 -0
- jarvis/jarvis_c2rust/collector.py +258 -0
- jarvis/jarvis_c2rust/library_replacer.py +1122 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
- jarvis/jarvis_c2rust/optimizer.py +960 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2325 -0
- jarvis/jarvis_code_agent/build_validation_config.py +133 -0
- jarvis/jarvis_code_agent/code_agent.py +1605 -178
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
- jarvis/jarvis_code_agent/lint.py +275 -13
- jarvis/jarvis_code_agent/utils.py +142 -0
- jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
- jarvis/jarvis_code_analysis/code_review.py +583 -548
- jarvis/jarvis_data/config_schema.json +339 -28
- jarvis/jarvis_git_squash/main.py +22 -13
- jarvis/jarvis_git_utils/git_commiter.py +171 -55
- jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
- jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
- jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
- jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
- jarvis/jarvis_methodology/main.py +48 -63
- jarvis/jarvis_multi_agent/__init__.py +302 -43
- jarvis/jarvis_multi_agent/main.py +70 -24
- jarvis/jarvis_platform/ai8.py +40 -23
- jarvis/jarvis_platform/base.py +210 -49
- jarvis/jarvis_platform/human.py +11 -1
- jarvis/jarvis_platform/kimi.py +82 -76
- jarvis/jarvis_platform/openai.py +73 -1
- jarvis/jarvis_platform/registry.py +8 -15
- jarvis/jarvis_platform/tongyi.py +115 -101
- jarvis/jarvis_platform/yuanbao.py +89 -63
- jarvis/jarvis_platform_manager/main.py +194 -132
- jarvis/jarvis_platform_manager/service.py +122 -86
- jarvis/jarvis_rag/cli.py +156 -53
- jarvis/jarvis_rag/embedding_manager.py +155 -12
- jarvis/jarvis_rag/llm_interface.py +10 -13
- jarvis/jarvis_rag/query_rewriter.py +63 -12
- jarvis/jarvis_rag/rag_pipeline.py +222 -40
- jarvis/jarvis_rag/reranker.py +26 -3
- jarvis/jarvis_rag/retriever.py +270 -14
- jarvis/jarvis_sec/__init__.py +3605 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +116 -0
- jarvis/jarvis_sec/report.py +257 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/workflow.py +219 -0
- jarvis/jarvis_smart_shell/main.py +405 -137
- jarvis/jarvis_stats/__init__.py +13 -0
- jarvis/jarvis_stats/cli.py +387 -0
- jarvis/jarvis_stats/stats.py +711 -0
- jarvis/jarvis_stats/storage.py +612 -0
- jarvis/jarvis_stats/visualizer.py +282 -0
- jarvis/jarvis_tools/ask_user.py +1 -0
- jarvis/jarvis_tools/base.py +18 -2
- jarvis/jarvis_tools/clear_memory.py +239 -0
- jarvis/jarvis_tools/cli/main.py +220 -144
- jarvis/jarvis_tools/execute_script.py +52 -12
- jarvis/jarvis_tools/file_analyzer.py +17 -12
- jarvis/jarvis_tools/generate_new_tool.py +46 -24
- jarvis/jarvis_tools/read_code.py +277 -18
- jarvis/jarvis_tools/read_symbols.py +141 -0
- jarvis/jarvis_tools/read_webpage.py +86 -13
- jarvis/jarvis_tools/registry.py +294 -90
- jarvis/jarvis_tools/retrieve_memory.py +227 -0
- jarvis/jarvis_tools/save_memory.py +194 -0
- jarvis/jarvis_tools/search_web.py +62 -28
- jarvis/jarvis_tools/sub_agent.py +205 -0
- jarvis/jarvis_tools/sub_code_agent.py +217 -0
- jarvis/jarvis_tools/virtual_tty.py +330 -62
- jarvis/jarvis_utils/builtin_replace_map.py +4 -5
- jarvis/jarvis_utils/clipboard.py +90 -0
- jarvis/jarvis_utils/config.py +607 -50
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/fzf.py +57 -0
- jarvis/jarvis_utils/git_utils.py +251 -29
- jarvis/jarvis_utils/globals.py +174 -17
- jarvis/jarvis_utils/http.py +58 -79
- jarvis/jarvis_utils/input.py +899 -153
- jarvis/jarvis_utils/methodology.py +210 -83
- jarvis/jarvis_utils/output.py +220 -137
- jarvis/jarvis_utils/utils.py +1906 -135
- jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
- jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
- jarvis/jarvis_git_details/main.py +0 -265
- jarvis/jarvis_platform/oyi.py +0 -357
- jarvis/jarvis_tools/edit_file.py +0 -255
- jarvis/jarvis_tools/rewrite_file.py +0 -195
- jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
- jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
- /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,612 @@
|
|
|
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, Set
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StatsStorage:
|
|
19
|
+
"""统计数据存储类"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, storage_dir: Optional[str] = None):
|
|
22
|
+
"""
|
|
23
|
+
初始化存储
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
storage_dir: 存储目录路径,默认为 ~/.jarvis/stats
|
|
27
|
+
"""
|
|
28
|
+
if storage_dir is None:
|
|
29
|
+
storage_dir = os.path.expanduser("~/.jarvis/stats")
|
|
30
|
+
|
|
31
|
+
self.storage_dir = Path(storage_dir)
|
|
32
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
# 数据目录路径
|
|
35
|
+
self.data_dir = self.storage_dir / "data"
|
|
36
|
+
self.data_dir.mkdir(exist_ok=True)
|
|
37
|
+
|
|
38
|
+
# 统计总量缓存目录(每个指标一个文件,内容为统计总量)
|
|
39
|
+
self.totals_dir = self.storage_dir / "totals"
|
|
40
|
+
self.totals_dir.mkdir(exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# 元数据文件路径
|
|
43
|
+
self.meta_file = self.storage_dir / "stats_meta.json"
|
|
44
|
+
|
|
45
|
+
# 初始化元数据
|
|
46
|
+
self._init_metadata()
|
|
47
|
+
|
|
48
|
+
def _init_metadata(self):
|
|
49
|
+
"""初始化元数据"""
|
|
50
|
+
if not self.meta_file.exists():
|
|
51
|
+
meta = {
|
|
52
|
+
"version": "1.0.0",
|
|
53
|
+
"created_at": datetime.now().isoformat(),
|
|
54
|
+
"metrics": {}, # 存储各个指标的元信息
|
|
55
|
+
}
|
|
56
|
+
self._save_json(self.meta_file, meta)
|
|
57
|
+
|
|
58
|
+
def _get_data_file(self, date_str: str) -> Path:
|
|
59
|
+
"""获取指定日期的数据文件路径"""
|
|
60
|
+
return self.data_dir / f"stats_{date_str}.json"
|
|
61
|
+
|
|
62
|
+
def _load_json(self, filepath: Path) -> Dict:
|
|
63
|
+
"""加载JSON文件"""
|
|
64
|
+
if not filepath.exists():
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
# 重试机制处理并发访问
|
|
68
|
+
max_retries = 3
|
|
69
|
+
for attempt in range(max_retries):
|
|
70
|
+
try:
|
|
71
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
72
|
+
data = json.load(f)
|
|
73
|
+
return data
|
|
74
|
+
except (json.JSONDecodeError, IOError):
|
|
75
|
+
if attempt < max_retries - 1:
|
|
76
|
+
time.sleep(0.1 * (attempt + 1)) # 递增延迟
|
|
77
|
+
continue
|
|
78
|
+
return {}
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
def _save_json(self, filepath: Path, data: Dict):
|
|
82
|
+
"""保存JSON文件"""
|
|
83
|
+
# 使用临时文件+重命名的原子操作来避免并发写入问题
|
|
84
|
+
# 使用唯一的临时文件名避免并发冲突
|
|
85
|
+
temp_suffix = f".tmp.{uuid.uuid4().hex[:8]}"
|
|
86
|
+
temp_filepath = filepath.with_suffix(temp_suffix)
|
|
87
|
+
max_retries = 3
|
|
88
|
+
|
|
89
|
+
for attempt in range(max_retries):
|
|
90
|
+
try:
|
|
91
|
+
# 先写入临时文件
|
|
92
|
+
with open(temp_filepath, "w", encoding="utf-8") as f:
|
|
93
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
94
|
+
|
|
95
|
+
# Windows上需要先删除目标文件(如果存在)
|
|
96
|
+
if sys.platform == "win32" and filepath.exists():
|
|
97
|
+
filepath.unlink()
|
|
98
|
+
|
|
99
|
+
# 原子性重命名
|
|
100
|
+
temp_filepath.rename(filepath)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
if attempt < max_retries - 1:
|
|
105
|
+
time.sleep(0.1 * (attempt + 1)) # 递增延迟
|
|
106
|
+
continue
|
|
107
|
+
# 清理临时文件
|
|
108
|
+
if temp_filepath.exists():
|
|
109
|
+
try:
|
|
110
|
+
temp_filepath.unlink()
|
|
111
|
+
except OSError:
|
|
112
|
+
pass
|
|
113
|
+
raise RuntimeError(f"保存数据失败: {e}") from e
|
|
114
|
+
|
|
115
|
+
def _save_text_atomic(self, filepath: Path, text: str):
|
|
116
|
+
"""原子性地保存纯文本内容"""
|
|
117
|
+
temp_suffix = f".tmp.{uuid.uuid4().hex[:8]}"
|
|
118
|
+
temp_filepath = filepath.with_suffix(temp_suffix)
|
|
119
|
+
max_retries = 3
|
|
120
|
+
for attempt in range(max_retries):
|
|
121
|
+
try:
|
|
122
|
+
with open(temp_filepath, "w", encoding="utf-8") as f:
|
|
123
|
+
f.write(text)
|
|
124
|
+
|
|
125
|
+
if sys.platform == "win32" and filepath.exists():
|
|
126
|
+
filepath.unlink()
|
|
127
|
+
temp_filepath.rename(filepath)
|
|
128
|
+
return
|
|
129
|
+
except Exception:
|
|
130
|
+
if attempt < max_retries - 1:
|
|
131
|
+
time.sleep(0.1 * (attempt + 1))
|
|
132
|
+
continue
|
|
133
|
+
if temp_filepath.exists():
|
|
134
|
+
try:
|
|
135
|
+
temp_filepath.unlink()
|
|
136
|
+
except OSError:
|
|
137
|
+
pass
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
def add_metric(
|
|
141
|
+
self,
|
|
142
|
+
metric_name: str,
|
|
143
|
+
value: float,
|
|
144
|
+
unit: Optional[str] = None,
|
|
145
|
+
timestamp: Optional[datetime] = None,
|
|
146
|
+
tags: Optional[Dict[str, str]] = None,
|
|
147
|
+
):
|
|
148
|
+
"""
|
|
149
|
+
添加统计数据
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
metric_name: 指标名称
|
|
153
|
+
value: 指标值
|
|
154
|
+
unit: 单位
|
|
155
|
+
timestamp: 时间戳,默认为当前时间
|
|
156
|
+
tags: 标签字典,用于数据分类
|
|
157
|
+
"""
|
|
158
|
+
if timestamp is None:
|
|
159
|
+
timestamp = datetime.now()
|
|
160
|
+
|
|
161
|
+
# 更新元数据
|
|
162
|
+
meta = self._load_json(self.meta_file)
|
|
163
|
+
if metric_name not in meta["metrics"]:
|
|
164
|
+
meta["metrics"][metric_name] = {
|
|
165
|
+
"unit": unit,
|
|
166
|
+
"created_at": timestamp.isoformat(),
|
|
167
|
+
"last_updated": timestamp.isoformat(),
|
|
168
|
+
}
|
|
169
|
+
else:
|
|
170
|
+
meta["metrics"][metric_name]["last_updated"] = timestamp.isoformat()
|
|
171
|
+
if unit and not meta["metrics"][metric_name].get("unit"):
|
|
172
|
+
meta["metrics"][metric_name]["unit"] = unit
|
|
173
|
+
|
|
174
|
+
# 记录分组信息(如果提供)
|
|
175
|
+
if tags and isinstance(tags, dict):
|
|
176
|
+
group = tags.get("group")
|
|
177
|
+
if group:
|
|
178
|
+
meta["metrics"][metric_name]["group"] = group
|
|
179
|
+
|
|
180
|
+
self._save_json(self.meta_file, meta)
|
|
181
|
+
|
|
182
|
+
# 获取日期对应的数据文件
|
|
183
|
+
date_key = timestamp.strftime("%Y-%m-%d")
|
|
184
|
+
hour_key = timestamp.strftime("%H")
|
|
185
|
+
date_file = self._get_data_file(date_key)
|
|
186
|
+
|
|
187
|
+
# 加载日期文件的数据
|
|
188
|
+
data = self._load_json(date_file)
|
|
189
|
+
|
|
190
|
+
# 组织数据结构:metric_name -> hour -> records
|
|
191
|
+
if metric_name not in data:
|
|
192
|
+
data[metric_name] = {}
|
|
193
|
+
|
|
194
|
+
if hour_key not in data[metric_name]:
|
|
195
|
+
data[metric_name][hour_key] = []
|
|
196
|
+
|
|
197
|
+
# 添加数据记录
|
|
198
|
+
record = {
|
|
199
|
+
"timestamp": timestamp.isoformat(),
|
|
200
|
+
"value": value,
|
|
201
|
+
"tags": tags or {},
|
|
202
|
+
}
|
|
203
|
+
data[metric_name][hour_key].append(record)
|
|
204
|
+
|
|
205
|
+
# 保存数据到日期文件
|
|
206
|
+
self._save_json(date_file, data)
|
|
207
|
+
|
|
208
|
+
# 更新总量缓存文件(每个指标一个文件,内容为累计统计值)
|
|
209
|
+
try:
|
|
210
|
+
total_file = self._get_total_file(metric_name)
|
|
211
|
+
if total_file.exists():
|
|
212
|
+
# 正常累加
|
|
213
|
+
try:
|
|
214
|
+
with open(total_file, "r", encoding="utf-8") as tf:
|
|
215
|
+
current_total = float(tf.read().strip() or "0")
|
|
216
|
+
except Exception:
|
|
217
|
+
current_total = 0.0
|
|
218
|
+
new_total = current_total + float(value)
|
|
219
|
+
self._save_text_atomic(total_file, str(new_total))
|
|
220
|
+
else:
|
|
221
|
+
# 首次生成:扫描历史数据(包含刚写入的这条记录)并写入
|
|
222
|
+
# 注意:get_metric_total 内部会完成扫描并写入 totals 文件,这里无需再额外写入或累加
|
|
223
|
+
_ = self.get_metric_total(metric_name)
|
|
224
|
+
except Exception:
|
|
225
|
+
# 静默失败,不影响主流程
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
def get_metrics(
|
|
229
|
+
self,
|
|
230
|
+
metric_name: str,
|
|
231
|
+
start_time: Optional[datetime] = None,
|
|
232
|
+
end_time: Optional[datetime] = None,
|
|
233
|
+
tags: Optional[Dict[str, str]] = None,
|
|
234
|
+
) -> List[Dict]:
|
|
235
|
+
"""
|
|
236
|
+
获取指定时间范围的统计数据
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
metric_name: 指标名称
|
|
240
|
+
start_time: 开始时间
|
|
241
|
+
end_time: 结束时间
|
|
242
|
+
tags: 过滤标签
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
数据记录列表
|
|
246
|
+
"""
|
|
247
|
+
# 默认时间范围
|
|
248
|
+
if end_time is None:
|
|
249
|
+
end_time = datetime.now()
|
|
250
|
+
if start_time is None:
|
|
251
|
+
start_time = end_time - timedelta(days=7) # 默认最近7天
|
|
252
|
+
|
|
253
|
+
results = []
|
|
254
|
+
|
|
255
|
+
# 遍历日期
|
|
256
|
+
current_date = start_time.date()
|
|
257
|
+
end_date = end_time.date()
|
|
258
|
+
|
|
259
|
+
while current_date <= end_date:
|
|
260
|
+
date_key = current_date.strftime("%Y-%m-%d")
|
|
261
|
+
date_file = self._get_data_file(date_key)
|
|
262
|
+
|
|
263
|
+
# 如果日期文件不存在,跳过
|
|
264
|
+
if not date_file.exists():
|
|
265
|
+
current_date += timedelta(days=1)
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# 加载日期文件的数据
|
|
269
|
+
data = self._load_json(date_file)
|
|
270
|
+
|
|
271
|
+
if metric_name in data:
|
|
272
|
+
for hour_key, records in data[metric_name].items():
|
|
273
|
+
for record in records:
|
|
274
|
+
record_time = datetime.fromisoformat(record["timestamp"])
|
|
275
|
+
|
|
276
|
+
# 检查时间范围
|
|
277
|
+
if start_time <= record_time <= end_time:
|
|
278
|
+
# 检查标签过滤
|
|
279
|
+
if tags:
|
|
280
|
+
record_tags = record.get("tags", {})
|
|
281
|
+
if all(
|
|
282
|
+
record_tags.get(k) == v for k, v in tags.items()
|
|
283
|
+
):
|
|
284
|
+
results.append(record)
|
|
285
|
+
else:
|
|
286
|
+
results.append(record)
|
|
287
|
+
|
|
288
|
+
current_date += timedelta(days=1)
|
|
289
|
+
|
|
290
|
+
# 按时间排序
|
|
291
|
+
results.sort(key=lambda x: x["timestamp"])
|
|
292
|
+
|
|
293
|
+
return results
|
|
294
|
+
|
|
295
|
+
def _get_total_file(self, metric_name: str) -> Path:
|
|
296
|
+
"""获取某个指标的总量文件路径"""
|
|
297
|
+
return self.totals_dir / metric_name
|
|
298
|
+
|
|
299
|
+
def get_metric_total(self, metric_name: str) -> float:
|
|
300
|
+
"""
|
|
301
|
+
获取某个指标的累计总量。
|
|
302
|
+
- 如果总量缓存文件存在,直接读取
|
|
303
|
+
- 如果不存在,则扫描历史数据计算一次并写入缓存
|
|
304
|
+
"""
|
|
305
|
+
total_file = self._get_total_file(metric_name)
|
|
306
|
+
# 优先读取缓存
|
|
307
|
+
if total_file.exists():
|
|
308
|
+
try:
|
|
309
|
+
with open(total_file, "r", encoding="utf-8") as f:
|
|
310
|
+
return float((f.read() or "0").strip() or "0")
|
|
311
|
+
except Exception:
|
|
312
|
+
# 读取失败则重建
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
# 扫描历史数据进行一次性计算,并尽可能推断分组信息
|
|
316
|
+
total = 0.0
|
|
317
|
+
group_counts: Dict[str, int] = {}
|
|
318
|
+
try:
|
|
319
|
+
for data_file in self.data_dir.glob("stats_*.json"):
|
|
320
|
+
data = self._load_json(data_file)
|
|
321
|
+
metric_data = data.get(metric_name) or {}
|
|
322
|
+
# metric_data: {hour_key: [records]}
|
|
323
|
+
for hour_records in metric_data.values():
|
|
324
|
+
for record in hour_records:
|
|
325
|
+
# 累加数值
|
|
326
|
+
try:
|
|
327
|
+
total += float(record.get("value", 0))
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
# 统计历史记录中的分组标签
|
|
331
|
+
try:
|
|
332
|
+
tags = record.get("tags", {})
|
|
333
|
+
grp = tags.get("group")
|
|
334
|
+
if grp:
|
|
335
|
+
group_counts[grp] = group_counts.get(grp, 0) + 1
|
|
336
|
+
except Exception:
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
# 写入缓存
|
|
340
|
+
self._save_text_atomic(total_file, str(total))
|
|
341
|
+
|
|
342
|
+
# 如果元数据中没有该指标或缺少分组,则根据历史数据推断一次
|
|
343
|
+
try:
|
|
344
|
+
meta = self._load_json(self.meta_file)
|
|
345
|
+
if "metrics" not in meta or not isinstance(meta.get("metrics"), dict):
|
|
346
|
+
meta["metrics"] = {}
|
|
347
|
+
info = meta["metrics"].get(metric_name)
|
|
348
|
+
now_iso = datetime.now().isoformat()
|
|
349
|
+
if info is None:
|
|
350
|
+
info = {
|
|
351
|
+
"unit": None,
|
|
352
|
+
"created_at": now_iso,
|
|
353
|
+
"last_updated": now_iso,
|
|
354
|
+
}
|
|
355
|
+
meta["metrics"][metric_name] = info
|
|
356
|
+
if not info.get("group"):
|
|
357
|
+
inferred_group = None
|
|
358
|
+
if group_counts:
|
|
359
|
+
inferred_group = max(
|
|
360
|
+
group_counts.items(), key=lambda kv: kv[1]
|
|
361
|
+
)[0]
|
|
362
|
+
# 名称启发式作为补充
|
|
363
|
+
if not inferred_group:
|
|
364
|
+
if (
|
|
365
|
+
metric_name.startswith("code_lines_")
|
|
366
|
+
or "commit" in metric_name
|
|
367
|
+
):
|
|
368
|
+
inferred_group = "code_agent"
|
|
369
|
+
if inferred_group:
|
|
370
|
+
info["group"] = inferred_group
|
|
371
|
+
# 保存元数据
|
|
372
|
+
self._save_json(self.meta_file, meta)
|
|
373
|
+
except Exception:
|
|
374
|
+
# 分组推断失败不影响总量结果
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
except Exception:
|
|
378
|
+
# 失败则返回0
|
|
379
|
+
return 0.0
|
|
380
|
+
return total
|
|
381
|
+
|
|
382
|
+
def resolve_metric_group(self, metric_name: str) -> Optional[str]:
|
|
383
|
+
"""
|
|
384
|
+
解析并确保写回某个指标的分组信息:
|
|
385
|
+
- 若元数据已存在group则直接返回
|
|
386
|
+
- 否则扫描历史记录中的tags['group']做多数投票推断
|
|
387
|
+
- 若仍无法得到,则用名称启发式(code_lines_*或包含commit -> code_agent)
|
|
388
|
+
- 推断出group后会写回到元数据,返回推断值;否则返回None
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
# 优先从元数据读取
|
|
392
|
+
meta = self._load_json(self.meta_file)
|
|
393
|
+
metrics_meta = (
|
|
394
|
+
meta.get("metrics", {}) if isinstance(meta.get("metrics"), dict) else {}
|
|
395
|
+
)
|
|
396
|
+
info = metrics_meta.get(metric_name)
|
|
397
|
+
if info and isinstance(info, dict):
|
|
398
|
+
grp = info.get("group")
|
|
399
|
+
if grp:
|
|
400
|
+
return grp
|
|
401
|
+
|
|
402
|
+
# 扫描历史记录以推断
|
|
403
|
+
group_counts: Dict[str, int] = {}
|
|
404
|
+
for data_file in self.data_dir.glob("stats_*.json"):
|
|
405
|
+
data = self._load_json(data_file)
|
|
406
|
+
metric_data = data.get(metric_name) or {}
|
|
407
|
+
for hour_records in metric_data.values():
|
|
408
|
+
for record in hour_records:
|
|
409
|
+
try:
|
|
410
|
+
tags = record.get("tags", {})
|
|
411
|
+
grp = tags.get("group")
|
|
412
|
+
if grp:
|
|
413
|
+
group_counts[grp] = group_counts.get(grp, 0) + 1
|
|
414
|
+
except Exception:
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
inferred_group: Optional[str] = None
|
|
418
|
+
if group_counts:
|
|
419
|
+
inferred_group = max(group_counts.items(), key=lambda kv: kv[1])[0]
|
|
420
|
+
|
|
421
|
+
# 名称启发式补充
|
|
422
|
+
if not inferred_group:
|
|
423
|
+
name = metric_name or ""
|
|
424
|
+
if name.startswith("code_lines_") or ("commit" in name):
|
|
425
|
+
inferred_group = "code_agent"
|
|
426
|
+
|
|
427
|
+
# 如果推断出了分组,写回元数据
|
|
428
|
+
if inferred_group:
|
|
429
|
+
if not isinstance(metrics_meta, dict):
|
|
430
|
+
meta["metrics"] = {}
|
|
431
|
+
metrics_meta = meta["metrics"]
|
|
432
|
+
if info is None:
|
|
433
|
+
now_iso = datetime.now().isoformat()
|
|
434
|
+
info = {
|
|
435
|
+
"unit": None,
|
|
436
|
+
"created_at": now_iso,
|
|
437
|
+
"last_updated": now_iso,
|
|
438
|
+
}
|
|
439
|
+
metrics_meta[metric_name] = info
|
|
440
|
+
info["group"] = inferred_group
|
|
441
|
+
self._save_json(self.meta_file, meta)
|
|
442
|
+
return inferred_group
|
|
443
|
+
|
|
444
|
+
return None
|
|
445
|
+
except Exception:
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
def get_metric_info(self, metric_name: str) -> Optional[Dict]:
|
|
449
|
+
"""获取指标元信息"""
|
|
450
|
+
meta = self._load_json(self.meta_file)
|
|
451
|
+
return meta.get("metrics", {}).get(metric_name)
|
|
452
|
+
|
|
453
|
+
def list_metrics(self) -> List[str]:
|
|
454
|
+
"""列出所有指标"""
|
|
455
|
+
# 从元数据文件获取指标
|
|
456
|
+
meta = self._load_json(self.meta_file)
|
|
457
|
+
metrics_from_meta = set(meta.get("metrics", {}).keys())
|
|
458
|
+
|
|
459
|
+
# 扫描所有数据文件获取实际存在的指标
|
|
460
|
+
metrics_from_data: Set[str] = set()
|
|
461
|
+
for data_file in self.data_dir.glob("stats_*.json"):
|
|
462
|
+
try:
|
|
463
|
+
data = self._load_json(data_file)
|
|
464
|
+
metrics_from_data.update(data.keys())
|
|
465
|
+
except (json.JSONDecodeError, OSError):
|
|
466
|
+
# 忽略无法读取的文件
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
# 扫描总量缓存目录中已有的指标文件
|
|
470
|
+
metrics_from_totals: Set[str] = set()
|
|
471
|
+
try:
|
|
472
|
+
for f in self.totals_dir.glob("*"):
|
|
473
|
+
if f.is_file():
|
|
474
|
+
metrics_from_totals.add(f.name)
|
|
475
|
+
except Exception:
|
|
476
|
+
pass
|
|
477
|
+
|
|
478
|
+
# 合并三个来源的指标并返回排序后的列表
|
|
479
|
+
all_metrics = metrics_from_meta.union(metrics_from_data).union(
|
|
480
|
+
metrics_from_totals
|
|
481
|
+
)
|
|
482
|
+
return sorted(list(all_metrics))
|
|
483
|
+
|
|
484
|
+
def aggregate_metrics(
|
|
485
|
+
self,
|
|
486
|
+
metric_name: str,
|
|
487
|
+
start_time: Optional[datetime] = None,
|
|
488
|
+
end_time: Optional[datetime] = None,
|
|
489
|
+
aggregation: str = "hourly",
|
|
490
|
+
tags: Optional[Dict[str, str]] = None,
|
|
491
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
492
|
+
"""
|
|
493
|
+
聚合统计数据
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
metric_name: 指标名称
|
|
497
|
+
start_time: 开始时间
|
|
498
|
+
end_time: 结束时间
|
|
499
|
+
aggregation: 聚合方式 (hourly, daily)
|
|
500
|
+
tags: 过滤标签
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
聚合后的数据字典
|
|
504
|
+
"""
|
|
505
|
+
records = self.get_metrics(metric_name, start_time, end_time, tags)
|
|
506
|
+
|
|
507
|
+
if not records:
|
|
508
|
+
return {}
|
|
509
|
+
|
|
510
|
+
# 聚合数据
|
|
511
|
+
aggregated: Dict[str, Dict[str, Any]] = defaultdict(
|
|
512
|
+
lambda: {
|
|
513
|
+
"count": 0,
|
|
514
|
+
"sum": 0,
|
|
515
|
+
"min": float("inf"),
|
|
516
|
+
"max": float("-inf"),
|
|
517
|
+
"values": [],
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
for record in records:
|
|
522
|
+
timestamp = datetime.fromisoformat(record["timestamp"])
|
|
523
|
+
value = record["value"]
|
|
524
|
+
|
|
525
|
+
if aggregation == "hourly":
|
|
526
|
+
key = timestamp.strftime("%Y-%m-%d %H:00")
|
|
527
|
+
elif aggregation == "daily":
|
|
528
|
+
key = timestamp.strftime("%Y-%m-%d")
|
|
529
|
+
else:
|
|
530
|
+
key = timestamp.strftime("%Y-%m-%d %H:00")
|
|
531
|
+
|
|
532
|
+
aggregated[key]["count"] += 1
|
|
533
|
+
aggregated[key]["sum"] += value
|
|
534
|
+
aggregated[key]["min"] = min(aggregated[key]["min"], value)
|
|
535
|
+
aggregated[key]["max"] = max(aggregated[key]["max"], value)
|
|
536
|
+
aggregated[key]["values"].append(value)
|
|
537
|
+
|
|
538
|
+
# 计算平均值
|
|
539
|
+
result = {}
|
|
540
|
+
for key, stats in aggregated.items():
|
|
541
|
+
result[key] = {
|
|
542
|
+
"count": stats["count"],
|
|
543
|
+
"sum": stats["sum"],
|
|
544
|
+
"min": stats["min"],
|
|
545
|
+
"max": stats["max"],
|
|
546
|
+
"avg": stats["sum"] / stats["count"] if stats["count"] > 0 else 0,
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return result
|
|
550
|
+
|
|
551
|
+
def delete_metric(self, metric_name: str) -> bool:
|
|
552
|
+
"""
|
|
553
|
+
删除指定的指标及其所有数据
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
metric_name: 要删除的指标名称
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
True 如果成功删除,False 如果指标不存在
|
|
560
|
+
"""
|
|
561
|
+
# 检查指标是否存在
|
|
562
|
+
meta = self._load_json(self.meta_file)
|
|
563
|
+
if metric_name not in meta.get("metrics", {}):
|
|
564
|
+
return False
|
|
565
|
+
|
|
566
|
+
# 从元数据中删除指标
|
|
567
|
+
del meta["metrics"][metric_name]
|
|
568
|
+
self._save_json(self.meta_file, meta)
|
|
569
|
+
|
|
570
|
+
# 遍历所有数据文件,删除该指标的数据
|
|
571
|
+
for data_file in self.data_dir.glob("stats_*.json"):
|
|
572
|
+
try:
|
|
573
|
+
data = self._load_json(data_file)
|
|
574
|
+
if metric_name in data:
|
|
575
|
+
del data[metric_name]
|
|
576
|
+
# 如果文件中还有其他数据,保存更新后的文件
|
|
577
|
+
if data:
|
|
578
|
+
self._save_json(data_file, data)
|
|
579
|
+
# 如果文件变空了,删除文件
|
|
580
|
+
else:
|
|
581
|
+
data_file.unlink()
|
|
582
|
+
except Exception:
|
|
583
|
+
# 忽略单个文件的错误,继续处理其他文件
|
|
584
|
+
pass
|
|
585
|
+
|
|
586
|
+
# 删除总量缓存文件
|
|
587
|
+
try:
|
|
588
|
+
total_file = self._get_total_file(metric_name)
|
|
589
|
+
if total_file.exists():
|
|
590
|
+
total_file.unlink()
|
|
591
|
+
except Exception:
|
|
592
|
+
pass
|
|
593
|
+
|
|
594
|
+
return True
|
|
595
|
+
|
|
596
|
+
def delete_old_data(self, days_to_keep: int = 30):
|
|
597
|
+
"""删除旧数据"""
|
|
598
|
+
cutoff_date = (datetime.now() - timedelta(days=days_to_keep)).date()
|
|
599
|
+
|
|
600
|
+
# 遍历数据目录中的所有文件
|
|
601
|
+
for data_file in self.data_dir.glob("stats_*.json"):
|
|
602
|
+
try:
|
|
603
|
+
# 从文件名中提取日期
|
|
604
|
+
date_str = data_file.stem.replace("stats_", "")
|
|
605
|
+
file_date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
606
|
+
|
|
607
|
+
# 如果文件日期早于截止日期,删除文件
|
|
608
|
+
if file_date < cutoff_date:
|
|
609
|
+
data_file.unlink()
|
|
610
|
+
except (ValueError, OSError):
|
|
611
|
+
# 忽略无法解析或删除的文件
|
|
612
|
+
continue
|