Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
71 lines
3.2 KiB
TypeScript
71 lines
3.2 KiB
TypeScript
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<Record<FeedbackType, string>> = {
|
|
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<Record<FeedbackStatus, string>> = {
|
|
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<typeof feedbackSubmissionSchema>;
|
|
|
|
/** 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:<feedback 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 };
|
|
}
|