jarvis-ai-assistant 0.2.3__py3-none-any.whl → 0.2.5__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 (33) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +13 -7
  3. jarvis/jarvis_agent/edit_file_handler.py +4 -0
  4. jarvis/jarvis_agent/jarvis.py +22 -25
  5. jarvis/jarvis_agent/main.py +6 -6
  6. jarvis/jarvis_code_agent/code_agent.py +273 -11
  7. jarvis/jarvis_code_analysis/code_review.py +21 -19
  8. jarvis/jarvis_data/config_schema.json +25 -29
  9. jarvis/jarvis_git_squash/main.py +3 -3
  10. jarvis/jarvis_git_utils/git_commiter.py +32 -11
  11. jarvis/jarvis_mcp/sse_mcp_client.py +4 -6
  12. jarvis/jarvis_mcp/streamable_mcp_client.py +5 -9
  13. jarvis/jarvis_rag/retriever.py +1 -1
  14. jarvis/jarvis_smart_shell/main.py +2 -2
  15. jarvis/jarvis_stats/__init__.py +13 -0
  16. jarvis/jarvis_stats/cli.py +404 -0
  17. jarvis/jarvis_stats/stats.py +538 -0
  18. jarvis/jarvis_stats/storage.py +381 -0
  19. jarvis/jarvis_stats/visualizer.py +282 -0
  20. jarvis/jarvis_tools/cli/main.py +82 -15
  21. jarvis/jarvis_tools/registry.py +32 -16
  22. jarvis/jarvis_tools/search_web.py +3 -3
  23. jarvis/jarvis_tools/virtual_tty.py +315 -26
  24. jarvis/jarvis_utils/config.py +12 -8
  25. jarvis/jarvis_utils/git_utils.py +8 -16
  26. jarvis/jarvis_utils/methodology.py +74 -67
  27. jarvis/jarvis_utils/utils.py +468 -72
  28. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/METADATA +29 -3
  29. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/RECORD +33 -28
  30. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/entry_points.txt +2 -0
  31. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/WHEEL +0 -0
  32. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/licenses/LICENSE +0 -0
  33. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,381 @@
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
+ # 从元数据文件获取指标
244
+ meta = self._load_json(self.meta_file)
245
+ metrics_from_meta = set(meta.get("metrics", {}).keys())
246
+
247
+ # 扫描所有数据文件获取实际存在的指标
248
+ metrics_from_data = set()
249
+ for data_file in self.data_dir.glob("stats_*.json"):
250
+ try:
251
+ data = self._load_json(data_file)
252
+ metrics_from_data.update(data.keys())
253
+ except (json.JSONDecodeError, OSError):
254
+ # 忽略无法读取的文件
255
+ continue
256
+
257
+ # 合并两个来源的指标并返回排序后的列表
258
+ all_metrics = metrics_from_meta.union(metrics_from_data)
259
+ return sorted(list(all_metrics))
260
+
261
+ def aggregate_metrics(
262
+ self,
263
+ metric_name: str,
264
+ start_time: Optional[datetime] = None,
265
+ end_time: Optional[datetime] = None,
266
+ aggregation: str = "hourly",
267
+ tags: Optional[Dict[str, str]] = None,
268
+ ) -> Dict[str, Dict[str, Any]]:
269
+ """
270
+ 聚合统计数据
271
+
272
+ Args:
273
+ metric_name: 指标名称
274
+ start_time: 开始时间
275
+ end_time: 结束时间
276
+ aggregation: 聚合方式 (hourly, daily)
277
+ tags: 过滤标签
278
+
279
+ Returns:
280
+ 聚合后的数据字典
281
+ """
282
+ records = self.get_metrics(metric_name, start_time, end_time, tags)
283
+
284
+ if not records:
285
+ return {}
286
+
287
+ # 聚合数据
288
+ aggregated = defaultdict(
289
+ lambda: {
290
+ "count": 0,
291
+ "sum": 0,
292
+ "min": float("inf"),
293
+ "max": float("-inf"),
294
+ "values": [],
295
+ }
296
+ )
297
+
298
+ for record in records:
299
+ timestamp = datetime.fromisoformat(record["timestamp"])
300
+ value = record["value"]
301
+
302
+ if aggregation == "hourly":
303
+ key = timestamp.strftime("%Y-%m-%d %H:00")
304
+ elif aggregation == "daily":
305
+ key = timestamp.strftime("%Y-%m-%d")
306
+ else:
307
+ key = timestamp.strftime("%Y-%m-%d %H:00")
308
+
309
+ aggregated[key]["count"] += 1
310
+ aggregated[key]["sum"] += value
311
+ aggregated[key]["min"] = min(aggregated[key]["min"], value)
312
+ aggregated[key]["max"] = max(aggregated[key]["max"], value)
313
+ aggregated[key]["values"].append(value)
314
+
315
+ # 计算平均值
316
+ result = {}
317
+ for key, stats in aggregated.items():
318
+ result[key] = {
319
+ "count": stats["count"],
320
+ "sum": stats["sum"],
321
+ "min": stats["min"],
322
+ "max": stats["max"],
323
+ "avg": stats["sum"] / stats["count"] if stats["count"] > 0 else 0,
324
+ }
325
+
326
+ return result
327
+
328
+ def delete_metric(self, metric_name: str) -> bool:
329
+ """
330
+ 删除指定的指标及其所有数据
331
+
332
+ Args:
333
+ metric_name: 要删除的指标名称
334
+
335
+ Returns:
336
+ True 如果成功删除,False 如果指标不存在
337
+ """
338
+ # 检查指标是否存在
339
+ meta = self._load_json(self.meta_file)
340
+ if metric_name not in meta.get("metrics", {}):
341
+ return False
342
+
343
+ # 从元数据中删除指标
344
+ del meta["metrics"][metric_name]
345
+ self._save_json(self.meta_file, meta)
346
+
347
+ # 遍历所有数据文件,删除该指标的数据
348
+ for data_file in self.data_dir.glob("stats_*.json"):
349
+ try:
350
+ data = self._load_json(data_file)
351
+ if metric_name in data:
352
+ del data[metric_name]
353
+ # 如果文件中还有其他数据,保存更新后的文件
354
+ if data:
355
+ self._save_json(data_file, data)
356
+ # 如果文件变空了,删除文件
357
+ else:
358
+ data_file.unlink()
359
+ except Exception:
360
+ # 忽略单个文件的错误,继续处理其他文件
361
+ pass
362
+
363
+ return True
364
+
365
+ def delete_old_data(self, days_to_keep: int = 30):
366
+ """删除旧数据"""
367
+ cutoff_date = (datetime.now() - timedelta(days=days_to_keep)).date()
368
+
369
+ # 遍历数据目录中的所有文件
370
+ for data_file in self.data_dir.glob("stats_*.json"):
371
+ try:
372
+ # 从文件名中提取日期
373
+ date_str = data_file.stem.replace("stats_", "")
374
+ file_date = datetime.strptime(date_str, "%Y-%m-%d").date()
375
+
376
+ # 如果文件日期早于截止日期,删除文件
377
+ if file_date < cutoff_date:
378
+ data_file.unlink()
379
+ except (ValueError, OSError):
380
+ # 忽略无法解析或删除的文件
381
+ continue
@@ -0,0 +1,282 @@
1
+ """
2
+ 统计数据可视化模块
3
+
4
+ 提供终端图形化展示功能
5
+ """
6
+
7
+ import os
8
+ import io
9
+ from typing import Dict, List, Optional, Any
10
+ from collections import OrderedDict
11
+ import plotext as plt
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+ from rich.panel import Panel
15
+ from rich import box
16
+
17
+
18
+ class StatsVisualizer:
19
+ """统计数据可视化类"""
20
+
21
+ def __init__(self, width: Optional[int] = None, height: Optional[int] = None):
22
+ """
23
+ 初始化可视化器
24
+
25
+ Args:
26
+ width: 图表宽度,默认为终端宽度-10
27
+ height: 图表高度,默认为20
28
+ """
29
+ self.width = width or self._get_terminal_width() - 10
30
+ self.height = height or 20
31
+
32
+ # 确保最小尺寸
33
+ self.width = max(self.width, 40)
34
+ self.height = max(self.height, 10)
35
+
36
+ # 初始化Rich Console
37
+ self.console = Console()
38
+
39
+ def _get_terminal_width(self) -> int:
40
+ """获取终端宽度"""
41
+ try:
42
+ columns = os.get_terminal_size().columns
43
+ return columns
44
+ except:
45
+ return 80
46
+
47
+ def plot_line_chart(
48
+ self,
49
+ data: Dict[str, float],
50
+ title: str = "",
51
+ unit: Optional[str] = None,
52
+ show_values: bool = True,
53
+ ) -> str:
54
+ """
55
+ 使用 plotext 绘制折线图
56
+ """
57
+ if not data:
58
+ return "无数据可显示"
59
+
60
+ sorted_data = OrderedDict(sorted(data.items()))
61
+ labels = list(sorted_data.keys())
62
+ values = list(sorted_data.values())
63
+
64
+ plt.clf()
65
+ plt.plotsize(self.width, self.height)
66
+ plt.plot(values)
67
+ plt.xticks(range(len(labels)), labels)
68
+ if title:
69
+ plt.title(title)
70
+ if unit:
71
+ plt.ylabel(unit)
72
+
73
+ chart = plt.build()
74
+
75
+ if show_values and values:
76
+ min_val = min(values)
77
+ max_val = max(values)
78
+ avg_val = sum(values) / len(values)
79
+ stats_info_text = (
80
+ f"最小值: {min_val:.2f}, 最大值: {max_val:.2f}, 平均值: {avg_val:.2f}"
81
+ )
82
+
83
+ # 使用StringIO捕获Panel输出
84
+ string_io = io.StringIO()
85
+ temp_console = Console(file=string_io, width=self.width)
86
+ temp_console.print(
87
+ Panel(
88
+ stats_info_text,
89
+ title="[bold]数据统计[/bold]",
90
+ expand=False,
91
+ style="dim",
92
+ border_style="blue",
93
+ )
94
+ )
95
+ stats_panel_str = string_io.getvalue()
96
+
97
+ return chart + "\n" + stats_panel_str.strip()
98
+ return chart
99
+
100
+ def plot_bar_chart(
101
+ self,
102
+ data: Dict[str, float],
103
+ title: str = "",
104
+ unit: Optional[str] = None,
105
+ horizontal: bool = False,
106
+ ) -> str:
107
+ """
108
+ 使用 plotext 绘制柱状图
109
+ """
110
+ if not data:
111
+ return "无数据可显示"
112
+
113
+ labels = list(data.keys())
114
+ values = list(data.values())
115
+
116
+ plt.clf()
117
+ plt.plotsize(self.width, self.height)
118
+
119
+ if horizontal:
120
+ plt.bar(labels, values, orientation="horizontal")
121
+ else:
122
+ plt.bar(labels, values)
123
+ if title:
124
+ plt.title(title)
125
+ if unit:
126
+ plt.ylabel(unit)
127
+
128
+ return plt.build()
129
+
130
+ def show_summary(
131
+ self,
132
+ aggregated_data: Dict[str, Dict[str, Any]],
133
+ metric_name: str,
134
+ unit: Optional[str] = None,
135
+ tags_filter: Optional[Dict[str, str]] = None,
136
+ ) -> str:
137
+ """
138
+ 显示数据摘要
139
+
140
+ Args:
141
+ aggregated_data: 聚合后的数据
142
+ metric_name: 指标名称
143
+ unit: 单位
144
+ tags_filter: 标签过滤条件
145
+
146
+ Returns:
147
+ 摘要字符串(用于兼容性,实际会直接打印)
148
+ """
149
+ if not aggregated_data:
150
+ self.console.print("[yellow]无数据可显示[/yellow]")
151
+ return "无数据可显示"
152
+
153
+ # 创建表格
154
+ table = Table(title=f"{metric_name} 统计摘要", box=box.ROUNDED)
155
+
156
+ # 添加列
157
+ table.add_column("时间", justify="center", style="cyan")
158
+ table.add_column("计数", justify="right", style="green")
159
+ table.add_column("总和", justify="right", style="yellow")
160
+ table.add_column("平均", justify="right", style="yellow")
161
+ table.add_column("最小", justify="right", style="blue")
162
+ table.add_column("最大", justify="right", style="red")
163
+
164
+ # 添加数据行
165
+ for time_key, stats in sorted(aggregated_data.items()):
166
+ table.add_row(
167
+ time_key,
168
+ str(stats["count"]),
169
+ f"{stats['sum']:.2f}",
170
+ f"{stats['avg']:.2f}",
171
+ f"{stats['min']:.2f}",
172
+ f"{stats['max']:.2f}",
173
+ )
174
+
175
+ # 显示表格
176
+ self.console.print(table)
177
+
178
+ # 显示单位信息
179
+ if unit:
180
+ self.console.print(f"\n[dim]单位: {unit}[/dim]")
181
+
182
+ # 显示过滤条件
183
+ if tags_filter:
184
+ filter_str = ", ".join([f"{k}={v}" for k, v in tags_filter.items()])
185
+ self.console.print(f"[dim]过滤条件: {filter_str}[/dim]")
186
+
187
+ return "" # 返回空字符串,实际输出已经通过console打印
188
+
189
+ def show_table(
190
+ self,
191
+ records: List[Dict[str, Any]],
192
+ metric_name: str,
193
+ unit: Optional[str] = None,
194
+ start_time: Optional[str] = None,
195
+ end_time: Optional[str] = None,
196
+ tags_filter: Optional[Dict[str, str]] = None,
197
+ ) -> str:
198
+ """
199
+ 使用Rich Table显示数据记录
200
+
201
+ Args:
202
+ records: 数据记录列表
203
+ metric_name: 指标名称
204
+ unit: 单位
205
+ start_time: 开始时间
206
+ end_time: 结束时间
207
+ tags_filter: 标签过滤条件
208
+
209
+ Returns:
210
+ 空字符串(实际通过console打印)
211
+ """
212
+ if not records:
213
+ self.console.print(f"[yellow]没有找到指标 '{metric_name}' 的数据[/yellow]")
214
+ return ""
215
+
216
+ # 创建表格
217
+ table = Table(title=f"指标: {metric_name}", box=box.ROUNDED)
218
+
219
+ # 添加列
220
+ table.add_column("时间", style="cyan", no_wrap=True)
221
+ table.add_column("值", justify="right", style="yellow")
222
+ table.add_column("标签", style="dim")
223
+
224
+ # 只显示最近的20条记录
225
+ display_records = records[-20:] if len(records) > 20 else records
226
+
227
+ # 添加数据行
228
+ from datetime import datetime
229
+
230
+ for record in display_records:
231
+ timestamp = datetime.fromisoformat(record["timestamp"])
232
+ time_str = timestamp.strftime("%Y-%m-%d %H:%M:%S")
233
+ value = f"{record['value']:.2f}"
234
+ tags_str = ", ".join(f"{k}={v}" for k, v in record.get("tags", {}).items())
235
+
236
+ table.add_row(time_str, value, tags_str)
237
+
238
+ # 显示表格
239
+ self.console.print(table)
240
+
241
+ # 显示元信息
242
+ info_items = []
243
+ if unit:
244
+ info_items.append(f"单位: {unit}")
245
+ if start_time and end_time:
246
+ info_items.append(f"时间范围: [cyan]{start_time}[/] ~ [cyan]{end_time}[/]")
247
+ if tags_filter:
248
+ filter_str = ", ".join([f"{k}={v}" for k, v in tags_filter.items()])
249
+ info_items.append(f"过滤条件: {filter_str}")
250
+
251
+ if info_items:
252
+ self.console.print(
253
+ Panel(
254
+ " | ".join(info_items),
255
+ title="[bold]查询详情[/bold]",
256
+ expand=False,
257
+ style="dim",
258
+ border_style="green",
259
+ )
260
+ )
261
+
262
+ # 统计信息
263
+ if len(records) > 0:
264
+ values = [r["value"] for r in records]
265
+ stats_info_text = (
266
+ f"总记录数: {len(records)} | "
267
+ f"显示: {len(display_records)} | "
268
+ f"最小值: {min(values):.2f} | "
269
+ f"最大值: {max(values):.2f} | "
270
+ f"平均值: {sum(values)/len(values):.2f}"
271
+ )
272
+ self.console.print(
273
+ Panel(
274
+ stats_info_text,
275
+ title="[bold]数据统计[/bold]",
276
+ expand=False,
277
+ style="dim",
278
+ border_style="blue",
279
+ )
280
+ )
281
+
282
+ return ""