feat(B-TRIG): Trigger-Kern konsolidiert — generischer Dispatcher, UI-Event-Typ, 4 Trigger-Typen
Check Cross-Plugin Imports / check (push) Has been cancelled

B-TRIG-GEN: app/core/trigger_dispatcher.py (NEU) — generischer Event→Automation Dispatcher
- Wildcard EventBus Subscribe → matcht AutomationDefinition mit trigger_type=event
- Keine hardcodierte Event-Liste mehr — alle Outbox Events können Automations triggern
- Registriert in main.py lifespan + worker.py on_startup

B-TRIG-UI: UI-Event Trigger-Typ
- trigger_type Pattern um „ui" erweitert (event/schedule/manual/ui)
- UI-Events laufen ephemeral über EventBus → TriggerDispatcher → run_automation
- UI-Events NIEMALS in Outbox (is_ui_event() Guard)

B-TRIG-CRON: Cron/Heartbeat verifiziert — bestehender Pfad funktioniert
B-TRIG-MAN: Manual-Trigger verifiziert — bestehender Pfad funktioniert

B-TRIG-TEST: 10 Tests in test_trigger_core.py — alle grün
- Domain Event, UI Event (ephemeral), Cron, Manual, alle nutzen selben Execution-Kern
B-TRIG-DOC: Plugin-Dev-Guide Kapitel 10 mit Implementierungsdetails erweitert
This commit is contained in:
Agent Zero
2026-08-13 21:04:46 +02:00
parent ae228bb484
commit 78963f2ca9
6 changed files with 963 additions and 26 deletions
+180 -24
View File
@@ -1208,40 +1208,135 @@ Plugin-Migrationen werden bei Plugin-Aktivierung automatisch ausgeführt (`sync_
## 10. Trigger
Plugins können durch vier Trigger-Typen aktiviert werden: **Domain-Events**, **UI-Events**, **Cron-Jobs** und **manuelle Trigger**. Webhook-Trigger folgt in Phase G.
Plugins können durch vier Trigger-Typen aktiviert werden: **Domain-Events**, **UI-Events**, **Cron-Jobs** und **manuelle Trigger**. Alle vier Typen konvergieren im selben Execution-Kern (`run_automation`). Webhook-Trigger folgt in Phase G.
### 10.0 Trigger-Typen-Übersicht
| Trigger-Typ | `trigger_type` | Event-Quelle | Durability | Dispatch-Pfad |
|-------------|----------------|-------------|------------|---------------|
| Domain-Event | `event` | Outbox → Worker → EventBus | **durable** (at-least-once) | EventBus `*` → TriggerDispatcher → `run_automation` |
| UI-Event | `ui` | WebSocket → EventBus (ephemeral) | **ephemeral** (keine Persistenz) | EventBus `*` → TriggerDispatcher → `run_automation` |
| Cron/Schedule | `schedule` | ARQ Cron → `scheduler_tick` | **durable** (ARQ-Queue) | `scheduler_tick``enqueue_job("run_automation")` |
| Manual | `manual` | API-Route `/execute` | **durable** (HTTP-Request) | Route → `run_automation(trigger_type="manual")` |
**Wichtig:** UI-Events (`ui.*`) dürfen **niemals** in die Outbox geschrieben werden. Sie sind ephemeral und fließen direkt über den EventBus.
### 10.1 Domain-Event-Trigger (durable)
Domain-Events werden über die **Outbox** gepublished und sind reliable. Siehe Kapitel 8 für die Event-System-Rollen.
Domain-Events werden über die **Transaction Outbox** gepublished: ein Service schreibt das Event in die `event_outbox` Tabelle innerhalb derselben DB-Transaktion. Der ARQ-Worker pollt die Outbox alle 5 Sekunden und published die Events auf den **EventBus**.
Der **TriggerDispatcher** (`app/core/trigger_dispatcher.py`) abonniert den `*` Wildcard-Handler auf dem EventBus und evaluiert jedes eingehende Event. Bei einer Übereinstimmung mit einer `AutomationDefinition` (`trigger_type="event"`, `trigger_config.event_name` matcht) wird `run_automation` aufgerufen.
**Flow:**
```
Service → enqueue_outbox_event() → event_outbox Tabelle
↓ (commit)
ARQ Worker (process_outbox_job, alle 5s)
EventBus.publish_with_results(event_name, envelope)
TriggerDispatcher._on_event(payload)
DB-Query: AutomationDefinition WHERE trigger_type='event' AND trigger_config->>'event_name' = event_name
run_automation(trigger_type="event", trigger_data=payload)
```
**Beispiel — Domain-Event in Automation umwandeln:**
```python
# Plugin deklariert Events im Manifest
manifest = PluginManifest(
name="my_plugin",
events=["contact.created", "contact.updated"],
...
)
# AutomationDefinition erstellen
POST /api/v1/automation/
{
"name": "notify-on-contact-create",
"trigger_type": "event",
"trigger_config": {"event_name": "contact.created"},
"conditions": [],
"actions": [
{"type": "notification", "config": {"user_id": "...", "title": "Neuer Kontakt"}}
]
}
```
# Handler wird automatisch via on_activate registriert:
async def on_contact_created(self, payload: dict[str, Any]) -> None:
"""Reagiert auf contact.created Outbox-Event."""
contact_id = payload.get("contact_id")
# Business logic here
**Beispiel — Plugin veröffentlicht Domain-Event:**
```python
from app.core.outbox import enqueue_outbox_event
await enqueue_outbox_event(db, tenant_id, "contact.created", {
"contact_id": str(contact.id),
"tenant_id": str(tenant_id),
})
# Event wird beim Commit der Transaktion persistent.
# Der Worker published es an den EventBus.
# Der TriggerDispatcher findet passende Automations und führt sie aus.
```
### 10.2 UI-Event-Trigger (ephemeral)
Flüchtige UI-Events (z.B. `ui.contact_selected`) laufen über den **EventBus** — nicht über die Outbox. Siehe Kapitel 18.
UI-Events (`ui.*`) sind **ephemeral** — sie werden **nicht** in die Outbox geschrieben. Sie fließen direkt vom Frontend über WebSocket → EventBus → TriggerDispatcher.
```python
event_bus = get_event_bus()
event_bus.subscribe("ui.contact_selected", self._on_contact_selected)
Der TriggerDispatcher erkennt UI-Events am `ui.` Prefix und setzt `trigger_type="ui"` beim Dispatch. `AutomationDefinition`-Einträge mit `trigger_type="ui"` werden automatisch gematcht.
**Flow:**
```
Frontend (WebSocket) → ws_helpers → EventBus.publish("ui.contact_selected", payload)
TriggerDispatcher._on_event(payload)
→ is_ui_event("ui.contact_selected") = True
→ trigger_type = "ui"
DB-Query: AutomationDefinition WHERE trigger_type='ui' AND trigger_config->>'event_name' = 'ui.contact_selected'
run_automation(trigger_type="ui", trigger_data=payload)
```
### 10.3 Cron-Trigger
**⚠️ Kritische Regel:** UI-Events dürfen **niemals** über `enqueue_outbox_event()` gepublished werden. Verwende ausschließlich `event_bus.publish()`:
```python
from app.core.event_bus import get_event_bus
Cron-Jobs werden im Manifest deklariert und vom Worker ausgeführt:
# RICHTIG — ephemeral, nur EventBus
event_bus = get_event_bus()
await event_bus.publish("ui.contact_selected", {
"event_name": "ui.contact_selected",
"tenant_id": str(tenant_id),
"data": {"contact_id": str(contact_id)},
})
# FALSCH — würde UI-Event in Outbox persistieren
# await enqueue_outbox_event(db, tenant_id, "ui.contact_selected", {...}) # ❌
```
**Beispiel — UI-Event-Automation erstellen:**
```python
POST /api/v1/automation/
{
"name": "track-contact-selection",
"trigger_type": "ui",
"trigger_config": {"event_name": "ui.contact_selected"},
"conditions": [],
"actions": [
{"type": "api_call", "config": {"url": "https://analytics.example.com/track", "method": "POST"}}
]
}
```
### 10.3 Cron-Trigger (schedule)
Cron-Jobs werden über `AutomationCronJob`-Einträge in der Datenbank verwaltet. Der ARQ-Worker führt `scheduler_tick` alle 5 Minuten aus, liest fällige Cron-Jobs und enqueued `run_automation` mit `trigger_type="scheduled"`.
**Flow:**
```
ARQ Cron (alle 5min) → scheduler_tick(ctx)
DB-Query: AutomationCronJob WHERE is_active=True AND next_run_at <= now()
enqueue_job("run_automation", job.target_id, trigger_type="scheduled")
run_automation(trigger_type="scheduled", trigger_data={})
Update: last_run_at = now, next_run_at = calculate_next_run(cron_expression)
```
**Beispiel — Cron-Job im Plugin-Manifest deklarieren:**
```python
from app.plugins.manifest import CronJobContribution
@@ -1255,19 +1350,80 @@ cron_jobs=[
],
```
### 10.4 Manuelle Trigger
**Beispiel — Cron-Job zur Automation verknüpfen:**
```python
POST /api/v1/automation/cron-jobs/
{
"name": "daily-report-auto",
"cron_expression": "0 9 * * *",
"job_type": "automation",
"target_id": "<automation-definition-uuid>",
"is_active": true
}
```
Manuelle Trigger laufen über API-Routes oder Automation-Templates:
### 10.4 Manuelle Trigger (manual)
Manuelle Trigger werden über die API-Route `POST /api/v1/automation/{id}/execute` ausgelöst. Die Route ruft `run_automation` direkt mit `trigger_type="manual"` auf.
**Flow:**
```
HTTP POST /api/v1/automation/{id}/execute
AutomationRun (status="running") wird in DB erstellt
run_automation(trigger_type="manual", trigger_data={"triggered_by": user_id})
AutomationRun wird mit Ergebnis aktualisiert (status="completed"/"partial")
```
**Beispiel — Manuelle Automation auslösen:**
```bash
curl -X POST https://crm.media-on.de/api/v1/automation/{id}/execute \
-H "Cookie: session=..."
```
**Beispiel — Eigene Manual-Trigger-Route im Plugin:**
```python
@router.post("/api/v1/my-plugin/run-sync")
async def run_manual_sync(request: Request, db: AsyncSession = Depends(get_db)):
"""Manueller Trigger — Benutzer startet Sync von der UI."""
# Business logic
return {"status": "started"}
# Business logic oder: run_automation direkt aufrufen
from app.plugins.builtins.automation.execution_engine import run_automation
result = await run_automation(
ctx={},
automation_id=str(automation_id),
trigger_type="manual",
trigger_data={"triggered_by": str(user_id)},
)
return {"status": "started", "result": result}
```
**Wichtig:** Durable Domain Events (Outbox) und ephemere UI-Events (EventBus) strikt trennen. Siehe Kapitel 8.5 Entscheidungsregel.
### 10.5 TriggerDispatcher — Der generische Event→Automation Dispatcher
Der `TriggerDispatcher` (`app/core/trigger_dispatcher.py`) ist das zentrale Bindeglied zwischen EventBus und Automation-Engine. Er ersetzt frühere hardcodierte Event-Handler-Stubs (`on_contact_created` etc.) durch eine generische Wildcard-Subscription.
**Registrierung:**
- In `app/main.py` lifespan (API-Container)
- In `app/core/worker.py` `on_startup` (Worker-Container)
```python
from app.core.trigger_dispatcher import register_trigger_dispatcher
from app.core.event_bus import get_event_bus
event_bus = get_event_bus()
register_trigger_dispatcher(event_bus)
# Dispatcher abonniert '*' auf dem EventBus
```
**Matching-Logik:**
1. Jedes Event auf dem EventBus erreicht `_on_event(payload)`
2. `event_name` wird aus `payload["event_name"]` extrahiert
3. `ui.*` Prefix → `trigger_type="ui"`, sonst `trigger_type="event"`
4. DB-Query: aktive `AutomationDefinition` mit passendem `trigger_type` und `trigger_config.event_name`
5. Jede Match-Definition wird über `run_automation` ausgeführt
**Wichtig:** Der Dispatcher ist **generisch** — es gibt keine hardcodierte Event-Liste. Jedes registrierte Outbox-Event kann eine Automation triggern, sobald eine `AutomationDefinition` mit passendem `trigger_config.event_name` existiert.
---