feat: 添加 Cross 模式统计报告功能
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Cross 模式统计报告功能
|
||||
|
||||
提供事件记录和统计报告生成功能,支持 Cross 和 Normal 两种模式。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import fcntl
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# 统计数据文件路径
|
||||
STATS_FILE = Path(__file__).parent / ".stats_data.json"
|
||||
|
||||
# 敏感字段列表(用于过滤)
|
||||
SENSITIVE_FIELDS = {"password", "token", "secret", "api_key", "apikey", "credential"}
|
||||
|
||||
|
||||
def _filter_sensitive_data(data: dict) -> dict:
|
||||
"""过滤敏感数据
|
||||
|
||||
递归过滤字典中的敏感字段,将值替换为 [REDACTED]
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
filtered = {}
|
||||
for key, value in data.items():
|
||||
key_lower = key.lower().replace("-", "_").replace(" ", "_")
|
||||
|
||||
# 检查是否为敏感字段
|
||||
is_sensitive = any(s in key_lower for s in SENSITIVE_FIELDS)
|
||||
|
||||
if is_sensitive:
|
||||
filtered[key] = "[REDACTED]"
|
||||
elif isinstance(value, dict):
|
||||
filtered[key] = _filter_sensitive_data(value)
|
||||
else:
|
||||
filtered[key] = value
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def _ensure_stats_file():
|
||||
"""确保统计数据文件存在,并设置正确权限"""
|
||||
if not STATS_FILE.exists():
|
||||
STATS_FILE.write_text(json.dumps({"events": []}, ensure_ascii=False, indent=2))
|
||||
os.chmod(STATS_FILE, 0o600)
|
||||
|
||||
|
||||
def _load_stats() -> dict:
|
||||
"""加载统计数据(带文件锁)"""
|
||||
_ensure_stats_file()
|
||||
|
||||
with open(STATS_FILE, "r") as f:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_SH) # 共享锁
|
||||
try:
|
||||
data = json.load(f)
|
||||
finally:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _save_stats(data: dict):
|
||||
"""保存统计数据(带文件锁)"""
|
||||
_ensure_stats_file()
|
||||
|
||||
with open(STATS_FILE, "w") as f:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_EX) # 排他锁
|
||||
try:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
finally:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
# 确保文件权限为 600
|
||||
os.chmod(STATS_FILE, 0o600)
|
||||
|
||||
|
||||
def record_event(
|
||||
event_type: str,
|
||||
mode: str,
|
||||
metadata: Optional[dict] = None,
|
||||
status: str = "success"
|
||||
) -> bool:
|
||||
"""记录一个事件
|
||||
|
||||
Args:
|
||||
event_type: 事件类型 (debate, review, audit)
|
||||
mode: 运行模式 (cross, normal)
|
||||
metadata: 事件元数据(可选)
|
||||
status: 事件状态 (success, failed)
|
||||
|
||||
Returns:
|
||||
bool: 记录是否成功
|
||||
"""
|
||||
# 验证参数
|
||||
valid_types = {"debate", "review", "audit"}
|
||||
valid_modes = {"cross", "normal"}
|
||||
valid_status = {"success", "failed"}
|
||||
|
||||
if event_type not in valid_types:
|
||||
print(f"错误: 无效的事件类型 '{event_type}',有效值: {valid_types}")
|
||||
return False
|
||||
|
||||
if mode not in valid_modes:
|
||||
print(f"错误: 无效的模式 '{mode}',有效值: {valid_modes}")
|
||||
return False
|
||||
|
||||
if status not in valid_status:
|
||||
print(f"错误: 无效的状态 '{status}',有效值: {valid_status}")
|
||||
return False
|
||||
|
||||
# 过滤敏感数据
|
||||
if metadata:
|
||||
metadata = _filter_sensitive_data(metadata)
|
||||
else:
|
||||
metadata = {}
|
||||
|
||||
# 创建事件记录
|
||||
event = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"event_type": event_type,
|
||||
"mode": mode,
|
||||
"status": status,
|
||||
"metadata": metadata
|
||||
}
|
||||
|
||||
try:
|
||||
data = _load_stats()
|
||||
data["events"].append(event)
|
||||
_save_stats(data)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"记录事件失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_stats() -> dict:
|
||||
"""获取统计汇总数据
|
||||
|
||||
Returns:
|
||||
dict: 统计汇总数据
|
||||
"""
|
||||
data = _load_stats()
|
||||
events = data.get("events", [])
|
||||
|
||||
# 初始化统计结构
|
||||
stats = {
|
||||
"total_events": len(events),
|
||||
"by_type": {
|
||||
"debate": {"total": 0, "success": 0, "failed": 0},
|
||||
"review": {"total": 0, "success": 0, "failed": 0},
|
||||
"audit": {"total": 0, "success": 0, "failed": 0}
|
||||
},
|
||||
"by_mode": {
|
||||
"cross": {"total": 0, "success": 0, "failed": 0},
|
||||
"normal": {"total": 0, "success": 0, "failed": 0}
|
||||
},
|
||||
"cross_mode_breakdown": {
|
||||
"debate": {"total": 0, "success": 0, "failed": 0},
|
||||
"review": {"total": 0, "success": 0, "failed": 0},
|
||||
"audit": {"total": 0, "success": 0, "failed": 0}
|
||||
},
|
||||
"normal_mode_breakdown": {
|
||||
"debate": {"total": 0, "success": 0, "failed": 0},
|
||||
"review": {"total": 0, "success": 0, "failed": 0},
|
||||
"audit": {"total": 0, "success": 0, "failed": 0}
|
||||
},
|
||||
"success_rate": 0.0,
|
||||
"first_event": None,
|
||||
"last_event": None
|
||||
}
|
||||
|
||||
if not events:
|
||||
return stats
|
||||
|
||||
# 统计各类数据
|
||||
for event in events:
|
||||
event_type = event["event_type"]
|
||||
mode = event["mode"]
|
||||
status = event["status"]
|
||||
|
||||
# 按类型统计
|
||||
stats["by_type"][event_type]["total"] += 1
|
||||
stats["by_type"][event_type][status] += 1
|
||||
|
||||
# 按模式统计
|
||||
stats["by_mode"][mode]["total"] += 1
|
||||
stats["by_mode"][mode][status] += 1
|
||||
|
||||
# 按模式+类型交叉统计
|
||||
breakdown_key = f"{mode}_mode_breakdown"
|
||||
stats[breakdown_key][event_type]["total"] += 1
|
||||
stats[breakdown_key][event_type][status] += 1
|
||||
|
||||
# 计算成功率
|
||||
total = stats["total_events"]
|
||||
success_count = sum(t["success"] for t in stats["by_type"].values())
|
||||
stats["success_rate"] = round(success_count / total * 100, 2) if total > 0 else 0.0
|
||||
|
||||
# 时间范围
|
||||
stats["first_event"] = events[0]["timestamp"]
|
||||
stats["last_event"] = events[-1]["timestamp"]
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def format_stats_report(stats: dict) -> str:
|
||||
"""格式化统计报告为可读字符串
|
||||
|
||||
Args:
|
||||
stats: get_stats() 返回的统计数据
|
||||
|
||||
Returns:
|
||||
str: 格式化的统计报告
|
||||
"""
|
||||
lines = []
|
||||
lines.append("=" * 50)
|
||||
lines.append(" Cross 模式统计报告")
|
||||
lines.append("=" * 50)
|
||||
|
||||
# 总览
|
||||
lines.append(f"\n📊 总览")
|
||||
lines.append(f" 总事件数: {stats['total_events']}")
|
||||
lines.append(f" 成功率: {stats['success_rate']}%")
|
||||
if stats['first_event']:
|
||||
lines.append(f" 时间范围: {stats['first_event']} ~ {stats['last_event']}")
|
||||
|
||||
# 按模式统计
|
||||
lines.append(f"\n📈 按模式统计")
|
||||
for mode, data in stats["by_mode"].items():
|
||||
mode_name = "Cross 模式" if mode == "cross" else "Normal 模式"
|
||||
rate = round(data["success"] / data["total"] * 100, 2) if data["total"] > 0 else 0
|
||||
lines.append(f" {mode_name}:")
|
||||
lines.append(f" 总数: {data['total']} | 成功: {data['success']} | 失败: {data['failed']} | 成功率: {rate}%")
|
||||
|
||||
# 按事件类型统计
|
||||
lines.append(f"\n📋 按事件类型统计")
|
||||
for event_type, data in stats["by_type"].items():
|
||||
type_name = {"debate": "辩论", "review": "评审", "audit": "审计"}.get(event_type, event_type)
|
||||
rate = round(data["success"] / data["total"] * 100, 2) if data["total"] > 0 else 0
|
||||
lines.append(f" {type_name}:")
|
||||
lines.append(f" 总数: {data['total']} | 成功: {data['success']} | 失败: {data['failed']} | 成功率: {rate}%")
|
||||
|
||||
# Cross 模式详细分解
|
||||
lines.append(f"\n🔍 Cross 模式详细分解")
|
||||
for event_type, data in stats["cross_mode_breakdown"].items():
|
||||
type_name = {"debate": "辩论", "review": "评审", "audit": "审计"}.get(event_type, event_type)
|
||||
rate = round(data["success"] / data["total"] * 100, 2) if data["total"] > 0 else 0
|
||||
lines.append(f" {type_name}:")
|
||||
lines.append(f" 总数: {data['total']} | 成功: {data['success']} | 失败: {data['failed']} | 成功率: {rate}%")
|
||||
|
||||
# Normal 模式详细分解
|
||||
lines.append(f"\n🔍 Normal 模式详细分解")
|
||||
for event_type, data in stats["normal_mode_breakdown"].items():
|
||||
type_name = {"debate": "辩论", "review": "评审", "audit": "审计"}.get(event_type, event_type)
|
||||
rate = round(data["success"] / data["total"] * 100, 2) if data["total"] > 0 else 0
|
||||
lines.append(f" {type_name}:")
|
||||
lines.append(f" 总数: {data['total']} | 成功: {data['success']} | 失败: {data['failed']} | 成功率: {rate}%")
|
||||
|
||||
lines.append("\n" + "=" * 50)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
print("测试 stats.py 模块...")
|
||||
|
||||
# 记录一些测试事件
|
||||
record_event("debate", "cross", {"topic": "AI 安全"}, "success")
|
||||
record_event("review", "normal", {"pr_id": 123}, "success")
|
||||
record_event("audit", "cross", {"password": "secret123"}, "failed") # 敏感数据会被过滤
|
||||
|
||||
# 获取并打印统计
|
||||
stats = get_stats()
|
||||
print(format_stats_report(stats))
|
||||
Reference in New Issue
Block a user