Zum Inhalt

Entwicklerhandbuch

Dieses Kapitel richtet sich an Integratoren und Entwickler, die nt-ai erweitern oder einbetten.

Architekturüberblick

flowchart TB
    subgraph BE["TYPO3-Backend"]
        MOD["Backend-Module<br/>Audit · Alt-Text · Seiten-Score · Lighthouse"]
        QG["Quality-Gate-Hook"]
        DW["Dashboard-Widgets<br/>Token / Kosten"]
        CFG["ConfigurationService"]
        AIS["AiService"]
        AUD["AuditService"]
    end

    subgraph PROV["KI-Anbieter (extern)"]
        OPENAI["OpenAI"]
        OTHER["Anthropic · Gemini · u. a."]
    end

    subgraph FE["Website-Frontend"]
        SET["Site-Einstellungen"]
        INL["Inline-Config-Script<br/>data-nt-* am html-Tag"]
        WID["a11y-widget.js<br/>Launcher + Panel"]
        CSS["CSS-Feature-Layer<br/>+ nt-dark-theme.css"]
        TTS["tts.js<br/>Vorlesen"]
        BR["Browser-Stimmen<br/>Web Speech, lokal"]
    end

    subgraph EP["nt-ai Frontend-Endpunkte (PSR-15)"]
        TM["TtsMiddleware<br/>/nt-ai/tts"]
        TS["TtsService<br/>Rate-Limit + Cache"]
        APM["AuditPreviewMiddleware"]
    end

    subgraph STORE["Speicher"]
        DB[("tx_ntai_* Tabellen")]
        CT[("Cache: ntai_tts")]
    end

    MOD --> AIS --> PROV
    AUD --> PROV
    AIS --> CFG
    MOD --> AUD
    MOD --> DB
    DW --> DB
    QG --> AUD

    SET --> INL --> WID
    INL --> TTS
    WID --> CSS
    TTS -->|"Web Speech"| BR
    TTS -->|"Cloud"| TM --> TS --> OPENAI
    TS --> CT
    TS --> DB

nt-ai hat zwei weitgehend unabhängige Schichten:

  1. KI-Service-SchichtAiService ist der Einstiegspunkt. Er wählt einen Anbieter anhand der Konfiguration aus, erstellt Prompts über PromptBuilder und ruft die Methode complete() oder describeImage() des Anbieters auf. Anbieter implementieren AiProviderInterface und sind tagged Services. Das ProviderRegistry löst sie anhand des Bezeichners auf.

  2. Audit-SchichtAuditService ist der Einstiegspunkt. Er lädt das gerenderte HTML über PageFetcher, parst es mit DomLoader und führt jede getaggte RuleInterface-Implementierung dagegen aus. Die Ergebnisse werden zu einem Report aggregiert.

  3. Frontend-Schicht — buildfreie Assets (a11y-widget.js/.css, tts.js, nt-dark-theme.css), die über das Sitepackage-Set eingebunden werden. Das Widget schreibt reine data-nt-*-Attribute auf den html-Tag (kein DOM-Rewriting); die Vorlesefunktion nutzt Web Speech (lokal) oder ruft den PSR-15-Endpunkt TtsMiddleware (/nt-ai/tts) auf, der über TtsService beim Anbieter generiert, cacht (ntai_tts) und pro IP rate-limitiert. Das Inline-Config-Skript (data-nt-* am <html>-Tag) wird über page.jsInline ausgeliefert, damit es bei aktiver Content-Security-Policy automatisch die Request-Nonce erhält und nicht blockiert wird.

Diese Schichten können unabhängig voneinander aus PHP, CLI, AJAX, Frontend oder Event-Listenern verwendet werden.

AiService verwenden

Netthinks\NtAi\Service\AiService injizieren und die High-Level-Methoden aufrufen:

use Netthinks\NtAi\Service\AiService;

final class WelcomeMessageController
{
    public function __construct(private readonly AiService $ai) {}

    public function action(): string
    {
        $response = $this->ai->generate(
            userPrompt: 'Write a friendly welcome message for new visitors.',
            language: 'de',
        );
        return $response->text;
    }
}

Verfügbare Methoden

Methode Beschreibung
generate(string $prompt, ?string $providerId = null, ?string $language = null) Freiformige Texterstellung.
translate(string $text, string $targetLanguage, ?string $sourceLanguage = null, ?string $providerId = null) Übersetzt $text in $targetLanguage. Erhält Formatierung und Platzhalter.
summarize(string $text, ?string $language = null, ?string $providerId = null) Gibt eine gekürzte Version von $text zurück.
generateAltText(string $absoluteImagePath, ?string $language = null, ?string $providerId = null, array $optionOverrides = []) Bildbasierte Alt-Text-Generierung. Wendet die konfigurierten Alt-Text-Optionen an, mit optionalen aufrufspezifischen Überschreibungen.
generateAltTextFromText(string $documentText, string $title = '', ?string $language = null, ?string $providerId = null, array $optionOverrides = []) Textbasierte Alt-Text-Generierung (z. B. PDF): fasst den übergebenen Dokumenttext zu einem Alt-Text zusammen. Teilt Optionen und Nachbearbeitung mit generateAltText(). PDF-Text extrahiert Netthinks\NtAi\Pdf\PdfTextExtractor::extract().
describeImage(string $absoluteImagePath, ?string $language = null, ?string $providerId = null) Bildbasierte ausführliche Beschreibung / Bildunterschrift.

AiResponse-DTO

Alle Methoden geben AiResult zurück (unveränderliches DTO):

$r->text;              // string — der generierte Inhalt
$r->provider;          // 'anthropic' | 'openai' | 'ollama' | ...
$r->model;             // verwendeter konkreter Modellname
$r->promptTokens;      // int (0 wenn unbekannt)
$r->completionTokens;  // int (0 wenn unbekannt)
$r->getTotalTokens();  // int

Alt-Text-Optionen je Aufruf überschreiben

$response = $this->ai->generateAltText(
    absoluteImagePath: $path,
    language: 'de',
    optionOverrides: [
        'maxLength' => 250,
        'style' => 'detailed',
        'customInstructions' => ['Always mention the camera angle.'],
    ],
);

Aufrufspezifische Überschreibungen haben Vorrang vor der globalen Konfiguration, ändern diese aber nicht.

Einen bestimmten Anbieter auswählen

Wer Netthinks\NtAi\Provider\AiProviderInterface typehinted, bekommt immer den konfigurierten Standardanbieter. Wird ein benannter Anbieter gebraucht — in aller Regel für eine Fallback-Kette —, wird stattdessen Netthinks\NtAi\Provider\AiProviderLocatorInterface injiziert:

use Netthinks\NtAi\Provider\AiProviderLocatorInterface;

final class SummaryService
{
    public function __construct(private readonly AiProviderLocatorInterface $providers) {}

    public function summarize(string $text): string
    {
        foreach (['openai', 'anthropic'] as $name) {
            if (!in_array($name, $this->providers->getAvailableProviders(), true)) {
                continue; // nicht konfiguriert, z. B. kein API-Key
            }

            try {
                return $this->providers->get($name)->complete('Fasse in 3 Sätzen zusammen.', $text)->getText();
            } catch (\Throwable) {
                continue; // nächster Anbieter in der Kette
            }
        }

        throw new \RuntimeException('Kein KI-Anbieter verfügbar.');
    }
}

Die von get() gelieferten Objekte implementieren dasselbe AiProviderInterface — der aufrufende Code ist also für Standard- und festgelegten Anbieter identisch. Sie bleiben an den angeforderten Anbieter gebunden, unabhängig von späteren Konfigurationsänderungen; genau das macht eine Kette verlässlich. getAvailableProviders() liefert die registrierten und konfigurierten Anbieter, getAllProviders() alle registrierten.

Eigenen KI-Anbieter hinzufügen

Netthinks\NtAi\Service\Provider\AiProviderInterface implementieren — das ist das interne Anbieter-Interface, nicht das öffentliche Netthinks\NtAi\Provider\AiProviderInterface, das Konsumenten injizieren:

namespace MyVendor\MyExt\Service;

use Netthinks\NtAi\Service\Provider\AiProviderInterface;

final class MyCustomProvider implements AiProviderInterface
{
    public function getIdentifier(): string { return 'my_custom'; }
    public function isAvailable(): bool { /* Konfiguration prüfen */ }
    public function complete(AiRequest $request): AiResponse { /* API aufrufen */ }
    public function describeImage(AiRequest $request, array $imagePaths): AiResponse
    {
        // UnsupportedCapabilityException werfen, wenn Vision nicht unterstützt wird
    }
}

In Configuration/Services.yaml taggen:

services:
  MyVendor\MyExt\Service\MyCustomProvider:
    tags: ['nt_ai.provider']

Der Anbieter wird automatisch von ProviderRegistry erkannt.

Eigene Audit-Regel hinzufügen

Netthinks\NtAi\Audit\Rule\AbstractRule erweitern:

namespace MyVendor\MyExt\Audit;

use Netthinks\NtAi\Audit\Result\Finding;
use Netthinks\NtAi\Audit\Result\Severity;
use Netthinks\NtAi\Audit\Rule\AbstractRule;
use Netthinks\NtAi\Audit\Rule\RuleContext;

final class MyCustomRule extends AbstractRule
{
    public function getId(): string { return 'my.custom.rule'; }
    public function getLabel(): string { return 'Custom rule'; }

    public function check(\DOMDocument $dom, RuleContext $context): array
    {
        $findings = [];
        foreach ($this->xpath($dom, '//figure[not(figcaption)]') as $figure) {
            $findings[] = new Finding(
                ruleId: $this->getId(),
                severity: Severity::Notice,
                message: '<figure> without <figcaption>.',
                suggestion: 'Add a figcaption to provide context.',
                wcagReference: '1.3.1 Info and Relationships',
                snippet: $this->snippet($figure),
                selector: $this->selector($figure),
            );
        }
        return $findings;
    }
}

In Configuration/Services.yaml taggen:

services:
  MyVendor\MyExt\Audit\MyCustomRule:
    tags: ['nt_ai.audit.rule']

AbstractRule stellt Hilfsmethoden bereit (xpath, snippet, selector, visibleText), sodass DOM-APIs selten direkt benötigt werden.

Integrierte Regeln

Regel-ID KI? Was geprüft wird
img.alt.missing Nein Fehlende / leere / Platzhalter-Alt-Attribute (WCAG 1.1.1).
heading.hierarchy Nein Fehlendes h1, Ebenensprünge, leere Überschriften (WCAG 1.3.1, 2.4.6).
link.text Nein Leere Links, generische Linktexte („here", „click here", „hier klicken"), reine URL als Text (WCAG 2.4.4).
lang.attribute Nein Vorhandensein und Gültigkeit von <html lang> (WCAG 3.1.1).
form.label Nein Formularelemente ohne zugehörige Labels (WCAG 3.3.2).
page.title Nein Fehlender, leerer oder zu kurzer <title> (WCAG 2.4.2).
contrast.inline Nein Inline-style="color:...;background:..."-Kontrast gegen WCAG AA (1.4.3).
iframe.title Nein <iframe> ohne title-Attribut (WCAG 4.1.2).
button.text Nein Schaltflächen mit leerem oder reinem Icon-Text (WCAG 2.4.4).
duplicate.id Nein Doppelte id-Attribute (WCAG 4.1.1).
document.landmark Nein Fehlende Landmark-Regionen (WCAG 1.3.1 / 2.4.1).
table.header Nein Datentabellen ohne <th>-Kopfzeilen (WCAG 1.3.1).
focus.outline Nein Elemente mit outline: none oder outline: 0 (WCAG 2.4.7).
aria.hidden.focus Nein Fokussierbare Elemente innerhalb von aria-hidden-Containern (WCAG 4.1.2).
video.captions Nein <video>-Elemente ohne <track kind="captions"> (WCAG 1.2.2).
meta.refresh Nein <meta http-equiv="refresh"> mit Zeitlimit (WCAG 2.2.1).
readability.sentence Nein Übermäßig lange Sätze (WCAG 3.1.5 AAA).
readability.paragraph Nein Übermäßig lange Absätze (WCAG 3.1.5 AAA).
readability.subheading Nein Große Textblöcke ohne Zwischenüberschriften (WCAG 3.1.5 AAA).
seo.title.length Nein SEO-Titel zu kurz oder zu lang (WCAG 2.4.2 / SEO).
seo.meta.description.length Nein Meta-Description zu kurz oder zu lang (SEO).
seo.keyphrase Nein Vorhandensein der Fokus-Keyphrase in Titel, Überschriften und erstem Absatz (SEO).
link.text.ai Ja KI-generierte, kontextbewusste Vorschläge für generische Linktexte.
content.readability.ai Ja Lesbarkeitsgrad-Schätzung mit konkreten Verbesserungsvorschlägen (WCAG 3.1.5 AAA).
ai.alt.text.quality Ja Qualität vorhandener Alt-Texte (generisch, dateinamenbasiert, unsinnig) — bis zu 20 Bilder.
ai.heading.quality Ja Aussagekraft von Überschriften — bis zu 15 Überschriften.
ai.meta.description Ja Genauigkeit und Klickattraktivität der Meta-Description.

AuditService direkt verwenden

use Netthinks\NtAi\Audit\AuditService;

final class MyController
{
    public function __construct(private readonly AuditService $audit) {}

    public function action(): array
    {
        // Eine TYPO3-Seite prüfen.
        $report = $this->audit->auditPage(pageUid: 42, languageUid: 0);

        // Oder einen rohen HTML-String prüfen (z. B. Inhaltselement-Vorschau).
        $report = $this->audit->auditHtml(
            html: '<html><body>...</body></html>',
            options: ['pageLanguageCode' => 'de'],
        );

        return $report->toArray();
    }
}

Report bietet:

  • getFindings(): Finding[]
  • getFindingsBySeverity(Severity $s): Finding[]
  • countBySeverity(Severity $s): int
  • getScore(): int
  • hasErrors(): bool
  • toArray(): array

Regeln je Aufruf deaktivieren

$report = $this->audit->auditPage(
    pageUid: 42,
    options: ['disabledRules' => ['contrast.inline', 'content.readability.ai']],
);

AJAX-Endpunkte

Alle Endpunkte liegen unter TYPO3.settings.ajaxUrls.*. Sie erwarten JSON-Bodies und erfordern eine gültige Backend-Benutzersitzung.

Endpunkt Anfrage / Antwort
nt_ai_generate { prompt, provider?, language? }{ success, text, provider, model, tokens }
nt_ai_summarize { text, provider?, language? }{ success, text, ... }
nt_ai_alt_text { fileUid, language?, provider? }{ success, altText }
nt_ai_describe_image { fileUid, language?, provider? }{ success, description }
nt_ai_form_alt_text { metaUid, hmac }{ success, altText }
nt_ai_form_image_description { metaUid, hmac }{ success, description }
nt_ai_form_text { table, field, uid, hmac }{ success, text } (pages: abstract, seo_title, description, tx_ntai_focus_keyphrase)
nt_ai_seo_assist_generate { pageUid, languageUid, field, hmac }{ success, value }
nt_ai_seo_assist_save { pageUid, languageUid, field, hmac, value }{ success, value }
nt_ai_audit_run { pageUid, languageUid? }{ success, reportUid, report }
nt_ai_audit_export_csv GET ?pageUid=...&languageUid=... → CSV-Download
nt_ai_score_analyze POST { pageUid, languageUid }{ success, scores, suggestions }
nt_ai_score_latest GET ?pageUid=...&languageUid=...{ success, scores, history }

Fehler werden als { success: false, error: 'message' } mit HTTP 4xx / 5xx zurückgegeben.

Events

nt-ai löst PSR-14-Events aus und lauscht auf solche:

AfterTokenUsageRecordedEvent — wird nach jedem Speichern eines KI-Aufrufs in tx_ntai_token_usage ausgelöst. Für benutzerdefinierte Kostenalarme oder Analytics-Integrationen nutzbar.

ModifyUpdateArrayEvent — wird vor dem Speichern von Seiten-Score- oder Alt-Text-Ergebnissen ausgelöst. Ermöglicht Drittanbieter-Code, die Werte zu ändern oder abzulehnen.

ShouldExcludeAltTextEvent — wird ausgelöst, bevor der Auto-Generierungs-Listener beim Upload aktiv wird. true zurückgeben, um die Alt-Text-Generierung für eine bestimmte Datei zu überspringen.

nt-ai selbst lauscht auf AfterDatabaseOperationsEvent (statusgefiltert), um automatische Audits beim Seitenspeichern auszulösen. Der Listener-Bezeichner lautet nt-ai-auto-audit.

Datenbankschema

CREATE TABLE tx_ntai_audit_report (
    uid           int(11) NOT NULL auto_increment,
    pid           int(11) DEFAULT '0' NOT NULL,
    tstamp        int(11) unsigned DEFAULT '0' NOT NULL,
    crdate        int(11) unsigned DEFAULT '0' NOT NULL,
    deleted       tinyint(4) unsigned DEFAULT '0' NOT NULL,
    page_uid      int(11) DEFAULT '0' NOT NULL,
    language_uid  int(11) DEFAULT '0' NOT NULL,
    url           varchar(2048) DEFAULT '' NOT NULL,
    score         int(11) DEFAULT '0' NOT NULL,
    errors        int(11) DEFAULT '0' NOT NULL,
    warnings      int(11) DEFAULT '0' NOT NULL,
    notices       int(11) DEFAULT '0' NOT NULL,
    findings      mediumtext,
    triggered_by  varchar(32) DEFAULT 'manual' NOT NULL,
    PRIMARY KEY (uid),
    KEY parent (pid),
    KEY page (page_uid, language_uid),
    KEY recent (tstamp)
);

Die Spalte findings enthält JSON. Wann immer möglich Netthinks\NtAi\Audit\ReportRepository anstelle von rohem SQL verwenden.

Weitere von der Extension angelegte Tabellen:

  • tx_ntai_page_score — additiver KI-Inhaltsqualitäts-Score-Verlauf je Seite + Sprache.
  • tx_ntai_lighthouse_report — additiver Lighthouse-/PSI-Score-Verlauf je Seite + Sprache + Strategie.
  • tx_ntai_token_usage — KI-Token-Verbrauchsprotokoll je Aufruf (Anbieter, Modell, Input-/Output-Token, Kontext, Backend-Benutzer).

Test-Suite ausführen

composer install
vendor/bin/phpunit --testsuite=Unit
vendor/bin/phpstan analyse

Die Unit-Tests erfordern keine TYPO3-Instanz — sie isolieren alles hinter Stubs.