import { z } from "zod"; /** * 反馈与投诉 (compliance round 2026-09-30). Shared by the account-menu dialog, * POST /api/feedback and the admin list. The table holds the user's words, an * optional contact and at most a session id — never conversation text. */ export const FEEDBACK_TYPES = ["problem", "content_report", "refund", "other"] as const; export type FeedbackType = (typeof FEEDBACK_TYPES)[number]; export const FEEDBACK_TYPE_LABELS: Readonly> = { problem: "问题反馈", content_report: "内容举报", refund: "退款与扣点", other: "其他", }; export const FEEDBACK_STATUSES = ["new", "in_progress", "resolved", "rejected"] as const; export type FeedbackStatus = (typeof FEEDBACK_STATUSES)[number]; export const FEEDBACK_STATUS_LABELS: Readonly> = { new: "待处理", in_progress: "处理中", resolved: "已解决", rejected: "不予处理", }; export const FEEDBACK_BODY_MIN = 10; export const FEEDBACK_BODY_MAX = 2000; export const FEEDBACK_CONTACT_MAX = 200; /** Promised reply time on the success screen, in working days. */ export const FEEDBACK_RESPONSE_WORKING_DAYS = 3; /** Mirrors submit_user_feedback(): five submissions per rolling hour. */ export const FEEDBACK_HOURLY_LIMIT = 5; export const feedbackSubmissionSchema = z.object({ type: z.enum(FEEDBACK_TYPES), body: z.string().trim().min(FEEDBACK_BODY_MIN).max(FEEDBACK_BODY_MAX), contact: z.string().trim().max(FEEDBACK_CONTACT_MAX).optional().transform((value) => value || undefined), sessionId: z.string().uuid().optional(), }); export type FeedbackSubmission = z.infer; /** Client-side check with the same limits the server enforces; null when valid. */ export function feedbackBodyProblem(body: string): string | null { const length = body.trim().length; if (length < FEEDBACK_BODY_MIN) return `请至少写 ${FEEDBACK_BODY_MIN} 个字,说清楚发生了什么。`; if (length > FEEDBACK_BODY_MAX) return `最多 ${FEEDBACK_BODY_MAX} 个字。`; return null; } export const FEEDBACK_RATE_LIMITED_COPY = "提交得有点频繁,请一小时后再试;急事可以直接发邮件给我们。"; /** Maps the database's error text to an HTTP answer; unknown errors are 503. */ export function feedbackRpcErrorStatus(message: string | undefined): { status: number; error: string } { if (message?.includes("feedback_rate_limited")) return { status: 429, error: FEEDBACK_RATE_LIMITED_COPY }; if (message?.includes("feedback_session_not_owned")) return { status: 400, error: "附上的对话不属于当前账号,请去掉勾选后再提交。" }; return { status: 503, error: "暂时没能提交,请稍后再试。" }; } /** Admin list filter: a status, or `type:`. Unknown values filter nothing. */ export function parseFeedbackFilter(value: string | undefined): { type: string | null; status: string | null } { if (!value) return { type: null, status: null }; if (value.startsWith("type:")) { const type = value.slice(5); return { type: (FEEDBACK_TYPES as readonly string[]).includes(type) ? type : null, status: null }; } return { type: null, status: (FEEDBACK_STATUSES as readonly string[]).includes(value) ? value : null }; }