feat: generate spam rules from text with ai

This commit is contained in:
ngfchl
2026-07-07 19:45:25 +08:00
parent 3e5936653b
commit 39d39b07f9
7 changed files with 522 additions and 358 deletions
+40 -2
View File
@@ -29,6 +29,7 @@ const spamTesting = ref(false);
const spamTestResult = ref<Record<string, any> | null>(null);
const ruleForm = reactive({ type: 'keyword', pattern: '', score: 75 });
const ruleSaving = ref(false);
const generatedRuleHint = ref('');
const spamRules = reactive({ keywords: [] as Record<string, any>[], regex: [] as Record<string, any>[], adblock_domains: [] as Record<string, any>[], counts: { keywords: 0, regex: 0, adblock_domains: 0 } });
const rulesLoading = ref(false);
const ruleKeyword = ref('');
@@ -224,7 +225,11 @@ async function logout() {
async function fetchJson(url: string, options?: RequestInit) {
const res = await fetch(url, options);
const data = await res.json().catch(() => ({}));
if (!res.ok || data.ok === false) throw new Error(data.error || data.message || `HTTP ${res.status}`);
if (!res.ok || data.ok === false) {
const err: any = new Error(data.error || data.message || `HTTP ${res.status}`);
err.data = data;
throw err;
}
return data;
}
async function loadChatHistory() {
@@ -441,6 +446,7 @@ async function saveSpamRule() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ruleForm)
});
generatedRuleHint.value = '';
showToast(`规则已写入,关键词 ${result.rule_counts?.keywords || '-'} / 正则 ${result.rule_counts?.regex || '-'}`);
ruleForm.pattern = '';
await loadSpamRules();
@@ -449,6 +455,37 @@ async function saveSpamRule() {
finally { ruleSaving.value = false; }
}
async function addSpamTextToRules() {
if (!spamTestText.value.trim()) return showToast('请输入要入库的聊天内容');
if (ruleSaving.value) return;
ruleSaving.value = true;
try {
const result = await fetchJson('/api/spam-rules/from-text', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: spamTestText.value, use_ai: spamUseAi.value })
});
const generated = result.generated_rule || {};
ruleForm.type = result.type || generated.type || 'keyword';
ruleForm.pattern = result.pattern || generated.pattern || '';
ruleForm.score = Number(result.score || generated.score || 90);
generatedRuleHint.value = `${generated.ai_used === false ? '兜底生成' : 'AI生成'}${ruleForm.type === 'regex' ? '正则' : '关键词'} / ${ruleForm.score} / ${ruleForm.pattern}${generated.reason ? `${generated.reason}` : ''}`;
showToast(`${generated.ai_used === false ? '兜底' : 'AI'}生成并写入规则:${ruleForm.pattern}`);
await loadSpamRules();
await testSpamText();
} catch (e: any) {
const generated = e?.data?.generated_rule;
if (generated?.pattern) {
ruleForm.type = generated.type || 'keyword';
ruleForm.pattern = generated.pattern;
ruleForm.score = Number(generated.score || 90);
generatedRuleHint.value = `已生成但未写入:${ruleForm.pattern}${generated.reason ? `${generated.reason}` : ''}`;
}
showToast(`内容入库失败:${e.message}`);
}
finally { ruleSaving.value = false; }
}
function reloadByGroup() { loadChatHistory(); loadBans(); loadStats(); loadShopItems(); loadShopTransactions(); loadLotteries(); }
function connectSSE() {
@@ -878,8 +915,9 @@ onBeforeUnmount(() => {
<div class="rule-action-bar">
<label class="switch-line compact"><input v-model="spamUseAi" type="checkbox" /> AI 兜底</label>
<SButtonLoading color="primary" :loading="spamTesting" :disabled="spamTesting" @click="testSpamText">开始检测</SButtonLoading>
<SButtonLoading variant="outline" color="warning" :loading="ruleSaving" :disabled="ruleSaving || !spamTestText.trim()" @click="ruleForm.pattern = spamTestText; ruleForm.type = 'keyword'; ruleForm.score = 90; saveSpamRule()">内容入库</SButtonLoading>
<SButtonLoading variant="outline" color="warning" :loading="ruleSaving" :disabled="ruleSaving || !spamTestText.trim()" @click="addSpamTextToRules">AI生成规则并入库</SButtonLoading>
</div>
<div v-if="generatedRuleHint" class="generated-rule-hint">{{ generatedRuleHint }}</div>
<div class="rule-write-grid inline-rule-writer compact-rule-writer">
<label>类型<select v-model="ruleForm.type" class="form-select"><option value="keyword">关键词</option><option value="regex">正则</option></select></label>
<label>分数<SInput v-model="ruleForm.score" type="number" placeholder="75" /></label>
+2
View File
@@ -549,3 +549,5 @@ body { background: #f3f6fb; }
.overview-ban-list .ban-meta { width: 100%; display: flex !important; align-items: center; flex-wrap: nowrap !important; gap: 5px !important; font-size: 10px !important; line-height: 1.2; white-space: nowrap !important; overflow: hidden; }
.overview-ban-list .ban-meta > span { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.overview-ban-list .ban-meta .s-button { height: 22px; min-height: 22px; padding: 0 8px; font-size: 10px; }
.generated-rule-hint { margin-top: 6px; padding: 6px 9px; border-radius: 10px; background: #f0fdf4; color: #166534; border: 1px solid rgba(34,197,94,.24); font-size: 12px; line-height: 1.35; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }