feat(pua): 新增重复检测——LLM语义相似度判断,相似60%效果降级,相似80%强制翻车,额外扣HC惩罚
This commit is contained in:
@@ -2,70 +2,118 @@ import { NextResponse } from 'next/server'
|
||||
import { llmClient } from '@/app/api/llm-client'
|
||||
|
||||
const SYSTEM_PROMPT = `你是一个专门分析互联网大厂老板PUA话术的AI裁判。
|
||||
用户输入一段老板/领导对员工说的"打鸡血"或"PUA"的话,你需要分析这段话并给出评分和游戏效果。
|
||||
|
||||
评分标准(1-10分):
|
||||
1-3分:废话/没营养("大家加油"之类)→ 轻微效果
|
||||
4-6分:标准大厂黑话("对齐""闭环""狼性")→ 中等效果
|
||||
7-8分:强力PUA("996是福报""不拼搏对不起父母")→ 强力效果
|
||||
9-10分:终极毒鸡汤(极限施压/情感绑架)→ 全场爆发
|
||||
你的任务分两步:
|
||||
1. 判断当前输入是否与历史记录重复或高度相似(意思相近算相似,不只是字面相同)
|
||||
2. 对当前输入进行鸡血值评分
|
||||
|
||||
相似度判断标准(similarity字段,0.0-1.0):
|
||||
- 0.0-0.3:全新内容,没有相似之处
|
||||
- 0.3-0.6:有一定相关,但表达不同
|
||||
- 0.6-0.8:明显相似,换汤不换药
|
||||
- 0.8-1.0:基本重复,几乎一样的意思
|
||||
|
||||
评分标准(score字段,1-10分):
|
||||
- 1-3分:废话/没营养 → backfire效果
|
||||
- 4-6分:标准大厂黑话 → 中等效果
|
||||
- 7-8分:强力PUA → 攻速暴增
|
||||
- 9-10分:终极毒鸡汤 → 全场狂暴
|
||||
|
||||
只返回如下JSON,不要其他内容:
|
||||
{
|
||||
"similarity": 数字(0.0-1.0),
|
||||
"similarTo": "最相似的历史内容摘要,没有则为空字符串",
|
||||
"score": 数字(1-10),
|
||||
"title": "效果名称(2-6个汉字,有创意)",
|
||||
"desc": "对话语的一句话点评(15字以内,带点讽刺)",
|
||||
"effect": "attack_boost" | "speed_boost" | "money_rain" | "rage_mode" | "backfire"
|
||||
}
|
||||
}`
|
||||
|
||||
effect说明:
|
||||
- attack_boost: 攻击力提升(score 4-6)
|
||||
- speed_boost: 攻击速度提升(score 7-8)
|
||||
- money_rain: HC暴增(score 5-7,话语中强调利益)
|
||||
- rage_mode: 全场狂暴(score 9-10)
|
||||
- backfire: 话太废了反而debuff(score 1-2)`
|
||||
// 重复惩罚倍率:根据相似度递增
|
||||
function getDuplicatePenaltyMultiplier(similarity: number): number {
|
||||
if (similarity >= 0.8) return 2.0 // 严重重复:再扣双倍
|
||||
if (similarity >= 0.6) return 1.0 // 明显相似:再扣一倍
|
||||
return 0 // 不额外惩罚
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { text } = await req.json()
|
||||
const { text, history } = await req.json() as {
|
||||
text: string
|
||||
history: string[] // 最近N条历史原文
|
||||
}
|
||||
|
||||
if (!text || typeof text !== 'string' || text.trim().length < 2) {
|
||||
return NextResponse.json({ error: '输入内容太短了' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 构建带历史的 prompt
|
||||
const historyBlock = history && history.length > 0
|
||||
? `\n\n历史记录(已经说过的话,按时间倒序):\n${history.slice(0, 5).map((h, i) => `${i + 1}. "${h}"`).join('\n')}`
|
||||
: '\n\n历史记录:(暂无)'
|
||||
|
||||
const userMsg = `当前输入:"${text.slice(0, 200)}"${historyBlock}`
|
||||
|
||||
const resp = await llmClient.chat({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: SYSTEM_PROMPT },
|
||||
{ role: 'user', content: text.slice(0, 200) },
|
||||
{ role: 'user', content: userMsg },
|
||||
],
|
||||
maxTokens: 150,
|
||||
temperature: 0.7,
|
||||
maxTokens: 200,
|
||||
temperature: 0.6,
|
||||
})
|
||||
|
||||
const raw = resp.choices?.[0]?.message?.content ?? ''
|
||||
|
||||
// 提取 JSON(防止 LLM 多输出文字)
|
||||
const match = raw.match(/\{[\s\S]*\}/)
|
||||
if (!match) throw new Error('LLM 返回格式异常')
|
||||
|
||||
const result = JSON.parse(match[0])
|
||||
const score = Math.max(1, Math.min(10, Number(result.score) || 5))
|
||||
const similarity = Math.max(0, Math.min(1, Number(result.similarity) || 0))
|
||||
const penaltyMultiplier = getDuplicatePenaltyMultiplier(similarity)
|
||||
const isDuplicate = similarity >= 0.6
|
||||
|
||||
// 如果重复,效果强制降级或 backfire
|
||||
let effect = result.effect || 'attack_boost'
|
||||
let adjustedScore = score
|
||||
if (isDuplicate && similarity >= 0.8) {
|
||||
effect = 'backfire'
|
||||
adjustedScore = Math.max(1, score - 3)
|
||||
} else if (isDuplicate && similarity >= 0.6) {
|
||||
// 效果打半折:rage_mode→speed_boost,speed_boost→attack_boost,etc.
|
||||
const downgrade: Record<string, string> = {
|
||||
rage_mode: 'speed_boost',
|
||||
speed_boost: 'attack_boost',
|
||||
money_rain: 'attack_boost',
|
||||
attack_boost: 'backfire',
|
||||
backfire: 'backfire',
|
||||
}
|
||||
effect = downgrade[effect] ?? 'backfire'
|
||||
adjustedScore = Math.max(1, score - 2)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
score,
|
||||
score: adjustedScore,
|
||||
title: result.title || '打鸡血',
|
||||
desc: result.desc || '还行吧',
|
||||
effect: result.effect || 'attack_boost',
|
||||
effect,
|
||||
similarity,
|
||||
similarTo: result.similarTo || '',
|
||||
isDuplicate,
|
||||
penaltyMultiplier, // 前端据此额外扣 HC
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[pua-score]', e)
|
||||
// 降级:随机给个分数
|
||||
const score = Math.floor(Math.random() * 7) + 3
|
||||
return NextResponse.json({
|
||||
score,
|
||||
score: 3,
|
||||
title: '随机鸡血',
|
||||
desc: 'AI开小差了,随机发力',
|
||||
effect: 'attack_boost',
|
||||
similarity: 0,
|
||||
similarTo: '',
|
||||
isDuplicate: false,
|
||||
penaltyMultiplier: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user