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:
-
KI-Service-Schicht —
AiServiceist der Einstiegspunkt. Er wählt einen Anbieter anhand der Konfiguration aus, erstellt Prompts überPromptBuilderund ruft die Methodecomplete()oderdescribeImage()des Anbieters auf. Anbieter implementierenAiProviderInterfaceund sind tagged Services. DasProviderRegistrylöst sie anhand des Bezeichners auf. -
Audit-Schicht —
AuditServiceist der Einstiegspunkt. Er lädt das gerenderte HTML überPageFetcher, parst es mitDomLoaderund führt jede getaggteRuleInterface-Implementierung dagegen aus. Die Ergebnisse werden zu einemReportaggregiert. -
Frontend-Schicht — buildfreie Assets (
a11y-widget.js/.css,tts.js,nt-dark-theme.css), die über das Sitepackage-Set eingebunden werden. Das Widget schreibt reinedata-nt-*-Attribute auf denhtml-Tag (kein DOM-Rewriting); die Vorlesefunktion nutzt Web Speech (lokal) oder ruft den PSR-15-EndpunktTtsMiddleware(/nt-ai/tts) auf, der überTtsServicebeim Anbieter generiert, cacht (ntai_tts) und pro IP rate-limitiert. Das Inline-Config-Skript (data-nt-*am<html>-Tag) wird überpage.jsInlineausgeliefert, 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:
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:
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): intgetScore(): inthasErrors(): booltoArray(): 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¶
Die Unit-Tests erfordern keine TYPO3-Instanz — sie isolieren alles hinter Stubs.