Files
deskhub/DeskHub-Webseite/assistant.php
T
2026-10-23 17:22:26 +00:00

137 lines
7.1 KiB
PHP

<?php
declare(strict_types=1);
/**
* assistant.php: Schnittstelle des digitalen Assistenten im Kundenportal (JSON).
* status -> {active, name, greeting, helpTopics, user}
* ask {message} -> {type: "answer"|"none"|"ticket", ...}
* ticket {name, email, subject, message, helpTopicId?} -> {ok, number}
* Jeder Aufruf braucht den CSRF-Token der Sitzung (csrf_token). Fragen und Tickets sind je Sitzung bzw. Adresse begrenzt.
*/
require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/assistant.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
function assistant_json(array $data, int $status = 200): never
{
http_response_code($status);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
$pdo = get_db();
$action = (string) ($_POST['action'] ?? $_GET['action'] ?? '');
if (!in_array($action, ['status', 'ask', 'ticket', 'feedback'], true)) assistant_json(['error' => 'Unbekannte Aktion.'], 400);
// Anfragen müssen vom eigenen Formular der Seite kommen (Token der Sitzung)
$token = (string) ($_POST['csrf_token'] ?? $_GET['csrf_token'] ?? '');
if ($token === '' || !hash_equals((string) ($_SESSION['csrf_token'] ?? ''), $token)) {
assistant_json(['error' => 'Die Sitzung ist abgelaufen. Bitte laden Sie die Seite neu.', 'csrfExpired' => true], 403);
}
$active = assistant_active($pdo);
$texts = assistant_texts();
if ($action === 'status') {
$user = current_user();
assistant_json([
'active' => $active,
'name' => $texts['name'],
'greeting' => $texts['greeting'],
'noAnswer' => $texts['no_answer'],
'helpTopics' => $active ? assistant_help_topics($pdo) : [],
'user' => $user ? ['name' => (string) ($user['name'] ?? ''), 'email' => (string) ($user['email'] ?? '')] : null,
]);
}
if (!$active) assistant_json(['error' => 'Der Assistent ist gerade nicht verfügbar.', 'active' => false], 409);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') assistant_json(['error' => 'Nur POST.'], 405);
if ($action === 'feedback') {
// Rückmeldung zu einer Antwort ("Das hat geholfen" oder Ticket statt Antwort); nur für Antworten dieser Sitzung
$id = (int) ($_POST['logId'] ?? 0);
if ($id > 0 && in_array($id, array_map('intval', (array) ($_SESSION['assistant_log_ids'] ?? [])), true)) {
try {
$pdo->prepare("UPDATE assistant_log SET helpful = ? WHERE id = ? AND result IN ('custom','kb')")->execute([!empty($_POST['helpful']) ? 1 : 0, $id]);
} catch (Throwable $e) {
// Datenbank noch nicht aktualisiert
}
}
assistant_json(['ok' => true]);
}
if ($action === 'ask') {
// höchstens 30 Fragen je 10 Minuten und Sitzung
$now = time();
$log = array_values(array_filter((array) ($_SESSION['assistant_asks'] ?? []), fn($t) => $t > $now - 600));
if (count($log) >= 30) assistant_json(['error' => 'Das waren viele Fragen in kurzer Zeit. Bitte warten Sie einen Moment.'], 429);
$log[] = $now;
$_SESSION['assistant_asks'] = $log;
$message = trim(mb_substr((string) ($_POST['message'] ?? ''), 0, 300));
if ($message === '') assistant_json(['error' => 'Bitte eine Frage eingeben.'], 422);
// Wunsch nach einem Ticket oder einem Menschen: nicht erst suchen
$norm = assistant_normalize($message);
if (preg_match('/\b(ticket|mitarbeiter|support|beschwerde|reklamation|kundenservice|ansprechpartner)\b/', $norm)
&& preg_match('/\b(erstell|eroeffn|anleg|oeffn|schreib|melden|sprechen|kontakt|will|moechte|brauche|bitte)/', $norm)) {
assistant_log_write($pdo, $message, 'ticket_request');
assistant_json(['type' => 'ticket', 'text' => 'Gern, ich erstelle ein Ticket für Sie.']);
}
// Begrüßung und Dank
if (preg_match('/^(hallo|hi|hey|guten (tag|morgen|abend)|moin|servus)\b/', $norm) && count(assistant_tokens($message)) === 0) {
assistant_json(['type' => 'smalltalk', 'text' => 'Hallo! Was möchten Sie wissen?']);
}
if (preg_match('/^(danke|vielen dank|dankeschoen|super danke|ok danke)\b/', $norm)) {
assistant_json(['type' => 'smalltalk', 'text' => 'Gern geschehen! Kann ich noch etwas für Sie tun?']);
}
$found = assistant_search($pdo, $message);
if (!$found) {
assistant_log_write($pdo, $message, 'none');
assistant_json(['type' => 'none', 'text' => $texts['no_answer']]);
}
$logId = assistant_log_write($pdo, $message, $found['source'] === 'custom' ? 'custom' : 'kb', ['source' => $found['source'], 'id' => $found['id'], 'title' => $found['title']]);
if ($logId) $_SESSION['assistant_log_ids'] = array_slice(array_merge((array) ($_SESSION['assistant_log_ids'] ?? []), [$logId]), -50);
assistant_json(['logId' => $logId, 'type' => 'answer', 'source' => $found['source'], 'title' => $found['title'], 'text' => $found['text'], 'link' => $found['link'], 'others' => $found['others']]);
}
// ── Ticket erstellen ─────────────────────────────────────────────────────────
if (!empty($_POST['hp_website'])) assistant_json(['error' => 'Anfrage abgelehnt.'], 400); // Spam-Falle: das versteckte Feld bleibt bei Menschen leer
$ip = client_ip();
// höchstens 3 Tickets je Stunde und Adresse über den Assistenten
$recent = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE requester_ip = ? AND created_at >= ? AND id IN (SELECT ticket_id FROM activity_log WHERE action LIKE 'Ticket über den digitalen Assistenten%')");
$recent->execute([$ip, date('Y-m-d H:i:s', time() - 3600)]);
if ((int) $recent->fetchColumn() >= 3) assistant_json(['error' => 'Über den Assistenten wurden bereits mehrere Tickets erstellt. Bitte versuchen Sie es später noch einmal oder antworten Sie auf die E-Mail zu Ihrem Ticket.'], 429);
$in = [
'name' => trim((string) ($_POST['name'] ?? '')),
'email' => trim((string) ($_POST['email'] ?? '')),
'subject' => trim((string) ($_POST['subject'] ?? '')),
'message' => trim((string) ($_POST['message'] ?? '')),
];
$topicId = (int) ($_POST['helpTopicId'] ?? 0);
if ($topicId > 0) {
$ok = false;
foreach (assistant_help_topics($pdo) as $t) if ($t['id'] === $topicId) $ok = true;
if (!$ok) assistant_json(['error' => 'Das gewählte Thema gibt es nicht.'], 422);
$in['helpTopicId'] = $topicId;
}
// Abteilung und Priorität für Assistenten-Tickets (Einstellung); ein gewähltes Hilfethema bestimmt sie selbst
if ($topicId <= 0) {
$dept = (int) get_setting('assistant_department_id', '0');
$prio = (int) get_setting('assistant_priority_id', '0');
if ($dept > 0) $in['departmentId'] = $dept;
if ($prio > 0) $in['priorityId'] = $prio;
}
try {
$created = public_ticket_create($pdo, $in, $ip, ['activity' => 'Ticket über den digitalen Assistenten erstellt', 'tag' => 'Assistent']);
} catch (AdminModelException $e) {
assistant_json(['error' => $e->getMessage()], $e->httpStatus());
}
assistant_log_write($pdo, mb_substr($in['subject'], 0, 300), 'ticket_created', null, $created['number']);
assistant_json(['ok' => true, 'number' => $created['number']], 201);