Octibiz
Demo

Durchsucht Website und Dokumentation gemeinsam. Enter zeigt alle Treffer, Esc schließt.

KI-Zugang und Automatisierung

Für den KI-Zugang musst du nichts tun

Das ist die ganze Nachricht dieses Abschnitts, und sie ist ernst gemeint.

Jede API-Operation deines Plugins wird von selbst zu einem Werkzeug für den KI-Zugang. Der Werkzeug-Katalog entsteht aus derselben Beschreibung wie die API. Es gibt keine zweite Liste, die du pflegen müsstest, und keine Anmeldung.

Rechte, Marken-Schnitt und Prüfspur binden dabei in der API, nicht im Werkzeug. Ein KI-Aufruf kann nie mehr als der Mensch, dessen Identität er trägt. In der Prüfspur steht die Urheber-Art, ein Aufruf durch eine KI ist also unterscheidbar.

Auch die Modul-Sperre gilt automatisch

Der Katalog trägt je Operation den zuständigen Modul-Schalter. Ist dein Modul ausgeschaltet, verschwinden deine Operationen aus der Werkzeug-Liste, ohne dass du etwas tust.

Nach einer API-Änderung

Im Entwicklungsbetrieb muss der Katalog neu erzeugt werden:

bin/console cache:clear
make mcp-operations

Der Zwischenschritt ist nicht optional. Ohne ihn fehlen neue Operationen im Katalog, und zwar still: Kein Fehler, keine Warnung, sie sind einfach nicht da.

Beim Einspielen eines Pakets läuft der Export von selbst mit.

Was bewusst kein Werkzeug wird

Ein einfacher Controller, etwa der Empfänger eines Rückrufs von einem fremden System, taucht nicht im Katalog auf. Das ist Absicht: Solche Endpunkte sind Maschine-zu-Maschine-Strecken mit eigener Prüfung. Sie gehören nicht in die Hand einer KI.

Wenn du willst, dass etwas als Werkzeug erscheint, bau es als API-Ressource.

Automatisierung

Hier musst du etwas tun. Vier Verträge, alle über ihr Kennzeichen eingesammelt — implementieren genügt, es gibt keine Registrierung:

Was du beisteuern willstVertragKennzeichen
Eine Aktion, die eine Automatisierung ausführtAutomationActionInterfaceapp.automation_action
Einen Auslöser, auf den eine Automatisierung reagiertTriggerProviderInterfaceapp.automation.trigger_provider
Denselben Auslöser auswählbar machenTriggerCatalogProviderInterfaceapp.automation.trigger_catalog
Einen Vergleichsoperator für BedingungenConditionOperatorInterfaceapp.automation.condition_operator

Alle vier liegen unter App\Platform\Automation\.

Ein Auslöser, vollständig

Die beiden Auslöser-Verträge gehören in eine Klasse. So macht es auch das ausgelieferte Pages-Plugin in PagesTriggerProvider:

<?php

declare(strict_types=1);

namespace Acme\Shipping\Catalog;

use Acme\Shipping\Event\ShipmentDelivered;
use Acme\Shipping\Event\ShipmentDispatched;
use App\Platform\Automation\Engine\TriggerCatalogProviderInterface;
use App\Platform\Automation\Engine\TriggerProviderInterface;

final class AcmeShippingTriggerProvider implements TriggerProviderInterface, TriggerCatalogProviderInterface
{
    /** @var array<class-string, string> */
    private const KEYS = [
        ShipmentDispatched::class => 'acme_shipping.shipment.dispatched',
        ShipmentDelivered::class => 'acme_shipping.shipment.delivered',
    ];

    /** Ordnet ein konkretes Ereignis den Auslöser-Schlüsseln zu. Speist die Laufzeit. */
    public function triggerKeysFor(string $eventClass): array
    {
        $key = self::KEYS[$eventClass] ?? null;

        return null === $key ? [] : [$key];
    }

    /** Zählt dieselben Schlüssel für den Baukasten auf. Speist `GET /api/v1/automation-catalog`. */
    public function authoringTriggerKeys(): array
    {
        return array_values(self::KEYS);
    }
}

Das ist die ganze Datei.

Wer nur TriggerProviderInterface implementiert, baut einen Auslöser, den niemand auswählen kann. Er funktioniert, er feuert, er ist getestet — aber er steht nicht im Katalog, den der Baukasten und der KI-Zugang abfragen. Das ist der häufigste Stolperstein an dieser Stelle, und genau deshalb stehen beide Verträge in derselben Klasse.

Eine Aktion

AutomationActionInterface verlangt zwei Methoden:

public function getType(): string;
public function execute(ActionContext $context): ActionResult;

Vollständig, mit den Regeln, die dabei gelten:

<?php

declare(strict_types=1);

namespace Acme\Shipping\Automation;

use Acme\Shipping\Entity\Shipment;
use App\Platform\Automation\Action\ActionContext;
use App\Platform\Automation\Action\ActionResult;
use App\Platform\Automation\Action\AutomationActionInterface;
use Doctrine\ORM\EntityManagerInterface;

final class MarkShipmentDeliveredAction implements AutomationActionInterface
{
    /**
     * Der Maschinen-Schlüssel als EIGENE Konstante der Klasse.
     *
     * Ein Wächter in der Oberfläche liest den Nenner aller Aktions-Typen aus dem Quelltext
     * und kennt genau zwei Formen: ein Literal oder `self::…`. Verweist du auf die Konstante
     * einer ANDEREN Klasse, fällt das dort als Lesefehler auf — der Typ hinge still ausserhalb
     * des Nenners und seine fehlende Beschriftung wäre niemandem aufgefallen.
     */
    public const TYPE = 'acme_shipping_mark_delivered';

    public function __construct(private readonly EntityManagerInterface $em)
    {
    }

    public function getType(): string
    {
        return self::TYPE;
    }

    public function execute(ActionContext $context): ActionResult
    {
        $ulid = $context->rawString('shipmentUlid');
        if ('' === $ulid) {
            return ActionResult::failure('acmeShipping.noShipment');
        }

        $shipment = $this->em->getRepository(Shipment::class)->findOneBy(['ulid' => $ulid]);
        if (!$shipment instanceof Shipment) {
            return ActionResult::failure('acmeShipping.shipmentNotFound:'.$ulid);
        }

        // Trockenlauf: prüfen, aber nichts ändern.
        if ($context->isDryRun) {
            return ActionResult::success(\sprintf('Dry-Run: %s nicht geändert.', $ulid));
        }

        $shipment->setStatus('delivered');
        $this->em->flush();

        return ActionResult::success(\sprintf('Sendung %s auf zugestellt gesetzt.', $ulid));
    }
}

Drei Dinge, die daran nicht offensichtlich sind:

  • $context->isDryRun musst du selbst beachten. Der Trockenlauf ist eine Bitte, keine Sperre.

Wer ihn ignoriert, ändert Daten in einer Vorschau.

  • Die Sätze in ActionResult sind Diagnose-Protokoll, kein Nutzertext. Sie landen im Protokoll

des Laufs und bleiben in der Systemsprache. Die Oberfläche zeigt sie nicht an.

  • Kein eigener Prüfspur-Eintrag. Die Änderung wird beim Speichern ohnehin protokolliert, mit

der Urheber-Art automation. Ein zusätzlicher Eintrag wäre eine Dublette je Feldänderung.

Deine Aktion beim Speichern prüfen

Der Kern kennt die Pflichtfelder deiner Aktion nicht. Ohne eigene Prüfung fällt eine unvollständige Konfiguration erst beim Lauf auf, und dann trifft es den Empfänger, nicht den Autor.

AutomationActionConfigValidatorInterface schließt die Lücke — implementiert von derselben Aktions-Klasse:

public function validateConfig(array $config, ?int $automationBrandId, bool $automationActive): void

Der dritte Parameter ist der Grund, warum die Methode drei bekommt: Ein ausgeschalteter Entwurf darf unvollständig sein, beim Einschalten nicht.

public function validateConfig(array $config, ?int $automationBrandId, bool $automationActive): void
{
    if (!$automationActive) {
        return; // Entwurf: alles erlaubt
    }

    if ('' === (string) ($config['shipmentUlid'] ?? '')) {
        throw new \InvalidArgumentException('shipmentUlid ist beim Einschalten Pflicht.');
    }
}

Ein Vergleichsoperator

ConditionOperatorInterface ist der kleinste der vier Verträge:

public function operator(): string;
public function matches(mixed $actual, mixed $expected): bool;

Beschriftungen

Auslöser und Aktionen werden über Schlüssel beschriftet, nicht über Text. Die Konvention ist verbindlich, weil der Katalog sie berechnet:

automation.trigger.<kleinCamel(Auslöser-Schlüssel)>
automation.action.<kleinCamel(Aktions-Typ)>

Aus acme_shipping.shipment.delivered wird also automation.trigger.acmeShippingShipmentDelivered.

Sie liegen in der Sprachdatei deiner Oberfläche, nicht im Backend. Details in Übersetzungen.

Felder für das Aktions-Formular

Braucht deine Aktion eigene Eingabefelder im Baukasten, kommen sie über das Manifest der Oberfläche, nicht aus dem Backend. Siehe Oberfläche.

Der Zusammenhang zu Ereignissen

Ein Auslöser sitzt auf einem Ereignis. Die Ereignisse nach einer Änderung speisen den Fan-out für Automatisierungen und Rückrufe; die Ereignisse vor einer Änderung tun das bewusst nicht, sie dienen dem Widerspruch.

Wenn du also willst, dass etwas eine Automatisierung auslöst, brauchst du ein Ereignis nach der Änderung. Der vollständige Katalog steht in der Ereignis-Referenz.

Ein Ereignis kann sich davon ausnehmen, wenn es sonst eine Rückkopplung erzeugte: Eine Änderung, die eine Automatisierung auslöst, die dieselbe Änderung erneut vornimmt.

Was schiefgeht, und woran es liegt

SymptomUrsache
Neue Operation fehlt im Werkzeug-KatalogZwischenspeicher vor dem Export nicht geleert
Auslöser läuft, ist aber nicht auswählbarTriggerCatalogProviderInterface fehlt
Aktion scheitert erst zur Laufzeit beim EmpfängerAutomationActionConfigValidatorInterface fehlt
Beschriftung zeigt den rohen SchlüsselSprachdatei fehlt, oder der Schlüssel folgt nicht automation.trigger.<kleinCamel>
Ein Trockenlauf ändert trotzdem Daten$context->isDryRun in execute() nicht beachtet
Eine Automatisierung ruft sich selbst aufEreignis nach der Änderung ohne Ausnahme vom Fan-out

Weiter

plugin.automation · Gilt ab Version 0.6.22