feat(pua): 新增重复检测——LLM语义相似度判断,相似60%效果降级,相似80%强制翻车,额外扣HC惩罚

This commit is contained in:
Cloud Bot
2026-03-24 08:44:35 +00:00
parent 1473542f65
commit a36c8af344
2 changed files with 163 additions and 36 deletions

View File

@@ -21,7 +21,11 @@ interface PuaResult {
title: string
desc: string
effect: EffectType
cost?: number // 实际扣除的 HC前端填写
cost?: number
similarity?: number
similarTo?: string
isDuplicate?: boolean
penaltyMultiplier?: number
}
const EFFECT_META: Record<EffectType, { label: string; color: string; icon: string }> = {
@@ -53,7 +57,10 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<PuaResult | null>(null)
const [history, setHistory] = useState<PuaResult[]>([])
// 历史原文列表(用于发给服务端做相似度检测)
const historyTexts = useRef<string[]>([])
const [insufficient, setInsufficient] = useState(false)
const [dupWarning, setDupWarning] = useState<string | null>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const placeholder = useRef(PUA_PLACEHOLDERS[Math.floor(Math.random() * PUA_PLACEHOLDERS.length)]).current
@@ -63,7 +70,6 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
const handleSubmit = useCallback(async () => {
if (!text.trim() || loading || !gameReady || !waveStarted) return
// 先从游戏扣除 HC扣不到则拒绝
const spendHC: ((n: number) => boolean) | undefined =
typeof window !== 'undefined' ? (window as any).__gameSpendHC : undefined
@@ -74,6 +80,7 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
const actualCost = calcPuaCost(currentHC)
// 扣除基础费用
if (!spendHC?.(actualCost)) {
setInsufficient(true)
setTimeout(() => setInsufficient(false), 2000)
@@ -82,18 +89,43 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
setLoading(true)
setResult(null)
setDupWarning(null)
setInsufficient(false)
try {
const res = await fetch('/api/pua-score', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text.trim() }),
body: JSON.stringify({
text: text.trim(),
history: historyTexts.current,
}),
})
const data: PuaResult & { cost?: number } = await res.json()
const data: PuaResult = await res.json()
data.cost = actualCost
// 处理重复惩罚:额外扣 HC
const penalty = data.penaltyMultiplier ?? 0
if (penalty > 0 && data.isDuplicate) {
const extraCost = Math.round(actualCost * penalty)
const penaltyPaid = spendHC?.(extraCost) ?? false
if (penaltyPaid) {
data.cost = actualCost + extraCost
// 显示重复警告
const sim = data.similarity ?? 0
const level = sim >= 0.8 ? '严重重复' : '内容相似'
setDupWarning(
`检测到${level}!额外扣除 ${extraCost} HC` +
(data.similarTo ? `\n与"${data.similarTo}"相似)` : '')
)
}
}
setResult(data)
// 更新历史原文列表最近8条
historyTexts.current = [text.trim(), ...historyTexts.current].slice(0, 8)
setHistory(prev => [data, ...prev].slice(0, 5))
// 通知游戏场景应用 buff
if (typeof window !== 'undefined') {
;(window as any).__gamePuaBuff?.(data.effect, data.score, data.title)
}
@@ -183,7 +215,7 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
</div>
</div>
{/* HC 不足闪烁提示 */}
{/* HC 不足提示 */}
{insufficient && (
<div style={{
backgroundColor: '#7F1D1D',
@@ -194,12 +226,28 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
fontSize: '14px',
color: '#FCA5A5',
textAlign: 'center',
animation: 'none',
}}>
HC
</div>
)}
{/* 重复惩罚警告 */}
{dupWarning && (
<div style={{
backgroundColor: '#431407',
border: '1px solid #F97316',
borderRadius: '6px',
padding: '7px 8px',
fontFamily: 'VT323, monospace',
fontSize: '13px',
color: '#FED7AA',
lineHeight: 1.4,
whiteSpace: 'pre-line',
}}>
{dupWarning}
</div>
)}
{/* 输入框 */}
<textarea
ref={textareaRef}
@@ -295,6 +343,35 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
}}>
{result.desc}
</div>
{/* 相似度指示(有重复时显示) */}
{(result.similarity ?? 0) >= 0.3 && (
<div style={{
backgroundColor: '#431407',
border: '1px solid #F97316',
borderRadius: '4px',
padding: '4px 6px',
fontFamily: 'VT323, monospace',
fontSize: '12px',
color: '#FED7AA',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
<span></span>
<span style={{ color: (result.similarity ?? 0) >= 0.8 ? '#EF4444' : '#F97316' }}>
{Math.round((result.similarity ?? 0) * 100)}%
</span>
</div>
{/* 相似度进度条 */}
<div style={{ height: '3px', backgroundColor: '#1c0a04', borderRadius: '2px' }}>
<div style={{
height: '100%',
width: `${(result.similarity ?? 0) * 100}%`,
backgroundColor: (result.similarity ?? 0) >= 0.8 ? '#EF4444' : '#F97316',
borderRadius: '2px',
transition: 'width 0.3s ease',
}} />
</div>
</div>
)}
{/* 效果标签 + 花费 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{
@@ -376,13 +453,15 @@ function PuaPanel({ gameReady, hc, waveStarted }: { gameReady: boolean; hc: numb
paddingTop: '8px',
}}>
<span style={{ color: '#475569' }}> = HC × 15%</span><br />
<span style={{ color: '#334155' }}>20 200</span><br />
<br />
Enter <br />
1-2分: 废话翻<br />
4-6分: 攻击+HC<br />
60%: <br />
80%: <br />
HC<br />
<br />
9-10分: 全场狂暴<br />
7-8分: 攻速暴增<br />
9-10: 全场狂暴
4-6: 攻击+HC<br />
1-2分: 废话翻车
</div>
</div>
)