// MessageIDDetector(veridrop detectors/message_id.py):被动,看 id 前缀约定(msg_、toolu_、srvtoolu_)。 // 官方只保证前缀,长度与字符集「以后可能变」,所以不查那些。每种违规扣 25(同一种只扣一次)。 import type { Body } from "../client.js"; import type { DetectorResult } from "../models.js"; import { cpSlice, pyRepr } from "../py.js"; import { PassiveDetector } from "./base.js"; const BASE_CHECK_WEIGHT = 25; const NESTED_VIOLATION_PENALTY = 25; const BASE_LABELS = new Set(["id_prefix_invalid", "type_not_message", "role_not_assistant", "model_not_claude"]); export class MessageIDDetector extends PassiveDetector { readonly name = "message_id"; readonly displayName = "消息标识规范"; readonly weight = 5; private readonly violations = new Set(); private readonly samples = new Map(); private observations = 0; override observe(_request: Body, response: Record): void { this.observations++; const rid = response["id"]; if (!(typeof rid === "string" && rid.startsWith("msg_") && Array.from(rid).length >= 8)) { this.record("id_prefix_invalid", cpSlice(pyRepr(rid), 80)); } if (response["type"] !== "message") this.record("type_not_message", cpSlice(pyRepr(response["type"]), 80)); if (response["role"] !== "assistant") this.record("role_not_assistant", cpSlice(pyRepr(response["role"]), 80)); const model = response["model"]; if (typeof model !== "string" || !model.toLowerCase().includes("claude")) { this.record("model_not_claude", cpSlice(pyRepr(model), 80)); } const content = Array.isArray(response["content"]) ? (response["content"] as unknown[]) : []; for (const block of content) { if (!block || typeof block !== "object" || Array.isArray(block)) continue; const b = block as Record; const bid = b["id"]; if (b["type"] === "tool_use" && typeof bid === "string") { if (!bid.startsWith("toolu_")) this.record("tool_use_id_prefix_invalid", cpSlice(pyRepr(bid), 80)); } else if (b["type"] === "server_tool_use" && typeof bid === "string") { if (!bid.startsWith("srvtoolu_")) this.record("server_tool_use_id_prefix_invalid", cpSlice(pyRepr(bid), 80)); } } } private record(label: string, example: string): void { this.violations.add(label); if (!this.samples.has(label)) this.samples.set(label, example); } finalize(): DetectorResult { if (this.observations === 0) return this.skip("no observations"); let score = 100; for (const label of this.violations) { score -= BASE_LABELS.has(label) ? BASE_CHECK_WEIGHT : NESTED_VIOLATION_PENALTY; } score = Math.max(0, score); return this.result(score >= 70 ? "pass" : "fail", score, { observation_count: this.observations, violations: [...this.violations].sort(), samples: Object.fromEntries(this.samples), }); } }