// BehavioralSignatureDetector(veridrop detectors/behavioral_signature.py):几道短题看 Claude 的典型行为 // (markdown 加粗、列表、婉拒时给替代方案)。expected 命中且 unexpected 不命中算一条命中;分 = 命中权重 / 总权重 × 100。 import { BEHAVIORAL_SIGNATURES } from "../data/behavioral_signatures.js"; import type { DetectorResult } from "../models.js"; import { cpSlice, joinText, pySearch } from "../py.js"; import { ActiveDetector, errText, type DetectClient } from "./base.js"; const MAX_TOKENS = 350; interface Signature { id: string; prompt: string; system?: string; expected_patterns?: readonly string[]; unexpected_patterns?: readonly string[]; expected_match?: string; weight?: number; } export function evaluateSignature(text: string, sig: Signature): boolean { const expected = sig.expected_patterns ?? []; const unexpected = sig.unexpected_patterns ?? []; const mode = sig.expected_match ?? "all"; const rx = (p: string) => pySearch(p, text, "im"); // re.IGNORECASE | re.MULTILINE if (unexpected.some(rx)) return false; if (expected.length === 0) return true; if (mode === "any") return expected.some(rx); return expected.every(rx); } export class BehavioralSignatureDetector extends ActiveDetector { readonly name = "behavioral_signature"; readonly displayName = "行为签名验证"; readonly weight = 15; async run(client: DetectClient, model: string): Promise { const signatures = BEHAVIORAL_SIGNATURES.signatures as readonly Signature[]; if (signatures.length === 0) return this.result("skip", 0, { reason: "no signatures loaded" }); const results: Record[] = []; let weightedHits = 0; let maxWeight = 0; for (const sig of signatures) { const w = Number(sig.weight ?? 1); maxWeight += w; const body: Record = { model, max_tokens: MAX_TOKENS, temperature: 0, messages: [{ role: "user", content: sig.prompt }], }; if (sig.system) body["system"] = sig.system; let resp: Record; try { ({ resp } = await client.messagesCreate(body)); } catch (e) { results.push({ id: sig.id, hit: false, error: errText(e), weight: w }); continue; } const text = joinText(resp["content"]); const hit = evaluateSignature(text, sig); results.push({ id: sig.id, hit, weight: w, response_excerpt: cpSlice(text, 200) }); if (hit) weightedHits += w; } const score = maxWeight ? (weightedHits / maxWeight) * 100 : 0; return this.result(score >= 70 ? "pass" : "fail", score, { signatures: results, hits: results.filter((r) => r["hit"]).length, total: results.length, }); } }