import type { SongBlacklist } from '~/drizzle/schema' export type BlacklistType = 'KEYWORD' | 'LANGUAGE' | 'GENRE' | 'SONG' export interface ModerationContext { title?: string | null artist?: string | null genre?: string | null } export interface ModerationResult { notePublic: boolean publicStatus: 'approved' | 'rejected' | null rejectedReasons: string[] matchedRules: { type: BlacklistType; value: string }[] } function escapeRegex(s: string): string { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } function isCJK(text: string): boolean { return /[\u4e00-\u9fa5]/.test(text) } function isLatin(text: string): boolean { return /[A-Za-z]/.test(text) } function isKana(text: string): boolean { return /[\u3040-\u30ff]/.test(text) } function isHangul(text: string): boolean { return /[\uac00-\ud7af]/.test(text) } function isCyrillic(text: string): boolean { return /[\u0400-\u04ff]/.test(text) } function isArabic(text: string): boolean { return /[\u0600-\u06ff]/.test(text) } function detectLanguages(text: string): string[] { const result: string[] = [] if (!text) return result const normalized = text.toLowerCase() if (isCJK(text)) result.push('zh', '中文', 'chinese', 'cjk') if (isLatin(text)) result.push('en', '英文', 'english', 'latin') if (isKana(text)) result.push('ja', '日文', 'japanese') if (isHangul(text)) result.push('ko', '韩文', 'korean') if (isCyrillic(text)) result.push('ru', '俄文', 'russian', 'cyrillic') if (isArabic(text)) result.push('ar', '阿拉伯文', 'arabic') return result } export function moderateSubmissionNote( note: string | null, wantPublic: boolean, requiresApproval: boolean, song: ModerationContext, blacklistRows: SongBlacklist[] ): ModerationResult { if (!note || !wantPublic) { return { notePublic: wantPublic && !!note, publicStatus: null, rejectedReasons: [], matchedRules: [] } } const noteLower = note.toLowerCase() const noteForKeyword = note.toLowerCase() const title = (song.title || '').toLowerCase() const artist = (song.artist || '').toLowerCase() const genre = (song.genre || '').toLowerCase() const rejectedReasons: string[] = [] const matchedRules: { type: BlacklistType; value: string }[] = [] const noteLanguages = detectLanguages(note) const textLanguages = new Set([ ...noteLanguages, ...detectLanguages(song.title || ''), ...detectLanguages(song.artist || ''), ...detectLanguages(song.genre || '') ]) for (const row of blacklistRows) { const t = row.type as BlacklistType const rawValue = row.value if (!rawValue) continue const value = rawValue.trim() if (!value) continue const valueLower = value.toLowerCase() if (t === 'KEYWORD') { // 命中 note / title / artist / genre 的其中一个都算 const haystacks = [noteForKeyword, title, artist, genre].filter(Boolean) as string[] const exact = haystacks.some((h) => h.includes(valueLower)) const asRegex = (() => { try { const re = new RegExp(escapeRegex(value), 'i') return haystacks.some((h) => re.test(h)) } catch (_) { return false } })() if (exact || asRegex) { matchedRules.push({ type: t, value }) rejectedReasons.push(`命中敏感词:${value}`) } } else if (t === 'LANGUAGE') { const v = valueLower const checkList = [ v, ...v.split(/[,,\/|\s]+/).map((s) => s.trim()).filter(Boolean) ] const hit = checkList.some((lang) => textLanguages.has(lang)) if (hit) { matchedRules.push({ type: t, value }) rejectedReasons.push(`命中语言限制:${value}`) } } else if (t === 'GENRE') { // 匹配流派:genre 字段或 note/title/artist 中的流派关键词 const combinedForGenre = `${genre} ${noteForKeyword} ${title} ${artist}`.trim() if (!combinedForGenre) continue const tokens = valueLower.split(/[,,\/|\s]+/).map((s) => s.trim()).filter(Boolean) const hit = tokens.some((tok) => combinedForGenre.includes(tok)) if (hit) { matchedRules.push({ type: t, value }) rejectedReasons.push(`命中流派限制:${value}`) } } else { // SONG 不参与 note 审查 continue } } const rejected = matchedRules.length > 0 return { notePublic: rejected ? false : (requiresApproval ? false : wantPublic), publicStatus: requiresApproval || matchedRules.length > 0 ? (rejected ? 'rejected' : 'approved') : null, rejectedReasons, matchedRules } }