Quelltext des aktuellen Standes
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* Website-KI-Assistent.
*
* Stellt den OpenNIT-Bot als schwebende Bubble (sitewide, optional) und als
* Shortcode [opennit_assistant] bereit. Der Browser spricht NICHT direkt mit
* OpenNIT – alle Anfragen laufen server-seitig über admin-ajax, damit Token
* und Signing-Secret das WordPress-Backend nie verlassen.
*/
class OpenNIT_Assistant {
public function register(): void {
add_shortcode('opennit_assistant', [$this, 'render_widget']);
add_action('wp_ajax_opennit_assistant_ask', [$this, 'ajax_ask']);
add_action('wp_ajax_nopriv_opennit_assistant_ask', [$this, 'ajax_ask']);
add_action('wp_enqueue_scripts', [$this, 'enqueue']);
add_action('wp_footer', [$this, 'render_bubble']);
}
/** @var array|null Zwischengespeicherte Assistent-Einstellungen pro Request. */
private ?array $assistant_settings = null;
/**
* Assistent-Anzeige-Einstellungen aus der OpenNIT-Konfiguration (Admin → API),
* die das Plugin über /config bezieht (gecacht). Fällt auf Defaults zurück,
* falls die Konfiguration nicht erreichbar ist.
*/
private function settings(): array {
if ($this->assistant_settings !== null) {
return $this->assistant_settings;
}
$defaults = [
'bubble' => false,
'logged_in_only' => false,
'title' => 'IT-Assistent',
'welcome' => 'Hallo! Ich helfe bei einfachen PC-Fragen und unserer Dokumentation.',
'icon' => '',
];
$client = new OpenNIT_ApiClient();
$config = $client->get_config();
$a = (!is_wp_error($config) && isset($config['assistant']) && is_array($config['assistant']))
? $config['assistant'] : [];
$this->assistant_settings = [
'bubble' => !empty($a['bubble']),
'logged_in_only' => !empty($a['logged_in_only']),
'title' => (string) ($a['title'] ?? $defaults['title']),
'welcome' => (string) ($a['welcome'] ?? $defaults['welcome']),
'icon' => esc_url_raw((string) ($a['icon'] ?? '')),
];
return $this->assistant_settings;
}
private function bubble_enabled(): bool {
$s = $this->settings();
if (empty($s['bubble'])) {
return false;
}
// Optional: Bubble nur für angemeldete WordPress-Nutzer anzeigen.
if (!empty($s['logged_in_only']) && !is_user_logged_in()) {
return false;
}
return true;
}
/** Wird der Assistent auf dieser Seite gebraucht (Bubble aktiv oder Shortcode vorhanden)? */
private function needed(): bool {
if ($this->bubble_enabled()) {
return true;
}
global $post;
return is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'opennit_assistant');
}
public function enqueue(): void {
if (!$this->needed()) {
return;
}
$s = $this->settings();
wp_enqueue_style('opennit-connector', OPENNIT_PLUGIN_URL . 'assets/style.css', [], OPENNIT_VERSION);
wp_enqueue_script('opennit-assistant', OPENNIT_PLUGIN_URL . 'assets/js/assistant.js', [], OPENNIT_VERSION, true);
wp_localize_script('opennit-assistant', 'OpenNITAssistant', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('opennit_nonce'),
'title' => $s['title'],
'welcome' => $s['welcome'],
'icon' => $s['icon'],
]);
}
/** Inline-Variante: [opennit_assistant] */
public function render_widget($atts = []): string {
return '<div class="opennit-assistant" data-opennit-assistant="inline"></div>';
}
/** Floating-Bubble im Footer (wenn aktiviert). */
public function render_bubble(): void {
if (!$this->bubble_enabled()) {
return;
}
echo '<div class="opennit-assistant-bubble-root" data-opennit-assistant="bubble"></div>';
}
/** AJAX-Proxy: Browser → WordPress → OpenNIT (signiert, server-seitig). */
public function ajax_ask(): void {
check_ajax_referer('opennit_nonce', 'nonce');
if (!opennit_rate_limit('assistant', 20)) {
wp_send_json_error('Zu viele Anfragen. Bitte warten Sie einen Moment.');
return;
}
$message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : '';
if ($message === '') {
wp_send_json_error('Bitte eine Frage eingeben.');
return;
}
if (mb_strlen($message) > 1000) {
wp_send_json_error('Die Frage ist zu lang.');
return;
}
// Konversationsverlauf (für Mehr-Runden-Dialoge, z. B. Ticket-Erstellung).
$history = [];
if (isset($_POST['history'])) {
$decoded = json_decode((string) wp_unslash($_POST['history']), true);
if (is_array($decoded)) {
foreach (array_slice($decoded, -12) as $h) {
if (!is_array($h)) {
continue;
}
$role = (($h['role'] ?? '') === 'assistant') ? 'assistant' : 'user';
$content = sanitize_textarea_field((string) ($h['content'] ?? ''));
if ($content === '') {
continue;
}
$history[] = ['role' => $role, 'content' => mb_substr($content, 0, 2000)];
}
}
}
// Benutzerkontext: E-Mail des angemeldeten WordPress-Nutzers (server-seitig,
// vertrauenswürdig – nie aus dem Browser übernommen).
$user_email = '';
if (is_user_logged_in()) {
$current = wp_get_current_user();
$user_email = sanitize_email((string) ($current->user_email ?? ''));
}
// Der zusätzliche Kontext wird jetzt zentral im OpenNIT-Backend gepflegt
// (Admin → API) und dort serverseitig berücksichtigt – nicht mehr hier.
$client = new OpenNIT_ApiClient();
$result = $client->ask_assistant($message, $history, $user_email);
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
return;
}
$answer = (string) ($result['answer'] ?? '');
$action = is_array($result['action'] ?? null) ? $result['action'] : null;
// Hat der Bot ein Ticket vorbereitet, legen wir es serverseitig über den
// regulären /tickets-Endpunkt an (benötigt Token-Scope "tickets").
if ($action !== null && ($action['type'] ?? '') === 'create_ticket') {
// Vertrauenswürdige E-Mail bevorzugen, sonst die im Dialog genannte.
$from_email = $user_email !== '' ? $user_email : sanitize_email((string) ($action['email'] ?? ''));
if (!is_email($from_email)) {
wp_send_json_success([
'answer' => 'Um ein Ticket zu erstellen, brauche ich noch deine E-Mail-Adresse. Wie lautet sie?',
]);
return;
}
$subject = sanitize_text_field((string) ($action['subject'] ?? ''));
$description = sanitize_textarea_field((string) ($action['description'] ?? ''));
if ($subject === '') {
$subject = 'Support-Anfrage über den Assistenten';
}
$ticket = $client->create_ticket([
'subject' => $subject,
'description' => $description,
'category_name' => sanitize_text_field((string) ($action['category'] ?? '')),
'location_name' => sanitize_text_field((string) ($action['location'] ?? '')),
'from_email' => $from_email,
// E-Mail zusätzlich als signatur-relevantes Feld mitgeben.
'email' => $from_email,
]);
if (is_wp_error($ticket)) {
wp_send_json_success([
'answer' => ($answer !== '' ? $answer . "\n\n" : '')
. 'Das Ticket konnte leider nicht erstellt werden: ' . $ticket->get_error_message(),
]);
return;
}
$tid = (int) ($ticket['ticket_id'] ?? 0);
$confirm = $tid > 0
? sprintf('✅ Ich habe Ticket #%d für dich erstellt. Das IT-Team kümmert sich darum – eine Bestätigung geht an %s.', $tid, $from_email)
: '✅ Dein Ticket wurde erstellt. Das IT-Team kümmert sich darum.';
wp_send_json_success([
'answer' => ($answer !== '' ? $answer . "\n\n" : '') . $confirm,
]);
return;
}
wp_send_json_success([
'answer' => $answer !== '' ? $answer : '(keine Antwort)',
]);
}
}