- 4维指纹采集: 性能/语言/能力/行为 - models.py 已加入 IdentityFingerprintModel (第5维数据模型) - comparator.py 已升级为5维评分 (含identity维度比较) - reporter.py 已加入身份验证报告输出 - main.py 已集成identity采集流程 - identity collector 待下次提交补充完整代码
392 lines
14 KiB
Python
392 lines
14 KiB
Python
"""Report generator — Rich terminal tables + JSON export."""
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
from rich.panel import Panel
|
|
from rich.text import Text
|
|
from rich import box
|
|
|
|
from core.models import ComparisonResult, FullFingerprint
|
|
|
|
|
|
console = Console()
|
|
|
|
|
|
def print_report(result: ComparisonResult,
|
|
genuine_fp: FullFingerprint,
|
|
suspect_fp: FullFingerprint) -> None:
|
|
"""Print a beautiful terminal report using Rich."""
|
|
|
|
console.print()
|
|
console.print(Panel(
|
|
"[bold cyan]AI API 指纹检测对比报告[/bold cyan]\n"
|
|
"[dim]Fingerprint Comparison Report[/dim]",
|
|
box=box.DOUBLE,
|
|
expand=False,
|
|
padding=(1, 4),
|
|
))
|
|
console.print()
|
|
|
|
# Channel info
|
|
info_table = Table(box=box.SIMPLE_HEAVY, show_header=True,
|
|
title="📡 渠道信息 / Channel Info")
|
|
info_table.add_column("", style="bold")
|
|
info_table.add_column("基准渠道 (Genuine)", style="green")
|
|
info_table.add_column("待检测渠道 (Suspect)", style="yellow")
|
|
|
|
info_table.add_row("Name", genuine_fp.channel_name, suspect_fp.channel_name)
|
|
info_table.add_row("Timestamp", genuine_fp.timestamp, suspect_fp.timestamp)
|
|
|
|
console.print(info_table)
|
|
console.print()
|
|
|
|
# Performance summary
|
|
_print_performance_summary(genuine_fp, suspect_fp)
|
|
console.print()
|
|
|
|
# Identity verification summary
|
|
_print_identity_summary(genuine_fp, suspect_fp)
|
|
console.print()
|
|
|
|
# Dimension scores table
|
|
score_table = Table(
|
|
box=box.ROUNDED,
|
|
show_header=True,
|
|
title="📊 维度评分 / Dimension Scores",
|
|
title_style="bold",
|
|
)
|
|
score_table.add_column("维度 Dimension", style="bold cyan", min_width=14)
|
|
score_table.add_column("得分 Score", justify="center", min_width=10)
|
|
score_table.add_column("权重 Weight", justify="center", min_width=10)
|
|
score_table.add_column("加权分 Weighted", justify="center", min_width=10)
|
|
score_table.add_column("详情 Details", min_width=30)
|
|
|
|
for ds in result.dimension_scores:
|
|
score_val = ds.score
|
|
if score_val >= 0.80:
|
|
score_str = f"[green]{score_val:.3f}[/green]"
|
|
elif score_val >= 0.60:
|
|
score_str = f"[yellow]{score_val:.3f}[/yellow]"
|
|
else:
|
|
score_str = f"[red]{score_val:.3f}[/red]"
|
|
|
|
weighted = ds.score * ds.weight
|
|
weighted_str = f"{weighted:.3f}"
|
|
weight_str = f"{ds.weight:.2f}"
|
|
|
|
# Format details
|
|
detail_parts = []
|
|
for k, v in ds.details.items():
|
|
if k == "component_scores":
|
|
continue
|
|
if isinstance(v, float):
|
|
detail_parts.append(f"{k}: {v:.3f}")
|
|
else:
|
|
detail_parts.append(f"{k}: {v}")
|
|
details_str = "\n".join(detail_parts[:5])
|
|
|
|
score_table.add_row(ds.dimension, score_str, weight_str, weighted_str, details_str)
|
|
|
|
console.print(score_table)
|
|
console.print()
|
|
|
|
# Overall result panel
|
|
overall = result.overall_score
|
|
verdict = result.verdict
|
|
|
|
if "GENUINE" in verdict:
|
|
style = "bold green"
|
|
border_style = "green"
|
|
elif "SUSPICIOUS" in verdict:
|
|
style = "bold yellow"
|
|
border_style = "yellow"
|
|
else:
|
|
style = "bold red"
|
|
border_style = "red"
|
|
|
|
result_text = Text()
|
|
result_text.append(f"总分 Overall Score: {overall:.3f}\n\n", style="bold")
|
|
result_text.append(f"判定 Verdict: {verdict}\n\n", style=style)
|
|
result_text.append("阈值 Thresholds: ", style="dim")
|
|
result_text.append("≥0.80 ✅ GENUINE", style="green")
|
|
result_text.append(" | ", style="dim")
|
|
result_text.append("≥0.60 ⚠️ SUSPICIOUS", style="yellow")
|
|
result_text.append(" | ", style="dim")
|
|
result_text.append("<0.60 ❌ LIKELY FAKE", style="red")
|
|
|
|
console.print(Panel(
|
|
result_text,
|
|
title="🎯 最终判定 / Final Verdict",
|
|
border_style=border_style,
|
|
box=box.HEAVY,
|
|
expand=False,
|
|
padding=(1, 4),
|
|
))
|
|
console.print()
|
|
|
|
|
|
def _print_performance_summary(genuine_fp: FullFingerprint,
|
|
suspect_fp: FullFingerprint) -> None:
|
|
"""Print performance metrics comparison."""
|
|
perf_table = Table(
|
|
box=box.SIMPLE,
|
|
show_header=True,
|
|
title="⚡ 性能对比 / Performance Comparison",
|
|
)
|
|
perf_table.add_column("指标 Metric", style="bold")
|
|
perf_table.add_column("基准 Genuine", justify="right", style="green")
|
|
perf_table.add_column("待检 Suspect", justify="right", style="yellow")
|
|
|
|
gp = genuine_fp.performance
|
|
sp = suspect_fp.performance
|
|
|
|
perf_table.add_row(
|
|
"P50 Latency (ms)",
|
|
f"{gp.p50_latency_ms:.1f}",
|
|
f"{sp.p50_latency_ms:.1f}",
|
|
)
|
|
perf_table.add_row(
|
|
"P95 Latency (ms)",
|
|
f"{gp.p95_latency_ms:.1f}",
|
|
f"{sp.p95_latency_ms:.1f}",
|
|
)
|
|
perf_table.add_row(
|
|
"Avg TTFT (ms)",
|
|
f"{gp.avg_ttft_ms:.1f}",
|
|
f"{sp.avg_ttft_ms:.1f}",
|
|
)
|
|
perf_table.add_row(
|
|
"Avg TPS",
|
|
f"{gp.avg_tps:.1f}",
|
|
f"{sp.avg_tps:.1f}",
|
|
)
|
|
perf_table.add_row(
|
|
"Avg Response Len",
|
|
f"{gp.avg_response_length:.0f}",
|
|
f"{sp.avg_response_length:.0f}",
|
|
)
|
|
|
|
console.print(perf_table)
|
|
|
|
|
|
def _print_identity_summary(genuine_fp: FullFingerprint,
|
|
suspect_fp: FullFingerprint) -> None:
|
|
"""Print identity verification comparison."""
|
|
gi = genuine_fp.identity
|
|
si = suspect_fp.identity
|
|
|
|
# Identity overview table
|
|
id_table = Table(
|
|
box=box.ROUNDED,
|
|
show_header=True,
|
|
title="🆔 模型身份验证 / Model Identity Verification",
|
|
title_style="bold",
|
|
)
|
|
id_table.add_column("检测项 Check", style="bold")
|
|
id_table.add_column("基准 Genuine", justify="center", style="green")
|
|
id_table.add_column("待检 Suspect", justify="center", style="yellow")
|
|
id_table.add_column("状态 Status", justify="center")
|
|
|
|
# Claimed identity
|
|
g_claim = gi.claimed_identity or "unknown"
|
|
s_claim = si.claimed_identity or "unknown"
|
|
claim_status = "[green]✓ 一致[/green]" if g_claim == s_claim else "[red]✗ 不一致[/red]"
|
|
id_table.add_row("声称身份 / Claimed Model", g_claim, s_claim, claim_status)
|
|
|
|
# Claimed developer
|
|
g_dev = gi.claimed_developer or "unknown"
|
|
s_dev = si.claimed_developer or "unknown"
|
|
dev_status = "[green]✓ 一致[/green]" if g_dev == s_dev else "[red]✗ 不一致[/red]"
|
|
id_table.add_row("声称开发者 / Developer", g_dev, s_dev, dev_status)
|
|
|
|
# Detected model
|
|
g_det = gi.detected_model or "unknown"
|
|
s_det = si.detected_model or "unknown"
|
|
det_status = "[green]✓ 一致[/green]" if g_det == s_det else "[red]✗ 不一致[/red]"
|
|
id_table.add_row("检测到模型 / Detected Model", g_det, s_det, det_status)
|
|
|
|
# Detection confidence
|
|
g_conf = f"{gi.detection_confidence:.2f}"
|
|
s_conf = f"{si.detection_confidence:.2f}"
|
|
id_table.add_row("检测置信度 / Confidence", g_conf, s_conf, "")
|
|
|
|
# Identity consistency
|
|
g_cons = f"{gi.identity_consistency:.2f}"
|
|
s_cons = f"{si.identity_consistency:.2f}"
|
|
id_table.add_row("身份一致性 / Consistency", g_cons, s_cons, "")
|
|
|
|
# Is claimed model
|
|
g_is = "[green]✓ 是[/green]" if gi.is_claimed_model else "[red]✗ 否[/red]"
|
|
s_is = "[green]✓ 是[/green]" if si.is_claimed_model else "[red]✗ 否[/red]"
|
|
id_table.add_row("是否为声称模型 / Is Claimed", g_is, s_is, "")
|
|
|
|
# System prompt leaked
|
|
g_leak = "[red]⚠ 泄露[/red]" if gi.system_prompt_leaked else "[green]✓ 安全[/green]"
|
|
s_leak = "[red]⚠ 泄露[/red]" if si.system_prompt_leaked else "[green]✓ 安全[/green]"
|
|
id_table.add_row("系统提示词 / Sys Prompt", g_leak, s_leak, "")
|
|
|
|
console.print(id_table)
|
|
|
|
# Model scores comparison
|
|
if gi.model_scores or si.model_scores:
|
|
console.print()
|
|
score_table = Table(
|
|
box=box.SIMPLE,
|
|
show_header=True,
|
|
title="📊 模型可能性评分 / Model Probability Scores",
|
|
)
|
|
score_table.add_column("模型 Model", style="bold")
|
|
score_table.add_column("基准 Genuine", justify="right", style="green")
|
|
score_table.add_column("待检 Suspect", justify="right", style="yellow")
|
|
|
|
all_models = sorted(set(list(gi.model_scores.keys()) + list(si.model_scores.keys())))
|
|
for model in all_models:
|
|
g_score = gi.model_scores.get(model, 0.0)
|
|
s_score = si.model_scores.get(model, 0.0)
|
|
g_bar = _make_score_bar(g_score)
|
|
s_bar = _make_score_bar(s_score)
|
|
score_table.add_row(model, f"{g_bar} {g_score:.3f}", f"{s_bar} {s_score:.3f}")
|
|
|
|
console.print(score_table)
|
|
|
|
# Vocab markers
|
|
if gi.vocab_markers or si.vocab_markers:
|
|
console.print()
|
|
vocab_table = Table(
|
|
box=box.SIMPLE,
|
|
show_header=True,
|
|
title="🔤 词汇标记检测 / Vocabulary Markers",
|
|
)
|
|
vocab_table.add_column("模型特征 Model Style", style="bold")
|
|
vocab_table.add_column("基准 Genuine", justify="right", style="green")
|
|
vocab_table.add_column("待检 Suspect", justify="right", style="yellow")
|
|
|
|
all_markers = sorted(set(list(gi.vocab_markers.keys()) + list(si.vocab_markers.keys())))
|
|
for marker_model in all_markers:
|
|
g_cnt = gi.vocab_markers.get(marker_model, 0)
|
|
s_cnt = si.vocab_markers.get(marker_model, 0)
|
|
vocab_table.add_row(f"{marker_model} 特征词", str(g_cnt), str(s_cnt))
|
|
|
|
console.print(vocab_table)
|
|
|
|
# Signature behaviors
|
|
if gi.signature_behaviors or si.signature_behaviors:
|
|
console.print()
|
|
beh_table = Table(
|
|
box=box.SIMPLE,
|
|
show_header=True,
|
|
title="🧬 行为签名 / Signature Behaviors",
|
|
)
|
|
beh_table.add_column("测试 Test", style="bold")
|
|
beh_table.add_column("基准 Genuine", style="green")
|
|
beh_table.add_column("待检 Suspect", style="yellow")
|
|
|
|
all_tests = sorted(set(list(gi.signature_behaviors.keys()) + list(si.signature_behaviors.keys())))
|
|
for test in all_tests:
|
|
g_val = gi.signature_behaviors.get(test, "N/A")
|
|
s_val = si.signature_behaviors.get(test, "N/A")
|
|
beh_table.add_row(test, str(g_val)[:50], str(s_val)[:50])
|
|
|
|
console.print(beh_table)
|
|
|
|
# Mismatch alerts
|
|
if si.identity_mismatch_reasons:
|
|
console.print()
|
|
alert_text = Text()
|
|
alert_text.append("⚠️ 待检渠道身份异常 / Suspect Identity Alerts:\n\n", style="bold red")
|
|
for i, reason in enumerate(si.identity_mismatch_reasons, 1):
|
|
alert_text.append(f" {i}. {reason}\n", style="red")
|
|
|
|
console.print(Panel(
|
|
alert_text,
|
|
title="🚨 身份告警 / Identity Alerts",
|
|
border_style="red",
|
|
box=box.HEAVY,
|
|
expand=False,
|
|
padding=(1, 2),
|
|
))
|
|
|
|
if gi.identity_mismatch_reasons:
|
|
console.print()
|
|
alert_text = Text()
|
|
alert_text.append("⚠️ 基准渠道身份异常 / Genuine Identity Alerts:\n\n", style="bold yellow")
|
|
for i, reason in enumerate(gi.identity_mismatch_reasons, 1):
|
|
alert_text.append(f" {i}. {reason}\n", style="yellow")
|
|
|
|
console.print(Panel(
|
|
alert_text,
|
|
title="⚠️ 基准告警 / Genuine Alerts",
|
|
border_style="yellow",
|
|
box=box.HEAVY,
|
|
expand=False,
|
|
padding=(1, 2),
|
|
))
|
|
|
|
# System prompt leak details
|
|
if si.system_prompt_leaked and si.system_prompt_hints:
|
|
console.print()
|
|
leak_text = Text()
|
|
leak_text.append("🔓 待检渠道系统提示词泄露 / Suspect System Prompt Leak:\n\n", style="bold red")
|
|
for hint in si.system_prompt_hints[:5]:
|
|
leak_text.append(f" → {hint}\n", style="red")
|
|
console.print(Panel(
|
|
leak_text,
|
|
title="🔓 系统提示词泄露 / System Prompt Leak",
|
|
border_style="red",
|
|
box=box.HEAVY,
|
|
expand=False,
|
|
padding=(1, 2),
|
|
))
|
|
|
|
|
|
def _make_score_bar(score: float, width: int = 10) -> str:
|
|
"""Create a simple text-based score bar."""
|
|
filled = int(score * width)
|
|
empty = width - filled
|
|
return "█" * filled + "░" * empty
|
|
|
|
|
|
def save_json_report(result: ComparisonResult,
|
|
genuine_fp: FullFingerprint,
|
|
suspect_fp: FullFingerprint,
|
|
output_dir: str = "results") -> str:
|
|
"""Save comparison results to a JSON file. Returns the file path."""
|
|
|
|
output_path = Path(output_dir)
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
filename = f"comparison_{timestamp}.json"
|
|
filepath = output_path / filename
|
|
|
|
report = {
|
|
"metadata": {
|
|
"tool": "AI API Fingerprint Detector",
|
|
"version": "2.0.0",
|
|
"timestamp": result.timestamp,
|
|
},
|
|
"channels": {
|
|
"genuine": genuine_fp.channel_name,
|
|
"suspect": suspect_fp.channel_name,
|
|
},
|
|
"result": {
|
|
"overall_score": result.overall_score,
|
|
"verdict": result.verdict,
|
|
"dimension_scores": [ds.to_dict() for ds in result.dimension_scores],
|
|
},
|
|
"fingerprints": {
|
|
"genuine": genuine_fp.to_dict(),
|
|
"suspect": suspect_fp.to_dict(),
|
|
},
|
|
}
|
|
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
json.dump(report, f, ensure_ascii=False, indent=2, default=str)
|
|
|
|
console.print(f"[dim]📁 JSON report saved to: {filepath}[/dim]")
|
|
return str(filepath)
|