A 👎 now saves, server side, the rated turn plus the context window the model read for it (reconstructed from the stored session with the same consultationHistoryWindow the consult route uses), model and run facts. 👍 is only counted. Switching to 👍 or clearing deletes the snapshot. Bodies are blanked after 90 days; the row cascades on session delete and on account deletion. - Optional 不满意原因 panel under the answer after a 👎 (five reasons, 200-char note, "会把这一轮对话发给我们排查"). - Admin 对话质量记录: 👍/👎 stats by day and model, list without text, audited snapshot open, 处理状态 + note (support.quality.read/write). - Privacy draft: what a 👎 keeps, why, 90 days, deletion. - Migration 20260930050000 is add-only; set_reply_rating() replaced with the same signature. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
/**
|
|
* 对话质量记录 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<Record<ReplyQualityReason, string>> = {
|
|
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<Record<ReplyQualityStatus, string>> = {
|
|
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:<key>`,
|
|
* `model:<id>` or `days:<n>`. 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;
|
|
}
|