OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

class-assistant.php

9 KB
<?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)',
        ]);
    }
}