diff --git a/config/blacklist-db.js b/config/blacklist-db.js index 294c2c3..32146da 100644 --- a/config/blacklist-db.js +++ b/config/blacklist-db.js @@ -44,7 +44,14 @@ async function loadBlacklistFromDB() { offensive: [], titles: [], brands: [], - inappropriate: [] + inappropriate: [], + racial: [], + religious: [], + disability: [], + leetspeak: [], + cyberbullying: [], + drugs: [], + violence: [] }; // Erstelle neuen Trigram-Index @@ -101,7 +108,14 @@ function getStaticBlacklist() { 'sex', 'porn', 'porno', 'fuck', 'shit', 'bitch', 'whore', 'prostitute', 'drug', 'cocaine', 'heroin', 'marijuana' - ] + ], + racial: [], + religious: [], + disability: [], + leetspeak: [], + cyberbullying: [], + drugs: [], + violence: [] }; } @@ -219,7 +233,14 @@ function getCategoryReason(category) { offensive: 'Beleidigender oder anstößiger Begriff', titles: 'Titel oder Berufsbezeichnung', brands: 'Markenname', - inappropriate: 'Unpassender Begriff' + inappropriate: 'Unpassender Begriff', + racial: 'Rassistischer oder ethnisch beleidigender Begriff', + religious: 'Religiös beleidigender oder blasphemischer Begriff', + disability: 'Beleidigender Begriff bezüglich Behinderungen', + leetspeak: 'Verschleierter beleidigender Begriff', + cyberbullying: 'Cyberbullying oder Online-Belästigung', + drugs: 'Drogenbezogener Begriff', + violence: 'Gewalt- oder bedrohungsbezogener Begriff' }; return reasons[category] || 'Unzulässiger Begriff'; diff --git a/config/llm-blacklist.js b/config/llm-blacklist.js deleted file mode 100644 index 80e6d87..0000000 --- a/config/llm-blacklist.js +++ /dev/null @@ -1,253 +0,0 @@ -/** - * LLM-basierte Blacklist-Prüfung mit Ollama - * Verwendet ein lokales LLM zur intelligenten Bewertung von Namen - */ - -const axios = require('axios'); - -// Ollama-Konfiguration -const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || 'http://localhost:11434'; -const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'llama3.2:3b'; // Schnelles, kleines Modell - -/** - * Prüft einen Namen mit dem LLM - * @param {string} firstname - Vorname - * @param {string} lastname - Nachname - * @returns {Object} - {isBlocked: boolean, reason: string, confidence: number} - */ -async function checkNameWithLLM(firstname, lastname) { - if (!firstname || !lastname) { - return { isBlocked: false, reason: '', confidence: 0 }; - } - - try { - const fullName = `${firstname} ${lastname}`; - - // Prompt für das LLM - const prompt = `Du bist ein strenger Moderator für ein Spielsystem. Prüfe ob der Name "${fullName}" für die Verwendung geeignet ist. - -WICHTIG: Blockiere ALLE Namen die: -- Historisch belastet sind (Adolf Hitler, Stalin, Mussolini, etc.) -- Beleidigend oder anstößig sind (Satan, Idiot, etc.) -- Unpassende Titel sind (Dr., Professor, etc.) -- Markennamen sind (Coca-Cola, Nike, etc.) -- Andere unangemessene Inhalte haben - -Antworte NUR mit "TRUE" (blockiert) oder "FALSE" (erlaubt) - keine Erklärungen. - -Name: "${fullName}" -Antwort:`; - - // Ollama API-Aufruf - const response = await axios.post(`${OLLAMA_BASE_URL}/api/generate`, { - model: OLLAMA_MODEL, - prompt: prompt, - stream: false, - options: { - temperature: 0.1, // Niedrige Temperatur für konsistente Antworten - top_p: 0.9, - max_tokens: 10 // Nur TRUE/FALSE erwartet - } - }, { - timeout: 10000 // 10 Sekunden Timeout - }); - - const llmResponse = response.data.response.trim().toUpperCase(); - - // Parse LLM-Antwort - let isBlocked = false; - let reason = ''; - let confidence = 0.8; // Standard-Konfidenz für LLM - - if (llmResponse === 'TRUE') { - isBlocked = true; - reason = 'Name wurde vom KI-Moderator als ungeeignet eingestuft'; - } else if (llmResponse === 'FALSE') { - isBlocked = false; - reason = 'Name wurde vom KI-Moderator als geeignet eingestuft'; - } else { - // Fallback bei unerwarteter Antwort - console.warn(`Unerwartete LLM-Antwort: "${llmResponse}" für Name: "${fullName}"`); - isBlocked = false; - reason = 'KI-Moderator konnte Name nicht eindeutig bewerten'; - confidence = 0.3; - } - - return { - isBlocked, - reason, - confidence, - llmResponse: llmResponse, - matchType: 'llm' - }; - - } catch (error) { - console.error('Error checking name with LLM:', error); - - // Fallback bei LLM-Fehlern - return { - isBlocked: false, - reason: 'KI-Moderator nicht verfügbar - Name wurde erlaubt', - confidence: 0.1, - error: error.message, - matchType: 'llm-error' - }; - } -} - -/** - * Testet die LLM-Verbindung - * @returns {Object} - {connected: boolean, model: string, error?: string} - */ -async function testLLMConnection() { - try { - const response = await axios.post(`${OLLAMA_BASE_URL}/api/generate`, { - model: OLLAMA_MODEL, - prompt: 'Test', - stream: false, - options: { - max_tokens: 1 - } - }, { - timeout: 5000 - }); - - return { - connected: true, - model: OLLAMA_MODEL, - baseUrl: OLLAMA_BASE_URL - }; - } catch (error) { - return { - connected: false, - model: OLLAMA_MODEL, - baseUrl: OLLAMA_BASE_URL, - error: error.message - }; - } -} - -/** - * Erweiterte LLM-Prüfung mit Kontext - * @param {string} firstname - Vorname - * @param {string} lastname - Nachname - * @param {string} context - Zusätzlicher Kontext (optional) - * @returns {Object} - Prüfungsergebnis - */ -async function checkNameWithContext(firstname, lastname, context = '') { - if (!firstname || !lastname) { - return { isBlocked: false, reason: '', confidence: 0 }; - } - - try { - const fullName = `${firstname} ${lastname}`; - - // Erweiterter Prompt mit Kontext - const prompt = `Du bist ein Moderator für ein Spielsystem. Prüfe ob der Name "${fullName}" für die Verwendung geeignet ist. - -Kontext: ${context || 'Standard-Spielname'} - -Beurteile den Namen basierend auf: -- Historisch belastete Namen (z.B. Adolf Hitler, Stalin, etc.) -- Beleidigende oder anstößige Begriffe -- Unpassende Titel oder Berufsbezeichnungen -- Markennamen die nicht verwendet werden sollten -- Andere unangemessene Inhalte - -Antworte NUR mit "TRUE" oder "FALSE" - keine Erklärungen. - -Name: "${fullName}" -Antwort:`; - - const response = await axios.post(`${OLLAMA_BASE_URL}/api/generate`, { - model: OLLAMA_MODEL, - prompt: prompt, - stream: false, - options: { - temperature: 0.1, - top_p: 0.9, - max_tokens: 10 - } - }, { - timeout: 10000 - }); - - const llmResponse = response.data.response.trim().toUpperCase(); - - let isBlocked = false; - let reason = ''; - let confidence = 0.8; - - if (llmResponse === 'TRUE') { - isBlocked = true; - reason = 'Name wurde vom KI-Moderator als ungeeignet eingestuft'; - } else if (llmResponse === 'FALSE') { - isBlocked = false; - reason = 'Name wurde vom KI-Moderator als geeignet eingestuft'; - } else { - console.warn(`Unerwartete LLM-Antwort: "${llmResponse}" für Name: "${fullName}"`); - isBlocked = false; - reason = 'KI-Moderator konnte Name nicht eindeutig bewerten'; - confidence = 0.3; - } - - return { - isBlocked, - reason, - confidence, - llmResponse: llmResponse, - matchType: 'llm-context', - context: context - }; - - } catch (error) { - console.error('Error checking name with LLM context:', error); - - return { - isBlocked: false, - reason: 'KI-Moderator nicht verfügbar - Name wurde erlaubt', - confidence: 0.1, - error: error.message, - matchType: 'llm-error' - }; - } -} - -/** - * Batch-Prüfung mehrerer Namen - * @param {Array} names - Array von {firstname, lastname} Objekten - * @returns {Array} - Array von Prüfungsergebnissen - */ -async function checkNamesBatch(names) { - const results = []; - - for (const name of names) { - try { - const result = await checkNameWithLLM(name.firstname, name.lastname); - results.push({ - ...name, - ...result - }); - } catch (error) { - results.push({ - ...name, - isBlocked: false, - reason: 'Fehler bei der Prüfung', - confidence: 0, - error: error.message, - matchType: 'error' - }); - } - } - - return results; -} - -module.exports = { - checkNameWithLLM, - checkNameWithContext, - checkNamesBatch, - testLLMConnection, - OLLAMA_BASE_URL, - OLLAMA_MODEL -}; diff --git a/public/admin-dashboard.html b/public/admin-dashboard.html index d17bf0b..1262d1c 100644 --- a/public/admin-dashboard.html +++ b/public/admin-dashboard.html @@ -106,12 +106,6 @@ - -
-

🧠 KI-Moderator

-

Intelligente Namensprüfung mit Ollama LLM

- -
@@ -211,6 +205,13 @@ + + + + + + +
@@ -237,49 +238,6 @@ - - diff --git a/public/js/admin-dashboard.js b/public/js/admin-dashboard.js index 0357a12..6c2b510 100644 --- a/public/js/admin-dashboard.js +++ b/public/js/admin-dashboard.js @@ -893,7 +893,14 @@ function getCategoryIcon(category) { offensive: '⚠️', titles: '👑', brands: '🏷️', - inappropriate: '🚫' + inappropriate: '🚫', + racial: '🌍', + religious: '⛪', + disability: '♿', + leetspeak: '🔤', + cyberbullying: '💻', + drugs: '💊', + violence: '⚔️' }; return icons[category] || '📝'; } @@ -904,7 +911,14 @@ function getCategoryDisplayName(category) { offensive: 'Beleidigend/anstößig', titles: 'Titel/Berufsbezeichnung', brands: 'Markenname', - inappropriate: 'Unpassend' + inappropriate: 'Unpassend', + racial: 'Rassistisch/ethnisch', + religious: 'Religiös beleidigend', + disability: 'Behinderungsbezogen', + leetspeak: 'Verschleiert', + cyberbullying: 'Cyberbullying', + drugs: 'Drogenbezogen', + violence: 'Gewalt/Bedrohung' }; return names[category] || category; } @@ -1103,196 +1117,7 @@ function displayBlacklistStats(stats) { statsDiv.innerHTML = html; } -// LLM-Management -function showLLMManagement() { - document.getElementById('llmModal').style.display = 'block'; - loadLLMStatus(); -} -function closeLLMModal() { - document.getElementById('llmModal').style.display = 'none'; -} -// LLM-Status laden -async function loadLLMStatus() { - try { - const response = await fetch('/api/v1/admin/llm/status', { - headers: { - 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` - } - }); - const result = await response.json(); - const statusContent = document.getElementById('llmStatusContent'); - if (result.success) { - const status = result.data; - if (status.connected) { - statusContent.innerHTML = ` -
- ✅ KI-Moderator verbunden
- Modell: ${status.model}
- URL: ${status.baseUrl} -
- `; - } else { - statusContent.innerHTML = ` -
- ❌ KI-Moderator nicht verfügbar
- Modell: ${status.model}
- URL: ${status.baseUrl}
- Fehler: ${status.error} -
- `; - } - } else { - statusContent.innerHTML = ` -
- ❌ Fehler beim Laden des Status
- ${result.message} -
- `; - } - } catch (error) { - console.error('Error loading LLM status:', error); - document.getElementById('llmStatusContent').innerHTML = ` -
- ❌ Fehler beim Laden des Status
- ${error.message} -
- `; - } -} - -// Name mit LLM testen -async function testNameWithLLM() { - const firstname = document.getElementById('llmFirstname').value.trim(); - const lastname = document.getElementById('llmLastname').value.trim(); - const context = document.getElementById('llmContext').value.trim(); - - if (!firstname || !lastname) { - showLLMMessage('Bitte gib Vorname und Nachname ein', 'error'); - return; - } - - try { - // LLM-Test - const llmResponse = await fetch('/api/v1/admin/llm/test', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` - }, - body: JSON.stringify({ firstname, lastname, context }) - }); - - const llmResult = await llmResponse.json(); - - // Blacklist-Test zum Vergleich - const blacklistResponse = await fetch('/api/v1/admin/blacklist/test', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` - }, - body: JSON.stringify({ firstname, lastname }) - }); - - const blacklistResult = await blacklistResponse.json(); - - // Ergebnisse anzeigen - displayLLMResults(llmResult, blacklistResult); - - } catch (error) { - console.error('Error testing name with LLM:', error); - showLLMMessage('Fehler beim Testen: ' + error.message, 'error'); - } -} - -// LLM-Ergebnisse anzeigen -function displayLLMResults(llmResult, blacklistResult) { - const resultDiv = document.getElementById('llmResult'); - const resultContent = document.getElementById('llmResultContent'); - const comparisonDiv = document.getElementById('llmComparison'); - const comparisonContent = document.getElementById('llmComparisonContent'); - - if (llmResult.success) { - const llm = llmResult.data; - - let llmStatus = ''; - if (llm.isBlocked) { - llmStatus = ` -
- ❌ Name blockiert
- Grund: ${llm.reason}
- Konfidenz: ${Math.round(llm.confidence * 100)}%
- LLM-Antwort: "${llm.llmResponse}"
- Typ: ${llm.matchType} -
- `; - } else { - llmStatus = ` -
- ✅ Name erlaubt
- Grund: ${llm.reason}
- Konfidenz: ${Math.round(llm.confidence * 100)}%
- LLM-Antwort: "${llm.llmResponse}"
- Typ: ${llm.matchType} -
- `; - } - - resultContent.innerHTML = llmStatus; - resultDiv.style.display = 'block'; - - // Vergleich mit Blacklist - if (blacklistResult.success) { - const blacklist = blacklistResult.data; - let comparisonStatus = ''; - - if (blacklist.combined.isBlocked) { - comparisonStatus = ` -
- ⚠️ Name blockiert (${blacklist.combined.source})
- Grund: ${blacklist.combined.reason} -
- `; - } else { - comparisonStatus = ` -
- ✅ Name erlaubt
- Sowohl KI als auch Blacklist erlauben den Namen -
- `; - } - - comparisonContent.innerHTML = comparisonStatus; - comparisonDiv.style.display = 'block'; - } - - } else { - resultContent.innerHTML = ` -
- ❌ Fehler beim Testen
- ${llmResult.message} -
- `; - resultDiv.style.display = 'block'; - } -} - -// LLM-Nachricht anzeigen -function showLLMMessage(message, type) { - const resultDiv = document.getElementById('llmResult'); - const resultContent = document.getElementById('llmResultContent'); - - const color = type === 'error' ? '#c62828' : '#2e7d32'; - const bgColor = type === 'error' ? '#ffebee' : '#e8f5e8'; - - resultContent.innerHTML = ` -
- ${type === 'error' ? '❌' : '✅'} ${message} -
- `; - resultDiv.style.display = 'block'; -} diff --git a/routes/api.js b/routes/api.js index 1085e63..4491779 100644 --- a/routes/api.js +++ b/routes/api.js @@ -1019,7 +1019,6 @@ router.post('/v1/private/users/find', requireApiKey, async (req, res) => { // Import blacklist module const { checkNameAgainstBlacklist, addToBlacklist, removeFromBlacklist, getBlacklist } = require('../config/blacklist-db'); -const { checkNameWithLLM, checkNameWithContext, testLLMConnection } = require('../config/llm-blacklist'); // Create new player with RFID and blacklist validation (no auth required for dashboard) router.post('/v1/public/players/create-with-rfid', async (req, res) => { @@ -1034,27 +1033,13 @@ router.post('/v1/public/players/create-with-rfid', async (req, res) => { } try { - // LLM-basierte Blacklist-Prüfung - const llmCheck = await checkNameWithLLM(firstname, lastname); - if (llmCheck.isBlocked) { - return res.status(400).json({ - success: false, - message: `Name nicht zulässig: ${llmCheck.reason}`, - details: llmCheck - }); - } - - // Fallback: Traditionelle Blacklist-Prüfung (optional) + // Blacklist-Prüfung mit Levenshtein-Algorithmus const blacklistCheck = await checkNameAgainstBlacklist(firstname, lastname); if (blacklistCheck.isBlocked) { return res.status(400).json({ success: false, message: `Name nicht zulässig: ${blacklistCheck.reason}`, - details: { - reason: blacklistCheck.reason, - category: blacklistCheck.category, - matchedTerm: blacklistCheck.matchedTerm - } + details: blacklistCheck }); } @@ -1618,22 +1603,16 @@ router.post('/v1/admin/blacklist/test', requireAdminAuth, async (req, res) => { } try { - // LLM-Prüfung - const llmResult = await checkNameWithLLM(firstname, lastname); - - // Traditionelle Blacklist-Prüfung + // Blacklist-Prüfung mit Levenshtein-Algorithmus const blacklistResult = await checkNameAgainstBlacklist(firstname, lastname); res.json({ success: true, data: { - llm: llmResult, blacklist: blacklistResult, - combined: { - isBlocked: llmResult.isBlocked || blacklistResult.isBlocked, - reason: llmResult.isBlocked ? llmResult.reason : blacklistResult.reason, - source: llmResult.isBlocked ? 'llm' : (blacklistResult.isBlocked ? 'blacklist' : 'none') - } + isBlocked: blacklistResult.isBlocked, + reason: blacklistResult.reason, + source: blacklistResult.isBlocked ? 'blacklist' : 'none' } }); } catch (error) {