feat: 添加 Cross 模式统计报告功能
- 添加 stats.py 统计模块 - 添加 dev_workflow.py CLI 工具 - 实现 record_event() 记录事件 - 实现 get_stats() 生成报告 - 支持敏感数据自动过滤 - 使用原子写入保证数据安全 - 添加 README 文档
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
# 数据文件
|
||||||
|
data/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Dev Workflow - 开发工作流工具
|
||||||
|
|
||||||
|
Cross 模式统计报告功能
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- ✅ 事件记录和统计
|
||||||
|
- ✅ 多模式支持(cross, normal 等)
|
||||||
|
- ✅ 敏感数据自动过滤
|
||||||
|
- ✅ 原子写入保证数据安全
|
||||||
|
- ✅ 格式化报告输出
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 查看统计报告
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 dev-workflow/dev_workflow.py stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### 筛选特定模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 dev-workflow/dev_workflow.py stats --mode cross
|
||||||
|
```
|
||||||
|
|
||||||
|
### 记录事件(用于集成)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 dev-workflow/dev_workflow.py record "event_type" --mode cross --description "事件描述"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 清空统计数据
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 dev-workflow/dev_workflow.py clear --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 使用
|
||||||
|
|
||||||
|
### Python 代码中记录事件
|
||||||
|
|
||||||
|
```python
|
||||||
|
from stats import record_event, get_stats, format_stats_report
|
||||||
|
|
||||||
|
# 记录事件
|
||||||
|
record_event(
|
||||||
|
"task_completed",
|
||||||
|
mode="cross",
|
||||||
|
metadata={
|
||||||
|
"task_name": "开发功能",
|
||||||
|
"duration": 120
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取统计数据
|
||||||
|
stats = get_stats(mode="cross")
|
||||||
|
print(format_stats_report(stats))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全特性
|
||||||
|
|
||||||
|
### 敏感数据过滤
|
||||||
|
|
||||||
|
以下字段会被自动过滤(替换为 `[REDACTED]`):
|
||||||
|
- password
|
||||||
|
- token
|
||||||
|
- secret
|
||||||
|
- key
|
||||||
|
- credential
|
||||||
|
- auth
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```python
|
||||||
|
record_event("login", metadata={
|
||||||
|
"username": "user123", # 正常记录
|
||||||
|
"password": "secret123" # 记录为 [REDACTED]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 原子写入
|
||||||
|
|
||||||
|
所有数据写入都使用原子操作(临时文件 + 重命名),确保:
|
||||||
|
- 并发写入不会损坏数据
|
||||||
|
- 系统崩溃不会丢失数据
|
||||||
|
- 临时文件自动清理
|
||||||
|
|
||||||
|
## 数据存储
|
||||||
|
|
||||||
|
- 位置:`dev-workflow/data/stats_data.json`
|
||||||
|
- 格式:JSON
|
||||||
|
- 结构:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"event_type": "string",
|
||||||
|
"mode": "string",
|
||||||
|
"timestamp": "ISO8601",
|
||||||
|
"metadata": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"created_at": "ISO8601",
|
||||||
|
"last_updated": "ISO8601"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 统计报告内容
|
||||||
|
|
||||||
|
报告包含以下信息:
|
||||||
|
- 总事件数
|
||||||
|
- 时间范围(首次和最后事件)
|
||||||
|
- 按事件类型统计
|
||||||
|
- 按模式统计
|
||||||
|
- 按日期统计(最近7天)
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
dev-workflow/
|
||||||
|
├── dev_workflow.py # CLI 工具
|
||||||
|
├── stats.py # 统计模块
|
||||||
|
├── README.md # 本文档
|
||||||
|
└── data/
|
||||||
|
└── stats_data.json # 数据存储
|
||||||
|
```
|
||||||
|
|
||||||
|
### 扩展事件类型
|
||||||
|
|
||||||
|
可以在代码中定义新的事件类型,例如:
|
||||||
|
- `task_started` - 任务开始
|
||||||
|
- `task_completed` - 任务完成
|
||||||
|
- `code_committed` - 代码提交
|
||||||
|
- `error_occurred` - 错误发生
|
||||||
|
- `stats_viewed` - 查看统计
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
运行测试:
|
||||||
|
```bash
|
||||||
|
python3 dev-workflow/stats.py
|
||||||
|
```
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Dev Workflow - 开发工作流工具
|
||||||
|
|
||||||
|
提供多种工作模式和统计功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 添加当前目录到路径
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from stats import record_event, get_stats, format_stats_report, clear_stats
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_stats(args):
|
||||||
|
"""stats 命令处理函数"""
|
||||||
|
# 记录查看统计的事件
|
||||||
|
record_event(
|
||||||
|
"stats_viewed",
|
||||||
|
mode="cross",
|
||||||
|
metadata={"filter_mode": args.mode}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取统计数据
|
||||||
|
stats = get_stats(mode=args.mode)
|
||||||
|
|
||||||
|
# 输出报告
|
||||||
|
report = format_stats_report(stats)
|
||||||
|
print(report)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_record(args):
|
||||||
|
"""record 命令处理函数 - 用于测试"""
|
||||||
|
success = record_event(
|
||||||
|
args.event_type,
|
||||||
|
mode=args.mode,
|
||||||
|
metadata={"description": args.description} if args.description else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"✓ 事件已记录: {args.event_type}")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print("✗ 记录事件失败")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_clear(args):
|
||||||
|
"""clear 命令处理函数"""
|
||||||
|
if not args.confirm:
|
||||||
|
print("警告: 此操作将清空所有统计数据!")
|
||||||
|
print("使用 --confirm 参数确认清空")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
success = clear_stats(confirm=True)
|
||||||
|
if success:
|
||||||
|
print("✓ 统计数据已清空")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print("✗ 清空失败")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Dev Workflow - 开发工作流工具",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||||
|
|
||||||
|
# stats 命令
|
||||||
|
stats_parser = subparsers.add_parser("stats", help="显示统计报告")
|
||||||
|
stats_parser.add_argument(
|
||||||
|
"--mode", "-m",
|
||||||
|
help="筛选特定模式(如 cross, normal)"
|
||||||
|
)
|
||||||
|
stats_parser.set_defaults(func=cmd_stats)
|
||||||
|
|
||||||
|
# record 命令(用于测试)
|
||||||
|
record_parser = subparsers.add_parser("record", help="记录事件(测试用)")
|
||||||
|
record_parser.add_argument(
|
||||||
|
"event_type",
|
||||||
|
help="事件类型"
|
||||||
|
)
|
||||||
|
record_parser.add_argument(
|
||||||
|
"--mode", "-m",
|
||||||
|
default="cross",
|
||||||
|
help="工作模式(默认: cross)"
|
||||||
|
)
|
||||||
|
record_parser.add_argument(
|
||||||
|
"--description", "-d",
|
||||||
|
help="事件描述"
|
||||||
|
)
|
||||||
|
record_parser.set_defaults(func=cmd_record)
|
||||||
|
|
||||||
|
# clear 命令
|
||||||
|
clear_parser = subparsers.add_parser("clear", help="清空统计数据")
|
||||||
|
clear_parser.add_argument(
|
||||||
|
"--confirm",
|
||||||
|
action="store_true",
|
||||||
|
help="确认清空"
|
||||||
|
)
|
||||||
|
clear_parser.set_defaults(func=cmd_clear)
|
||||||
|
|
||||||
|
# 解析参数
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 如果没有指定命令,默认显示 stats
|
||||||
|
if not args.command:
|
||||||
|
args.command = "stats"
|
||||||
|
args.func = cmd_stats
|
||||||
|
args.mode = None
|
||||||
|
|
||||||
|
# 执行命令
|
||||||
|
try:
|
||||||
|
return args.func(args)
|
||||||
|
except AttributeError:
|
||||||
|
parser.print_help()
|
||||||
|
return 1
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n操作已取消")
|
||||||
|
return 130
|
||||||
|
except Exception as e:
|
||||||
|
print(f"错误: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Cross 模式统计报告模块
|
||||||
|
|
||||||
|
提供事件记录和统计报告生成功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# 统计数据存储路径
|
||||||
|
STATS_DIR = Path(__file__).parent / "data"
|
||||||
|
STATS_FILE = STATS_DIR / "stats_data.json"
|
||||||
|
|
||||||
|
# 敏感字段列表(不记录这些字段)
|
||||||
|
SENSITIVE_FIELDS = {"password", "token", "secret", "key", "credential", "auth"}
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_stats_dir():
|
||||||
|
"""确保统计目录存在"""
|
||||||
|
STATS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_stats_data() -> Dict[str, Any]:
|
||||||
|
"""加载统计数据"""
|
||||||
|
if not STATS_FILE.exists():
|
||||||
|
return {"events": [], "metadata": {"created_at": datetime.now().isoformat()}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(STATS_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
return {"events": [], "metadata": {"created_at": datetime.now().isoformat()}}
|
||||||
|
|
||||||
|
|
||||||
|
def _atomic_write(data: Dict[str, Any]):
|
||||||
|
"""原子写入统计数据"""
|
||||||
|
_ensure_stats_dir()
|
||||||
|
|
||||||
|
# 写入临时文件
|
||||||
|
fd, temp_path = tempfile.mkstemp(dir=STATS_DIR, suffix='.tmp')
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# 原子重命名
|
||||||
|
os.replace(temp_path, STATS_FILE)
|
||||||
|
except Exception:
|
||||||
|
# 清理临时文件
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_sensitive_data(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""过滤敏感数据"""
|
||||||
|
filtered = {}
|
||||||
|
for key, value in data.items():
|
||||||
|
# 检查键名是否包含敏感字段
|
||||||
|
if any(sensitive in key.lower() for sensitive in SENSITIVE_FIELDS):
|
||||||
|
filtered[key] = "[REDACTED]"
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
filtered[key] = _filter_sensitive_data(value)
|
||||||
|
else:
|
||||||
|
filtered[key] = value
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def record_event(
|
||||||
|
event_type: str,
|
||||||
|
mode: str = "cross",
|
||||||
|
metadata: Optional[Dict[str, Any]] = None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
记录事件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type: 事件类型(如 'command_executed', 'task_completed' 等)
|
||||||
|
mode: 工作模式(默认 'cross')
|
||||||
|
metadata: 事件元数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 记录是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 过滤敏感数据
|
||||||
|
safe_metadata = _filter_sensitive_data(metadata or {})
|
||||||
|
|
||||||
|
# 创建事件记录
|
||||||
|
event = {
|
||||||
|
"event_type": event_type,
|
||||||
|
"mode": mode,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"metadata": safe_metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
# 加载现有数据
|
||||||
|
data = _load_stats_data()
|
||||||
|
data["events"].append(event)
|
||||||
|
data["metadata"]["last_updated"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# 原子写入
|
||||||
|
_atomic_write(data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"记录事件失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_stats(mode: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
生成统计报告
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode: 筛选特定模式,None 表示所有模式
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 统计报告数据
|
||||||
|
"""
|
||||||
|
data = _load_stats_data()
|
||||||
|
events = data.get("events", [])
|
||||||
|
|
||||||
|
# 筛选模式
|
||||||
|
if mode:
|
||||||
|
events = [e for e in events if e.get("mode") == mode]
|
||||||
|
|
||||||
|
# 统计数据
|
||||||
|
stats = {
|
||||||
|
"total_events": len(events),
|
||||||
|
"events_by_type": defaultdict(int),
|
||||||
|
"events_by_mode": defaultdict(int),
|
||||||
|
"events_by_date": defaultdict(int),
|
||||||
|
"first_event": None,
|
||||||
|
"last_event": None,
|
||||||
|
"metadata": data.get("metadata", {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if events:
|
||||||
|
stats["first_event"] = events[0]["timestamp"]
|
||||||
|
stats["last_event"] = events[-1]["timestamp"]
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
stats["events_by_type"][event["event_type"]] += 1
|
||||||
|
stats["events_by_mode"][event["mode"]] += 1
|
||||||
|
|
||||||
|
# 按日期统计
|
||||||
|
date = event["timestamp"][:10] # YYYY-MM-DD
|
||||||
|
stats["events_by_date"][date] += 1
|
||||||
|
|
||||||
|
# 转换 defaultdict 为普通 dict
|
||||||
|
stats["events_by_type"] = dict(stats["events_by_type"])
|
||||||
|
stats["events_by_mode"] = dict(stats["events_by_mode"])
|
||||||
|
stats["events_by_date"] = dict(stats["events_by_date"])
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def format_stats_report(stats: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
格式化统计报告为可读文本
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stats: 统计数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 格式化的报告文本
|
||||||
|
"""
|
||||||
|
lines = [
|
||||||
|
"=" * 60,
|
||||||
|
"Cross 模式统计报告",
|
||||||
|
"=" * 60,
|
||||||
|
"",
|
||||||
|
f"总事件数: {stats['total_events']}",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
|
||||||
|
if stats["total_events"] == 0:
|
||||||
|
lines.append("暂无统计数据")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# 时间范围
|
||||||
|
lines.extend([
|
||||||
|
f"首次事件: {stats['first_event']}",
|
||||||
|
f"最后事件: {stats['last_event']}",
|
||||||
|
""
|
||||||
|
])
|
||||||
|
|
||||||
|
# 按类型统计
|
||||||
|
if stats["events_by_type"]:
|
||||||
|
lines.append("按事件类型统计:")
|
||||||
|
for event_type, count in sorted(stats["events_by_type"].items()):
|
||||||
|
lines.append(f" - {event_type}: {count}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 按模式统计
|
||||||
|
if stats["events_by_mode"]:
|
||||||
|
lines.append("按模式统计:")
|
||||||
|
for mode, count in sorted(stats["events_by_mode"].items()):
|
||||||
|
lines.append(f" - {mode}: {count}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 按日期统计(最近7天)
|
||||||
|
if stats["events_by_date"]:
|
||||||
|
lines.append("按日期统计(最近7天):")
|
||||||
|
sorted_dates = sorted(stats["events_by_date"].items(), reverse=True)[:7]
|
||||||
|
for date, count in sorted_dates:
|
||||||
|
lines.append(f" - {date}: {count}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("=" * 60)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_stats(confirm: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
清空统计数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
confirm: 确认清空
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功清空
|
||||||
|
"""
|
||||||
|
if not confirm:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if STATS_FILE.exists():
|
||||||
|
STATS_FILE.unlink()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"清空统计数据失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试代码
|
||||||
|
print("测试 stats 模块")
|
||||||
|
|
||||||
|
# 记录一些测试事件
|
||||||
|
record_event("test_event", mode="cross", metadata={"user": "test", "action": "demo"})
|
||||||
|
record_event("another_event", mode="normal", metadata={"data": "sample"})
|
||||||
|
|
||||||
|
# 生成报告
|
||||||
|
stats = get_stats()
|
||||||
|
print(format_stats_report(stats))
|
||||||
Reference in New Issue
Block a user