/** * 对话质量记录 vocabulary shared by the 👎 picker, the API and the admin page * (compliance round 2026-09-30). Client-safe: no node imports. */ export const REPLY_QUALITY_REASONS = ["off_topic", "inaccurate", "too_long_or_empty", "tone", "other"] as const; export type ReplyQualityReason = (typeof REPLY_QUALITY_REASONS)[number]; /** 不满意原因, in the order the picker shows them. */ export const REPLY_QUALITY_REASON_LABELS: Readonly> = { off_topic: "答非所问", inaccurate: "内容不准", too_long_or_empty: "太长太空", tone: "语气不对", other: "其他", }; export const REPLY_QUALITY_NOTE_MAX = 200; export const REPLY_QUALITY_RETENTION_DAYS = 90; /** 处理状态 of a 对话质量记录. */ export const REPLY_QUALITY_STATUSES = ["new", "in_progress", "resolved", "ignored"] as const; export type ReplyQualityStatus = (typeof REPLY_QUALITY_STATUSES)[number]; export const REPLY_QUALITY_STATUS_LABELS: Readonly> = { new: "待处理", in_progress: "处理中", resolved: "已解决", ignored: "忽略", }; export type ReplyQualityFilter = Readonly<{ status: ReplyQualityStatus | null; reason: ReplyQualityReason | null; modelId: string | null; days: number | null; }>; /** * The admin table has one filter slot; it carries `status`, `reason:`, * `model:` or `days:`. Unknown values mean no filter. */ export function parseReplyQualityFilter(value: string | null | undefined): ReplyQualityFilter { const empty = { status: null, reason: null, modelId: null, days: null }; if (!value) return empty; if ((REPLY_QUALITY_STATUSES as readonly string[]).includes(value)) return { ...empty, status: value as ReplyQualityStatus }; const [kind, rest] = [value.slice(0, value.indexOf(":")), value.slice(value.indexOf(":") + 1)]; if (kind === "reason" && (REPLY_QUALITY_REASONS as readonly string[]).includes(rest)) return { ...empty, reason: rest as ReplyQualityReason }; if (kind === "model" && /^[\w.:/-]{1,120}$/.test(rest)) return { ...empty, modelId: rest }; if (kind === "days" && /^\d{1,2}$/.test(rest) && Number(rest) >= 1 && Number(rest) <= 90) return { ...empty, days: Number(rest) }; return empty; }