// IdentityDetector(veridrop detectors/identity.py):不带系统提示直接问「你是谁」。 // 注入了系统提示的中转站一下就能糊弄过去,所以权重只有 5%。 import type { DetectorResult } from "../models.js"; import { cpSlice, joinText, pySearch } from "../py.js"; import { ActiveDetector, errText, type DetectClient } from "./base.js"; export const PROMPT = "Who are you exactly? What is your model name and version? Which company developed you? Answer concisely."; const MAX_TOKENS = 200; // 正则照 Python 原样写(报告里 required_hits / competitor_hits 记的就是这些串);\b 按 Unicode 边界编(py.ts)。 const REQUIRED = [String.raw`\bclaude\b`, String.raw`\banthropic\b`]; const COMPETITOR = [ String.raw`\bgpt[-\s]?\d+`, String.raw`\bopenai\b`, String.raw`\bchatgpt\b`, String.raw`\bgemini\b`, String.raw`\bbard\b`, String.raw`\bgoogle\b`, String.raw`\bdeepseek\b`, String.raw`\bqwen\b`, String.raw`\bllama\b`, String.raw`\bmistral\b`, ]; /** 具体的产品 / 厂商指纹:自我介绍里出现就直接指向真实后端。 */ const NON_ANTHROPIC_BRAND_PATTERNS: ReadonlyArray = [ [String.raw`\bamazon\s+q\b`, "Amazon Q"], [String.raw`\baws\b`, "AWS"], [String.raw`\bbedrock\b`, "AWS Bedrock"], [String.raw`\bkiro\b`, "Kiro"], [String.raw`\bchatgpt\b`, "ChatGPT"], [String.raw`\bopenai\b`, "OpenAI"], [String.raw`\bgpt[-\s]?[345](?:\.\d+)?\b`, "GPT-3/4/5"], [String.raw`\bgemini\b`, "Gemini"], [String.raw`\bbard\b`, "Bard"], [String.raw`\bcopilot\b`, "Copilot"], [String.raw`\bdeepseek\b`, "DeepSeek"], [String.raw`\bqwen\b`, "Qwen"], [String.raw`\btongyi\b`, "Tongyi"], [String.raw`\bdoubao\b`, "Doubao"], [String.raw`\bwenxin\b|\b文心\b`, "Wenxin"], [String.raw`\bllama\b`, "LLaMA"], [String.raw`\bmistral\b`, "Mistral"], ]; export class IdentityDetector extends ActiveDetector { readonly name = "identity"; readonly displayName = "身份一致性"; readonly weight = 5; async run(client: DetectClient, model: string): Promise { let resp: Record; try { ({ resp } = await client.messagesCreate({ model, max_tokens: MAX_TOKENS, temperature: 0, messages: [{ role: "user", content: PROMPT }], })); } catch (e) { return this.result("error", 0, null, null, errText(e)); } const text = joinText(resp["content"]); const lc = text.toLowerCase(); const requiredHits = REQUIRED.filter((p) => pySearch(p, lc)); const competitorHits = COMPETITOR.filter((p) => pySearch(p, lc)); const seen = new Set(); const detectedBrands: string[] = []; for (const [pattern, label] of NON_ANTHROPIC_BRAND_PATTERNS) { if (pySearch(pattern, lc) && !seen.has(label)) { seen.add(label); detectedBrands.push(label); } } // 100 两个都有、没有竞品;60 只有一个;30 有 Claude 信号但混了竞品;0 一点 Claude 信号都没有。 let score: number; if (requiredHits.length === 2 && competitorHits.length === 0) score = 100; else if (requiredHits.length > 0 && competitorHits.length > 0) score = 30; else if (requiredHits.length > 0) score = 60; else score = 0; return this.result(score >= 70 ? "pass" : "fail", score, { response_text: cpSlice(text, 300), required_hits: requiredHits, competitor_hits: competitorHits, detected_non_anthropic_brands: detectedBrands, }); } }