// 数据模型与评分:对应 veridrop src/relay_detector/core/{models.py,scorer.py}。 export type Mode = "quick" | "standard" | "full"; export const MODES: readonly Mode[] = ["quick", "standard", "full"]; export type DetectorStatus = "pass" | "fail" | "skip" | "error"; export type Verdict = "passed" | "marginal" | "failed"; export interface ExecutionConfig { mode: Mode; maxConcurrent: number; requestTimeoutS: number; overallTimeoutS: number; includeLongContext: boolean; } /** ExecutionConfig.for_mode:整轮的墙钟上限按档位给。 */ export function configForMode( mode: Mode, overrides: Partial> = {}, ): ExecutionConfig { const overall: Record = { quick: 60, standard: 120, full: 180 }; return { mode, maxConcurrent: 3, requestTimeoutS: 30, overallTimeoutS: overall[mode], includeLongContext: false, ...overrides, }; } export interface DetectorResult { name: string; display_name: string; status: DetectorStatus; score: number; weight: number; details: Record; duration_ms: number | null; error: string | null; } export interface UsageMetrics { input_tokens: number; output_tokens: number; cache_read_input_tokens: number | null; cache_creation_input_tokens: number | null; server_tool_use: Record | null; } export function emptyUsage(): UsageMetrics { return { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: null, cache_creation_input_tokens: null, server_tool_use: null, }; } /** UsageMetrics.add:缓存两项原来是 None 的,见到第一个值才变成数。 */ export function addUsage(into: UsageMetrics, other: UsageMetrics): void { into.input_tokens += other.input_tokens; into.output_tokens += other.output_tokens; if (other.cache_read_input_tokens !== null) { into.cache_read_input_tokens = (into.cache_read_input_tokens ?? 0) + other.cache_read_input_tokens; } if (other.cache_creation_input_tokens !== null) { into.cache_creation_input_tokens = (into.cache_creation_input_tokens ?? 0) + other.cache_creation_input_tokens; } } export interface PerformanceMetrics { total_latency_ms: number; ttft_ms: number | null; tokens_per_second: number | null; usage: UsageMetrics; request_count: number; backoff_events: number; } export interface DetectionReport { id?: string; protocol: "anthropic"; tier: "cryptographic"; tier_title: string; tier_message: string; base_url: string; api_key_masked: string; target_model: string; mode: Mode; timestamp: string; total_score: number; verdict: Verdict; results: DetectorResult[]; performance: PerformanceMetrics; summary: string; run_error: string | null; self_reported_identity: string | null; detected_non_anthropic_brands: string[]; } /** mask_api_key:'sk-y7xUabc...0h' → 'sk-y7xU••••••0h'。按 Unicode 码点切,与 Python 一致。 */ export function maskApiKey(key: string): string { if (!key) return ""; const cp = Array.from(key); if (cp.length <= 8) return cp.slice(0, 2).join("") + "•".repeat(cp.length - 2); return `${cp.slice(0, 6).join("")}${"•".repeat(6)}${cp.slice(-2).join("")}`; } // ── scorer.py ───────────────────────────────────────────────────────── export function verdictFor(score: number): Verdict { if (score >= 70) return "passed"; if (score >= 50) return "marginal"; return "failed"; } export function hasCriticalIssues(results: readonly DetectorResult[]): boolean { for (const r of results) { if (r.status === "skip") continue; const details = r.details && typeof r.details === "object" ? r.details : {}; const count = details["critical_issue_count"]; if (typeof count === "number" && Number.isInteger(count) && count > 0) return true; const issues = details["issues"]; if (Array.isArray(issues)) { for (const issue of issues) { if (issue && typeof issue === "object" && (issue as Record)["severity"] === "critical") { return true; } } } } return false; } export function effectiveVerdict(score: number, results: readonly DetectorResult[]): Verdict { const base = verdictFor(score); if (hasCriticalIssues(results) && base === "passed") return "marginal"; return base; } export function summaryText(score: number, verdict: Verdict): string { if (verdict === "passed" && score >= 85) return "优秀"; if (verdict === "passed") return "通过"; if (verdict === "marginal") return "基本合格"; return "未达标"; } /** 加权平均:skip 的不进分母。 */ export function computeTotal(results: readonly DetectorResult[]): number { const valid = results.filter((r) => r.status !== "skip"); if (valid.length === 0) return 0; const weightSum = valid.reduce((n, r) => n + r.weight, 0); if (weightSum <= 0) return 0; return valid.reduce((n, r) => n + r.score * r.weight, 0) / weightSum; } const FATAL_UPSTREAM_PATTERNS = [ "credit balance is too low", "specified api usage limits", "usage limits", "insufficient_quota", "billing", ]; const FATAL_MODEL_UNAVAILABLE_PATTERNS = [ "model_not_found", "model not found", "model_not_available", "model not available", "no available channel", "无可用渠道", ]; /** 上游余额 / 额度 / 型号不可用时整轮作废,不按普通失败打分。 */ export function fatalRunError(results: readonly DetectorResult[]): string | null { const haystacks: string[] = []; for (const r of results) { if (r.error) haystacks.push(r.error); collectNestedErrors(r.details, haystacks); } const text = haystacks.join("\n").toLowerCase(); if (!text) return null; if (FATAL_UPSTREAM_PATTERNS.some((p) => text.includes(p))) { return "检测无效: 上游返回余额不足或用量限制错误。请更换有额度的 API key, 或降低检测模式/模型后重新检测。"; } if (FATAL_MODEL_UNAVAILABLE_PATTERNS.some((p) => text.includes(p))) { return "检测无效: 中转站没有当前模型的可用渠道,或该模型名称不被此中转站支持。 请更换该站已开通的模型后重新检测。"; } return null; } function collectNestedErrors(value: unknown, out: string[]): void { if (Array.isArray(value)) { for (const child of value) collectNestedErrors(child, out); } else if (value && typeof value === "object") { const obj = value as Record; if (typeof obj["error"] === "string") out.push(obj["error"]); for (const child of Object.values(obj)) collectNestedErrors(child, out); } }