feat(ECR-012–016): 合规、题库、时辰刷新、头像、MBTI OEJTS 与埋点
落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,3 +18,10 @@ export function validateBirth(year: number, month: number, day: number): string
|
||||
if (day < 1 || day > 31) return '日期无效'
|
||||
return null
|
||||
}
|
||||
|
||||
export {
|
||||
normalizeUserText,
|
||||
validateUserText,
|
||||
type UserTextKind,
|
||||
type ValidateUserTextResult,
|
||||
} from './textsafe'
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/** User free-text kinds (mirror apps/api/internal/textsafe). */
|
||||
export type UserTextKind =
|
||||
| 'nickname'
|
||||
| 'display_name'
|
||||
| 'ask_content'
|
||||
| 'note'
|
||||
| 'title'
|
||||
| 'focus'
|
||||
| 'scene'
|
||||
|
||||
const limits: Record<UserTextKind, { min: number; max: number; allowNL: boolean }> = {
|
||||
nickname: { min: 1, max: 16, allowNL: false },
|
||||
display_name: { min: 1, max: 24, allowNL: false },
|
||||
ask_content: { min: 1, max: 2000, allowNL: true },
|
||||
note: { min: 0, max: 200, allowNL: true },
|
||||
title: { min: 1, max: 40, allowNL: false },
|
||||
focus: { min: 0, max: 40, allowNL: false },
|
||||
scene: { min: 1, max: 80, allowNL: false },
|
||||
}
|
||||
|
||||
const banned = [
|
||||
'占卜',
|
||||
'算命',
|
||||
'改命',
|
||||
'测测',
|
||||
'疗效',
|
||||
'包治',
|
||||
'根治',
|
||||
'治疗疾病',
|
||||
'治愈癌症',
|
||||
'不测就有灾',
|
||||
'大师算卦',
|
||||
'神棍',
|
||||
]
|
||||
|
||||
/** Normalize like backend textsafe.Normalize. */
|
||||
export function normalizeUserText(raw: string, allowNL: boolean): string {
|
||||
let s = raw.trim()
|
||||
if (!allowNL) {
|
||||
s = s.replace(/[\n\r\t]/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
function rejectControls(s: string, allowNL: boolean): string | null {
|
||||
for (const ch of s) {
|
||||
const code = ch.codePointAt(0) ?? 0
|
||||
if (code === 0) return '含非法字符'
|
||||
if (ch === '\n' || ch === '\r' || ch === '\t') {
|
||||
if (!allowNL) return '含非法空白'
|
||||
continue
|
||||
}
|
||||
if (code < 0x20 || code === 0x7f) return '含非法字符'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function rejectMarkup(s: string): string | null {
|
||||
const low = s.toLowerCase()
|
||||
if (low.includes('<script') || low.includes('javascript:')) return '含不安全内容'
|
||||
if (low.includes('onerror=') || low.includes('onload=')) return '含不安全内容'
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (s[i] !== '<') continue
|
||||
const c = s[i + 1]
|
||||
if (!c) continue
|
||||
if (c === '/' || c === '!' || /[a-zA-Z]/.test(c)) return '含不安全内容'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function rejectBanned(s: string): string | null {
|
||||
for (const b of banned) {
|
||||
if (b && s.includes(b)) return '含不允许的表述,请换一种说法'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function rejectSpam(s: string): string | null {
|
||||
const runes = [...s]
|
||||
if (runes.length >= 6) {
|
||||
if (runes.every((r) => r === runes[0]) && runes[0]?.trim()) return '请勿重复刷屏'
|
||||
}
|
||||
let streak = 1
|
||||
for (let i = 1; i < runes.length; i++) {
|
||||
if (runes[i] === runes[i - 1] && runes[i]?.trim()) {
|
||||
streak++
|
||||
if (streak >= 8) return '请勿重复刷屏'
|
||||
} else {
|
||||
streak = 1
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export type ValidateUserTextResult =
|
||||
| { ok: true; value: string }
|
||||
| { ok: false; message: string }
|
||||
|
||||
/**
|
||||
* Frontend mirror of API textsafe.Check.
|
||||
* Backend remains authoritative (HTTP 400 / code 40060).
|
||||
*/
|
||||
export function validateUserText(kind: UserTextKind, raw: string): ValidateUserTextResult {
|
||||
const lim = limits[kind]
|
||||
const s = normalizeUserText(raw, lim.allowNL)
|
||||
const n = [...s].length
|
||||
if (n < lim.min) {
|
||||
if (lim.min === 0) return { ok: true, value: '' }
|
||||
return { ok: false, message: '文案不合规:请填写内容' }
|
||||
}
|
||||
if (n > lim.max) return { ok: false, message: '文案不合规:内容过长' }
|
||||
const ctrl = rejectControls(s, lim.allowNL)
|
||||
if (ctrl) return { ok: false, message: `文案不合规:${ctrl}` }
|
||||
const markup = rejectMarkup(s)
|
||||
if (markup) return { ok: false, message: `文案不合规:${markup}` }
|
||||
const ban = rejectBanned(s)
|
||||
if (ban) return { ok: false, message: `文案不合规:${ban}` }
|
||||
const spam = rejectSpam(s)
|
||||
if (spam) return { ok: false, message: `文案不合规:${spam}` }
|
||||
return { ok: true, value: s }
|
||||
}
|
||||
Reference in New Issue
Block a user