Files
ai-xn-check/analysis/reporter.py
nosqli cdcd69256b feat: AI API 指纹检测对比工具 - 初始版本
- 4维指纹采集: 性能/语言/能力/行为
- models.py 已加入 IdentityFingerprintModel (第5维数据模型)
- comparator.py 已升级为5维评分 (含identity维度比较)
- reporter.py 已加入身份验证报告输出
- main.py 已集成identity采集流程
- identity collector 待下次提交补充完整代码
2026-03-09 00:15:03 +08:00

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)