694 lines
31 KiB
Markdown
694 lines
31 KiB
Markdown
<!-- status: done -->
|
||
<!-- lastReviewed: 2026-04-10 -->
|
||
|
||
# Typed Node Handover System — Spezifikation & Execution Plan
|
||
|
||
**Ansatz: Greenfield.** Keine Rückwärtskompatibilität, keine Migration, kein Deprecation.
|
||
|
||
---
|
||
|
||
## 1 Problemstellung
|
||
|
||
Der Graphical Editor hat keine formale Typisierung der Daten zwischen Nodes. Jede Node produziert ein freiformiges Dict; die Folge-Node extrahiert heuristisch. Kern-Symptome:
|
||
|
||
- `actionNodeExecutor.py` (~860 Zeilen, >50 % handover-Heuristik pro Node-Paar)
|
||
- `outputPreviewRegistry.ts` hartcodiert Beispiel-Schemas, die von Laufzeit-Outputs abweichen
|
||
- Keine `data.transform`, `data.filter`, `data.aggregate`, `flow.merge` implementiert
|
||
- Frontend baut pro Node-Typ eine eigene Config-Component (12+ Dateien)
|
||
- Keine System-Variablen (Timestamp, Datum, User, etc.)
|
||
|
||
Die Method/Action-Schicht hat bereits `WorkflowActionParameter` mit `FrontendType`, `_validateType`, `_validateParameters`. Dieses System wird für den Graphen übernommen und erweitert.
|
||
|
||
---
|
||
|
||
## 2 Architektur
|
||
|
||
### 2.1 Port-Schema (Kern-Datenmodell)
|
||
|
||
Jede Node deklariert **typisierte Ports** für Ein- und Ausgänge sowie **Parameter** für die Konfiguration:
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────┐
|
||
│ Node Definition (Beispiel: email.draftEmail) │
|
||
│ │
|
||
│ inputPorts: │
|
||
│ [0]: { accepts: [EmailDraft, AiResult] } │
|
||
│ │
|
||
│ outputPorts: │
|
||
│ [0]: { schema: ActionResult } │
|
||
│ │
|
||
│ parameters: │
|
||
│ connectionId: { type: str, frontendType: USER_CONNECTION } │
|
||
│ subject: { type: str, frontendType: TEXT } │
|
||
│ body: { type: str, frontendType: TEXTAREA } │
|
||
│ to: { type: List[str], frontendType: TEXT } │
|
||
└──────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.2 Port-Schema Pydantic-Modelle
|
||
|
||
**Neue Datei:** `platform-core/modules/features/graphicalEditor/portTypes.py`
|
||
|
||
```python
|
||
class PortField(BaseModel):
|
||
name: str
|
||
type: str # str, int, bool, List[str], List[Document], Dict[str,Any]
|
||
description: Dict[str, str] # {en, de, fr}
|
||
required: bool = True
|
||
|
||
class PortSchema(BaseModel):
|
||
name: str # z.B. "EmailDraft", "AiResult", "Transit"
|
||
fields: List[PortField]
|
||
|
||
class InputPortDef(BaseModel):
|
||
accepts: List[str] # Liste akzeptierter Schema-Namen
|
||
|
||
class OutputPortDef(BaseModel):
|
||
schema: str # Schema-Name aus dem Katalog
|
||
dynamic: bool = False # True = Schema wird aus Parametern abgeleitet
|
||
deriveFrom: Optional[str] = None # Parameter-Name für dynamische Ableitung
|
||
```
|
||
|
||
### 2.3 Port-Typen-Katalog
|
||
|
||
| Port-Typ | Felder | Produziert von | Konsumiert von |
|
||
|----------|--------|----------------|----------------|
|
||
| `DocumentList` | `documents: List[{name, data, mimeType, metadata}]` | sharepoint.readFile, sharepoint.downloadFile, ai.*, file.create, input.upload | ai.*, file.create, email.draftEmail, sharepoint.uploadFile |
|
||
| `FileList` | `files: List[{url, name, path, size}]` | sharepoint.listFiles, sharepoint.findFile | sharepoint.readFile, sharepoint.downloadFile, flow.loop |
|
||
| `EmailDraft` | `subject: str, body: str, to: List[str], cc?: List[str], attachments?: List[Document]` | ai.prompt (outputFormat=emailDraft) | email.draftEmail |
|
||
| `EmailList` | `emails: List[{subject, from, to, body, date, attachments}]` | email.checkEmail, email.searchEmail | ai.prompt, flow.loop |
|
||
| `TaskList` | `tasks: List[{id, name, status, url}]` | clickup.searchTasks, clickup.listTasks | flow.loop, clickup.updateTask |
|
||
| `TaskResult` | `success: bool, taskId: str, task: {id, name, status}` | clickup.createTask, clickup.updateTask | flow.ifElse |
|
||
| `FormPayload` | `payload: Dict[str, Any]` (dynamisch) | trigger.form, input.form | Alle (via Feld-Referenz) |
|
||
| `AiResult` | `prompt: str, response: str, responseData?: Dict, context: str, documents: List[Document]` | ai.* | email.draftEmail, file.create, sharepoint.uploadFile |
|
||
| `BoolResult` | `result: bool, reason?: str` | input.approval, input.confirmation | flow.ifElse |
|
||
| `TextResult` | `text: str` | input.comment | ai.*, file.create |
|
||
| `LoopItem` | `currentItem: Any, currentIndex: int, items: List[Any], count: int` | flow.loop (pro Iteration) | Loop-Body Nodes |
|
||
| `AggregateResult` | `items: List[Any], count: int` | data.aggregate | Alle |
|
||
| `MergeResult` | `inputs: Dict[int, Any], first: Any, merged: Dict` | flow.merge | Alle |
|
||
|
||
Jeder Port-Typ bekommt implizit zwei Meta-Felder: **`_success: bool`** und **`_error: str?`**. Wenn ein Node fehlschlägt, liefert der Normalizer `{ _success: false, _error: "...", ...restliche Felder leer/default }`.
|
||
|
||
### 2.4 Transit-Typ
|
||
|
||
Flow-Control-Nodes transformieren keine Daten — sie routen. Transit bedeutet: **Output-Schema = Input-Schema des Upstream-Produzenten**.
|
||
|
||
Transit-Nodes produzieren ein Envelope:
|
||
|
||
```python
|
||
{
|
||
"_transit": True,
|
||
"_meta": { "branch": 0, "conditionResult": True }, # Routing-Metadaten
|
||
"data": <upstream-output-unverändert>,
|
||
}
|
||
```
|
||
|
||
| Node | Output-Typ | _meta-Felder |
|
||
|------|-----------|-------------|
|
||
| `flow.ifElse` | Transit | `branch: int`, `conditionResult: bool` |
|
||
| `flow.switch` | Transit | `match: int`, `value: str` |
|
||
| `data.filter` | Transit | `originalCount: int`, `filteredCount: int` |
|
||
|
||
**Transit-Auflösung** (rekursiv): DataPicker und Engine folgen `_transit`-Kette bis zum echten Produzenten. Multi-Output-Ports (ifElse hat 2): Transit-Auflösung berücksichtigt `sourceOutput`-Index, beide Ports zeigen dasselbe Upstream-Schema.
|
||
|
||
`flow.loop` ist **kein** Transit — er transformiert eine Liste in `LoopItem` pro Iteration.
|
||
|
||
### 2.5 Dynamische Schemas
|
||
|
||
Nodes mit konfigurationsabhängigem Output (Formulare, data.transform):
|
||
|
||
```python
|
||
{
|
||
"id": "input.form",
|
||
"outputPorts": {
|
||
0: {"schema": "FormPayload", "dynamic": True, "deriveFrom": "fields"},
|
||
},
|
||
}
|
||
```
|
||
|
||
Schema-Ableitung: die Engine und das Frontend leiten aus dem Parameter `fields` die konkreten Output-Felder ab. Jedes Formular-Feld erscheint als wählbares Attribut im DataPicker.
|
||
|
||
---
|
||
|
||
## 3 Handover-Mechanismen
|
||
|
||
### 3.1 Prioritätsreihenfolge
|
||
|
||
Wenn ein Parameter-Wert aus mehreren Quellen kommen kann:
|
||
|
||
**DataRef/SystemVar/Static > Wire-Handover > Default**
|
||
|
||
1. Wire-Handover: Extraktor füllt Input-Felder aus dem Upstream-Output.
|
||
2. DataRef/System/Static: überschreiben Wire-Werte (= explizite User-Wahl gewinnt).
|
||
3. Defaults aus Parameter-Definition, falls noch leer.
|
||
|
||
### 3.2 Mechanismus 1: DataRef (Hauptmechanismus, ~80 %)
|
||
|
||
User wählt im DataPicker, welches Feld eines Vorgängers in welchen Parameter fließt:
|
||
|
||
```
|
||
email.draftEmail.parameters:
|
||
subject = DataRef { nodeId: "ai_1", path: ["responseData", "subject"] }
|
||
body = DataRef { nodeId: "ai_1", path: ["responseData", "body"] }
|
||
to = DataRef { nodeId: "form_1", path: ["payload", "recipient"] }
|
||
```
|
||
|
||
### 3.3 Mechanismus 2: Wire-Handover (Extraktoren)
|
||
|
||
Pro **Input-Port-Typ** ein Extraktor (~12 Funktionen, nicht N×M):
|
||
|
||
```python
|
||
INPUT_EXTRACTORS: Dict[str, Callable] = {
|
||
"EmailDraft": _extractEmailDraft,
|
||
"DocumentList": _extractDocuments,
|
||
"TextResult": _extractText,
|
||
...
|
||
}
|
||
```
|
||
|
||
Extraktor kennt das Upstream-Schema (aus `outputPorts`) und greift Felder direkt ab — kein Raten mit Fallbacks.
|
||
|
||
### 3.4 Mechanismus 3: data.transform
|
||
|
||
Expliziter Konverter-Node für Fälle, wo DataRef und Extraktor nicht reichen.
|
||
|
||
### 3.5 Mechanismus 4: System-Variablen
|
||
|
||
Dritter DynamicValue-Typ neben `ref` und `value`:
|
||
|
||
```python
|
||
SYSTEM_VARIABLES = {
|
||
"system.timestamp": { "type": "int", "description": "Unix timestamp (ms)" },
|
||
"system.date": { "type": "str", "description": "ISO date (YYYY-MM-DD)" },
|
||
"system.datetime": { "type": "str", "description": "ISO datetime" },
|
||
"system.time": { "type": "str", "description": "HH:MM:SS" },
|
||
"system.userId": { "type": "str", "description": "Aktueller User-ID" },
|
||
"system.userName": { "type": "str", "description": "Aktueller User-Name" },
|
||
"system.userEmail": { "type": "str", "description": "Aktueller User-Email" },
|
||
"system.workflowId": { "type": "str", "description": "Workflow-ID" },
|
||
"system.runId": { "type": "str", "description": "Run-ID" },
|
||
"system.instanceId": { "type": "str", "description": "Feature-Instanz-ID" },
|
||
"system.mandateId": { "type": "str", "description": "Mandant-ID" },
|
||
"system.loopIndex": { "type": "int", "description": "Aktueller Loop-Index (nur in Loop)" },
|
||
"system.loopCount": { "type": "int", "description": "Anzahl Loop-Items (nur in Loop)" },
|
||
"system.uuid": { "type": "str", "description": "Neue zufällige UUID" },
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4 Generisches Frontend-Rendering
|
||
|
||
### 4.1 Prinzip: Jede UI-Darstellung ist ein FrontendType
|
||
|
||
Es gibt **keine** Node-spezifischen Config-Components. `NODE_CONFIG_REGISTRY` entfällt komplett.
|
||
|
||
### 4.2 FrontendType-Katalog
|
||
|
||
```python
|
||
class FrontendType(str, Enum):
|
||
# Standard Types
|
||
TEXT = "text"
|
||
TEXTAREA = "textarea"
|
||
NUMBER = "number"
|
||
CHECKBOX = "checkbox"
|
||
DATE = "date"
|
||
DATETIME = "datetime"
|
||
EMAIL = "email"
|
||
SELECT = "select"
|
||
MULTISELECT = "multiselect"
|
||
JSON = "json"
|
||
FILE = "file"
|
||
HIDDEN = "hidden"
|
||
|
||
# Picker Types (API-backed)
|
||
USER_CONNECTION = "userConnection"
|
||
SHAREPOINT_FOLDER = "sharepointFolder"
|
||
SHAREPOINT_FILE = "sharepointFile"
|
||
CLICKUP_LIST = "clickupList"
|
||
CLICKUP_TASK = "clickupTask"
|
||
|
||
# Complex Structure Types
|
||
CASE_LIST = "caseList"
|
||
FIELD_BUILDER = "fieldBuilder"
|
||
KEY_VALUE_ROWS = "keyValueRows"
|
||
CRON = "cron"
|
||
CONDITION = "condition"
|
||
MAPPING_TABLE = "mappingTable"
|
||
FILTER_EXPRESSION = "filterExpression"
|
||
```
|
||
|
||
### 4.3 Parameter-Abhängigkeiten
|
||
|
||
Picker-Parameter die von anderen Parametern desselben Nodes abhängen:
|
||
|
||
```python
|
||
{
|
||
"name": "path",
|
||
"type": "str",
|
||
"frontendType": "sharepointFolder",
|
||
"frontendOptions": {"dependsOn": "connectionId"},
|
||
}
|
||
```
|
||
|
||
Der generische Renderer übergibt den Wert des abhängigen Parameters. Der Picker rendert sich disabled, solange die Abhängigkeit nicht erfüllt ist.
|
||
|
||
### 4.4 Generischer Renderer
|
||
|
||
```typescript
|
||
const FRONTEND_TYPE_RENDERERS: Record<string, ComponentType<FieldRendererProps>> = {
|
||
text: TextInput,
|
||
textarea: TextareaInput,
|
||
number: NumberInput,
|
||
checkbox: CheckboxInput,
|
||
date: DatePicker,
|
||
select: SelectInput,
|
||
multiselect: MultiSelectInput,
|
||
json: JsonEditor,
|
||
userConnection: ConnectionPicker,
|
||
sharepointFolder: FolderPicker,
|
||
clickupList: ClickUpListPicker,
|
||
caseList: CaseListEditor,
|
||
fieldBuilder: FieldBuilderEditor,
|
||
keyValueRows: KeyValueRowsEditor,
|
||
cron: CronBuilder,
|
||
condition: ConditionBuilder,
|
||
mappingTable: MappingTableEditor,
|
||
filterExpression: FilterExpressionEditor,
|
||
};
|
||
|
||
function GenericNodeConfig({ node, nodeType }) {
|
||
return nodeType.parameters.map(param => {
|
||
const Renderer = FRONTEND_TYPE_RENDERERS[param.frontendType] ?? TextInput;
|
||
return <Renderer key={param.name} param={param} value={...} />;
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5 Neue Nodes
|
||
|
||
### 5.1 data.aggregate
|
||
|
||
```python
|
||
{
|
||
"id": "data.aggregate",
|
||
"category": "data",
|
||
"label": {"en": "Aggregate", "de": "Sammeln", "fr": "Agréger"},
|
||
"parameters": [
|
||
{"name": "mode", "type": "str", "frontendType": "select",
|
||
"options": ["collect", "concat", "sum", "count"], "default": "collect"},
|
||
],
|
||
"inputPorts": {0: {"accepts": ["Transit"]}},
|
||
"outputPorts": {0: {"schema": "AggregateResult"}},
|
||
"executor": "data",
|
||
}
|
||
```
|
||
|
||
**Engine-Semantik:** Wenn data.aggregate im Loop-Body steht, wird ihr Output **nicht** pro Iteration überschrieben, sondern in `_aggregateAccumulators[nodeId]` gesammelt. Nach Loop-Ende: `nodeOutputs[nodeId] = { items: akkumulator, count: len }`.
|
||
|
||
### 5.2 data.transform
|
||
|
||
```python
|
||
{
|
||
"id": "data.transform",
|
||
"category": "data",
|
||
"label": {"en": "Transform", "de": "Umwandeln", "fr": "Transformer"},
|
||
"parameters": [
|
||
{"name": "mappings", "type": "json", "frontendType": "mappingTable"},
|
||
],
|
||
"inputPorts": {0: {"accepts": ["Transit"]}},
|
||
"outputPorts": {0: {"schema": "Dict", "dynamic": True, "deriveFrom": "mappings"}},
|
||
"executor": "data",
|
||
}
|
||
```
|
||
|
||
### 5.3 data.filter
|
||
|
||
```python
|
||
{
|
||
"id": "data.filter",
|
||
"category": "data",
|
||
"label": {"en": "Filter", "de": "Filtern", "fr": "Filtrer"},
|
||
"parameters": [
|
||
{"name": "condition", "type": "str", "frontendType": "filterExpression"},
|
||
],
|
||
"inputPorts": {0: {"accepts": ["AggregateResult", "FileList", "TaskList", "EmailList", "DocumentList"]}},
|
||
"outputPorts": {0: {"schema": "Transit"}},
|
||
"executor": "data",
|
||
}
|
||
```
|
||
|
||
**Filter-Expression-Sprache:** Vergleiche (`==`, `!=`, `<`, `>`, `<=`, `>=`), Logik (`and`, `or`, `not`), String (`contains`, `startsWith`), Null (`isEmpty`, `isNotEmpty`). Der `FILTER_EXPRESSION`-Renderer baut einen visuellen Condition-Builder (Dropdown Feld → Operator → Wert).
|
||
|
||
### 5.4 flow.merge
|
||
|
||
```python
|
||
{
|
||
"id": "flow.merge",
|
||
"category": "flow",
|
||
"label": {"en": "Merge", "de": "Zusammenführen", "fr": "Fusionner"},
|
||
"parameters": [
|
||
{"name": "mode", "type": "str", "frontendType": "select",
|
||
"options": ["first", "all", "append"], "default": "first"},
|
||
],
|
||
"inputs": 2,
|
||
"outputs": 1,
|
||
"inputPorts": {0: {"accepts": ["Transit"]}, 1: {"accepts": ["Transit"]}},
|
||
"outputPorts": {0: {"schema": "MergeResult"}},
|
||
"executor": "flow",
|
||
"meta": {"icon": "mdi-call-merge", "color": "#FF9800"},
|
||
}
|
||
```
|
||
|
||
**Engine-Semantik:** flow.merge wird erst ausgeführt, wenn alle verbundenen Vorgänger verarbeitet sind. Für inaktive Branches (nach ifElse): der übersprungene Vorgänger ist nicht in `nodeOutputs` → wird als „nicht verfügbar" markiert. mode=first nimmt den ersten verfügbaren, mode=all merged nur die verfügbaren.
|
||
|
||
---
|
||
|
||
## 6 Entscheidungen
|
||
|
||
| # | Frage | Entscheidung |
|
||
|---|-------|-------------|
|
||
| 1 | Typ-Mismatch bei Kante? | **Soft** — Warnung + visuelle Markierung, kein Hard-Block. |
|
||
| 2 | Transit statt Any für Flow-Nodes? | **Ja.** Transit-Envelope mit `_meta` und `data`. DataPicker resolved rekursiv. |
|
||
| 3 | Dynamische Schemas? | **Ja.** `deriveFrom`-Parameter → Schema-Ableitung. |
|
||
| 4 | Migration? | **Nein.** Greenfield. |
|
||
| 5 | data.aggregate separat? | **Ja.** Separater Node. |
|
||
| 6 | flow.merge? | **Ja, sofort.** Komplettes Set. |
|
||
| 7 | `_paramMap` Zukunft? | **Entfällt.** Node-Parameter = Action-Parameter-Namen. UI-Labels separat in `description`. |
|
||
| 8 | Parameter vs Wire Priorität? | **DataRef > Wire > Default.** |
|
||
| 9 | Fehler-Output? | Jeder Port-Typ hat `_success`/`_error` Meta-Felder. |
|
||
| 10 | AI Structured Output? | `AiResult.responseData: Dict` + Parameter `outputFormat` (text/json). |
|
||
| 11 | Pause/Resume? | Resume-Handler validiert User-Eingabe gegen Output-Schema der pausierten Node. |
|
||
|
||
---
|
||
|
||
## 7 Execution Plan — Backend
|
||
|
||
### 7.1 NEU: `platform-core/modules/features/graphicalEditor/portTypes.py`
|
||
|
||
Erstellen. Inhalt:
|
||
|
||
- `PortField`, `PortSchema`, `InputPortDef`, `OutputPortDef` (Pydantic-Modelle, Abschnitt 2.2)
|
||
- `PORT_TYPE_CATALOG`: Dict aller PortSchemas (Abschnitt 2.3)
|
||
- `SYSTEM_VARIABLES`: Dict (Abschnitt 3.5)
|
||
- Output-Normalizer: `_normalizeToSchema(raw: Dict, schemaName: str) -> Dict` + pro Port-Typ eine Funktion
|
||
- Input-Extraktoren: `INPUT_EXTRACTORS: Dict[str, Callable]` (Abschnitt 3.3)
|
||
- Transit-Envelope-Helpers: `_wrapTransit(data, meta)`, `_unwrapTransit(output)`, `_resolveTransitChain(nodeId, nodeOutputs, connectionMap)`
|
||
- Schema-Ableitungsfunktionen: `_deriveFormPayloadSchema(node)`, `_deriveTransformSchema(node)` (Abschnitt 2.5)
|
||
|
||
### 7.2 ÄNDERN: `platform-core/modules/shared/frontendTypes.py`
|
||
|
||
**Aktuell:** `FrontendType` Enum mit 14 Werten (Zeilen 16–63).
|
||
|
||
**Änderung:** Erweitern um Complex Structure Types:
|
||
|
||
```
|
||
Hinzufügen:
|
||
SHAREPOINT_FILE = "sharepointFile"
|
||
CLICKUP_LIST = "clickupList"
|
||
CLICKUP_TASK = "clickupTask"
|
||
CASE_LIST = "caseList"
|
||
FIELD_BUILDER = "fieldBuilder"
|
||
KEY_VALUE_ROWS = "keyValueRows"
|
||
CRON = "cron"
|
||
CONDITION = "condition"
|
||
MAPPING_TABLE = "mappingTable"
|
||
FILTER_EXPRESSION = "filterExpression"
|
||
```
|
||
|
||
`CUSTOM_TYPE_OPTIONS_API` und `CUSTOM_TYPE_DESCRIPTIONS` entsprechend erweitern.
|
||
|
||
### 7.3 NEU SCHREIBEN: `platform-core/modules/features/graphicalEditor/nodeDefinitions/*.py`
|
||
|
||
Alle 10 Dateien (`triggers.py`, `flow.py`, `input.py`, `ai.py`, `email.py`, `sharepoint.py`, `clickup.py`, `file.py`, `trustee.py`, `__init__.py`) **komplett neu**.
|
||
|
||
Jede Node-Definition bekommt:
|
||
|
||
- `inputPorts`: Dict mit `InputPortDef` pro Port-Index
|
||
- `outputPorts`: Dict mit `OutputPortDef` pro Port-Index
|
||
- `parameters`: Liste von Dicts im `WorkflowActionParameter`-Format (mit `frontendType`, `frontendOptions` inkl. `dependsOn`)
|
||
- **Kein** `_paramMap` mehr — Parameter-Namen = Action-Parameter-Namen
|
||
- `_method` und `_action` bleiben (für ActionExecutor-Mapping)
|
||
|
||
NEU: `data.py` mit `data.aggregate`, `data.transform`, `data.filter`.
|
||
|
||
`__init__.py`: `STATIC_NODE_TYPES` um `DATA_NODES` erweitern; `flow.py` um `flow.merge` erweitern.
|
||
|
||
### 7.4 ÄNDERN: `platform-core/modules/features/graphicalEditor/nodeRegistry.py`
|
||
|
||
**Funktion `getNodeTypesForApi`** (Zeile 54–75):
|
||
|
||
- `_localizeNode` anpassen: `inputPorts` und `outputPorts` in API-Response einschließen (nicht als `_`-Prefix, also nicht wegstrippen)
|
||
- Response erweitern um `systemVariables` aus `portTypes.SYSTEM_VARIABLES`
|
||
- Response erweitern um `portTypeCatalog` aus `portTypes.PORT_TYPE_CATALOG`
|
||
|
||
**Funktion `getNodeTypeToMethodAction`** (Zeile 78–89): bleibt, `_method`/`_action` weiterhin verwendet.
|
||
|
||
### 7.5 ÄNDERN: `platform-core/modules/workflows/automation2/graphUtils.py`
|
||
|
||
**Funktion `resolveParameterReferences`** (Zeile 185–243):
|
||
|
||
- Neuen Case hinzufügen für `type: "system"`:
|
||
|
||
```python
|
||
if isinstance(value, dict) and value.get("type") == "system":
|
||
return _resolveSystemVariable(value["variable"], context)
|
||
```
|
||
|
||
- `_resolveSystemVariable(variable, context)` als neue Funktion (Abschnitt 3.5).
|
||
|
||
**Funktion `validateGraph`** (Zeile 86–120): erweitern um Port-Kompatibilitäts-Check (soft — Warnings sammeln, nicht hart ablehnen).
|
||
|
||
### 7.6 NEU SCHREIBEN: `platform-core/modules/workflows/automation2/executors/actionNodeExecutor.py`
|
||
|
||
**Komplett neu.** Die ~860 Zeilen mit heuristischen Merge-Funktionen (`_extractEmailContentFromUpstream`, `_getContextFromUpstream`, `_gatherAttachmentDocumentsFromUpstream`, `_formatEmailOutputAsContext`, `_unpackIncomingEmail`, `_getIncomingEmailFromUpstream`, `_buildActionParams`, `_paramMap`-Logik, etc.) werden **ersetzt** durch:
|
||
|
||
1. Node-Definition laden (`_getNodeDefinition`)
|
||
2. Parameter per `resolveParameterReferences` auflösen (DataRef, SystemVar, Static)
|
||
3. Wire-Handover: wenn `inputSources[nodeId][0]` existiert, Extraktor aus `INPUT_EXTRACTORS` aufrufen
|
||
4. Priorität anwenden (DataRef > Wire > Default)
|
||
5. `ActionExecutor.executeAction(method, action, params)` aufrufen
|
||
6. Ergebnis durch `_normalizeToSchema(result, outputSchema)` normalisieren
|
||
7. Fehler-Envelope: bei Exception `{ _success: false, _error: str, ...defaults }` zurückgeben
|
||
|
||
**Keine** node-type-spezifische Logik mehr im Executor.
|
||
|
||
### 7.7 NEU: `platform-core/modules/workflows/automation2/executors/dataExecutor.py`
|
||
|
||
Neuer Executor für `data.aggregate`, `data.transform`, `data.filter`:
|
||
|
||
- `data.aggregate`: im Loop-Kontext akkumuliert Ergebnisse (Modus: collect, concat, sum, count)
|
||
- `data.transform`: wendet `mappings` an (jedes Mapping löst eine DataRef/Static-Referenz auf)
|
||
- `data.filter`: evaluiert `condition` pro Item der Input-Liste, gibt gefilterte Liste zurück
|
||
|
||
### 7.8 ÄNDERN: `platform-core/modules/workflows/automation2/executors/flowExecutor.py`
|
||
|
||
**Funktion `_ifElse`** (Zeile 55–65): Output ändern zu Transit-Envelope:
|
||
|
||
```python
|
||
# Heute:
|
||
return {"branch": 0 if ok else 1, "conditionResult": ok, "input": inp}
|
||
|
||
# Neu:
|
||
return {"_transit": True, "_meta": {"branch": 0 if ok else 1, "conditionResult": ok}, "data": inp}
|
||
```
|
||
|
||
**Funktion `_switch`** (Zeile 199–207): analog Transit-Envelope.
|
||
|
||
**Funktion `_loop`** (Zeile 261–272): bleibt wie bisher (kein Transit, produziert `LoopItem`).
|
||
|
||
**NEU: `_merge`** Funktion: sammelt Outputs aller verbundenen Input-Ports aus `nodeOutputs`, wendet Modus an (first/all/append), gibt `MergeResult` zurück.
|
||
|
||
### 7.9 ÄNDERN: `platform-core/modules/workflows/automation2/executors/__init__.py`
|
||
|
||
`DataExecutor` importieren und exportieren.
|
||
|
||
### 7.10 ÄNDERN: `platform-core/modules/workflows/automation2/executionEngine.py`
|
||
|
||
**Funktion `_getExecutor`** (Zeile 71–85): Case für `data.*` hinzufügen → `DataExecutor`.
|
||
|
||
**Funktion `_is_node_on_active_path`** (Zeile 39–68): Transit-Envelope berücksichtigen — `branch`/`match` jetzt in `out["_meta"]` statt direkt in `out`.
|
||
|
||
**Loop-Body-Verarbeitung** (Zeile 400–460): Spezialbehandlung für `data.aggregate` Nodes: statt `nodeOutputs[bnid] = result` ein `_aggregateAccumulators[bnid].append(result)`. Nach Loop-Ende: `nodeOutputs[aggregateNodeId] = { items: akkumulator, count: len }`.
|
||
|
||
**Nach jedem Execute** (Zeile 390+): `_normalizeToSchema(result, outputSchema)` aufrufen bevor in `nodeOutputs` gespeichert.
|
||
|
||
**flow.merge Wartelogik:** merge-Nodes werden von `topoSort` automatisch nach ihren Vorgängern eingeordnet (BFS). Vor Ausführung: prüfe ob alle verbundenen Vorgänger in `nodeOutputs` sind oder übersprungen wurden (inaktiver Branch → `not in nodeOutputs` und Node wurde als skipped geloggt).
|
||
|
||
**Pause/Resume:** Resume-Handler (`initialNodeOutputs`) — Validierung der User-Eingabe gegen Output-Schema der pausierten Node (in `executeGraph` nach Empfang von `initialNodeOutputs`).
|
||
|
||
### 7.11 ÄNDERN: `platform-core/modules/features/graphicalEditor/routeFeatureGraphicalEditor.py`
|
||
|
||
**Funktion `get_node_types`** (Zeile 146–168): Response-Struktur erweitern:
|
||
|
||
```python
|
||
# Heute:
|
||
return {"nodeTypes": localized, "categories": categories}
|
||
|
||
# Neu:
|
||
return {
|
||
"nodeTypes": localized, # mit inputPorts, outputPorts
|
||
"categories": categories,
|
||
"portTypeCatalog": PORT_TYPE_CATALOG, # alle Schemas
|
||
"systemVariables": SYSTEM_VARIABLES,
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8 Execution Plan — Frontend
|
||
|
||
### 8.1 ÄNDERN: `ui-nyla/src/api/workflowApi.ts`
|
||
|
||
**Interface `NodeTypeParameter`** (Zeile 14–20): erweitern um `frontendType`, `frontendOptions`, `options`, `validation`.
|
||
|
||
**Interface `NodeType`** (Zeile 22–39): erweitern um `inputPorts`, `outputPorts`.
|
||
|
||
**Interface `NodeTypesResponse`** (Zeile 46–49): erweitern um `portTypeCatalog`, `systemVariables`.
|
||
|
||
### 8.2 LÖSCHEN + NEU: `ui-nyla/src/components/FlowEditor/nodes/configs/`
|
||
|
||
**Löschen:** `index.ts`, `AiNodeConfig.tsx`, `EmailNodeConfig.tsx`, `SharePointNodeConfig.tsx`, `ClickUpNodeConfig.tsx`, `ApprovalNodeConfig.tsx`, `UploadNodeConfig.tsx`, `CommentNodeConfig.tsx`, `ReviewNodeConfig.tsx`, `SelectionNodeConfig.tsx`, `ConfirmationNodeConfig.tsx`, `FileCreateNodeConfig.tsx`, `TrusteeNodeConfig.tsx`, `types.ts`.
|
||
|
||
**Neu:** `frontendTypeRenderers/` Ordner mit einem Renderer pro FrontendType (TextInput, TextareaInput, NumberInput, CheckboxInput, DatePicker, SelectInput, MultiSelectInput, JsonEditor, ConnectionPicker, FolderPicker, ClickUpListPicker, CaseListEditor, FieldBuilderEditor, KeyValueRowsEditor, CronBuilder, ConditionBuilder, MappingTableEditor, FilterExpressionEditor).
|
||
|
||
**Neu:** `frontendTypeRenderers/index.ts` mit `FRONTEND_TYPE_RENDERERS` Registry.
|
||
|
||
Bestehende Logik aus den gelöschten Config-Components (z.B. ConnectionPicker aus `EmailNodeConfig`, CaseEditor aus `SwitchNodeConfig`, FieldBuilder aus `FormNodeConfig`) wird in die entsprechenden FrontendType-Renderer extrahiert.
|
||
|
||
### 8.3 ÄNDERN: `ui-nyla/src/components/FlowEditor/editor/NodeConfigPanel.tsx`
|
||
|
||
**Komplett neu.** Statt `NODE_CONFIG_REGISTRY`-Lookup:
|
||
|
||
```typescript
|
||
import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers';
|
||
|
||
// Iteriere nodeType.parameters, rendere pro Parameter den passenden Renderer
|
||
nodeType.parameters.map(param => {
|
||
const Renderer = FRONTEND_TYPE_RENDERERS[param.frontendType] ?? TextInput;
|
||
return <Renderer param={param} value={...} onChange={...} />;
|
||
});
|
||
```
|
||
|
||
### 8.4 LÖSCHEN: `ui-nyla/src/components/FlowEditor/nodes/shared/outputPreviewRegistry.ts`
|
||
|
||
Komplett entfernen. Ersetzen durch generische Funktion die aus dem `portTypeCatalog` + `outputPorts` Schema den Preview-Baum baut:
|
||
|
||
```typescript
|
||
function buildPreviewFromSchema(
|
||
node: CanvasNode,
|
||
nodeTypes: NodeType[],
|
||
portTypeCatalog: Record<string, PortSchema>
|
||
): Record<string, unknown> {
|
||
const nt = nodeTypes.find(t => t.id === node.type);
|
||
const outputSchema = nt?.outputPorts?.[0]?.schema;
|
||
if (outputSchema === 'Transit') return {}; // resolved dynamisch
|
||
const schema = portTypeCatalog[outputSchema];
|
||
// Generiere Beispielwerte pro Feld aus schema.fields
|
||
}
|
||
```
|
||
|
||
### 8.5 ÄNDERN: `ui-nyla/src/components/FlowEditor/nodes/shared/dataRef.ts`
|
||
|
||
**`DynamicValue` Union** (Zeile 20): erweitern um `SystemVarRef`:
|
||
|
||
```typescript
|
||
interface SystemVarRef {
|
||
type: 'system';
|
||
variable: string;
|
||
}
|
||
|
||
type DynamicValue = DataRef | DataValue | SystemVarRef;
|
||
```
|
||
|
||
`isRef`, `isValue` etc. ergänzen um `isSystemVar` Type Guard.
|
||
|
||
### 8.6 ÄNDERN: `ui-nyla/src/components/FlowEditor/nodes/shared/DataPicker.tsx`
|
||
|
||
- `buildPickablePaths` (Zeile 20–40): **Schema-basiert** statt aus Preview-Daten. Erhält `portTypeCatalog` und baut Baum aus Schema-Feldern.
|
||
- Transit-Auflösung: wenn Output-Schema = Transit, folge connectionMap rückwärts bis zum echten Produzenten.
|
||
- Neue Sektion **„System"** mit allen Variablen aus `systemVariables`, gruppiert (Datum/Zeit, User, Workflow).
|
||
- Bei Pick eines System-Werts: `onPick` liefert `SystemVarRef` statt `DataRef`.
|
||
|
||
### 8.7 ÄNDERN: `ui-nyla/src/components/FlowEditor/context/Automation2DataFlowContext.tsx`
|
||
|
||
- `Automation2DataFlowContextValue` (Zeile 10–19): erweitern um `portTypeCatalog`, `systemVariables`.
|
||
- `nodeOutputsPreview` Berechnung: aus Schema statt aus `outputPreviewRegistry`.
|
||
|
||
### 8.8 ÄNDERN: `ui-nyla/src/components/FlowEditor/nodes/shared/graphUtils.ts`
|
||
|
||
`fromApiGraph` / `toApiGraph`: `inputPorts` und `outputPorts` mitserialisieren in `CanvasNode`.
|
||
|
||
### 8.9 ÄNDERN: `ui-nyla/src/components/FlowEditor/editor/FlowCanvas` (Verbindungs-Validierung)
|
||
|
||
Beim Verbinden zweier Ports: prüfe `sourceNode.outputPorts[outputIdx].schema` gegen `targetNode.inputPorts[inputIdx].accepts`. Bei Mismatch: Kante gelb/orange markieren (Soft-Warnung).
|
||
|
||
---
|
||
|
||
## 9 Dateien-Übersicht
|
||
|
||
### Neue Dateien
|
||
|
||
| Datei | Inhalt |
|
||
|-------|--------|
|
||
| `platform-core/.../graphicalEditor/portTypes.py` | PortSchema, Katalog, Normalizer, Extraktoren, System-Variablen |
|
||
| `platform-core/.../automation2/executors/dataExecutor.py` | Executor für data.aggregate, data.transform, data.filter |
|
||
| `platform-core/.../nodeDefinitions/data.py` | Node-Definitionen: data.aggregate, data.transform, data.filter |
|
||
| `ui-nyla/.../nodes/frontendTypeRenderers/index.ts` | FRONTEND_TYPE_RENDERERS Registry |
|
||
| `ui-nyla/.../nodes/frontendTypeRenderers/*.tsx` | Ein Renderer pro FrontendType |
|
||
|
||
### Komplett neu geschriebene Dateien
|
||
|
||
| Datei | Grund |
|
||
|-------|-------|
|
||
| `platform-core/.../automation2/executors/actionNodeExecutor.py` | Heuristische Merge-Logik → Normalizer + Extraktor |
|
||
| `platform-core/.../nodeDefinitions/triggers.py` | + inputPorts, outputPorts, frontendType |
|
||
| `platform-core/.../nodeDefinitions/flow.py` | + flow.merge, Transit-Ports |
|
||
| `platform-core/.../nodeDefinitions/input.py` | + dynamische FormPayload-Schemas |
|
||
| `platform-core/.../nodeDefinitions/ai.py` | + AiResult-Ports, outputFormat-Param |
|
||
| `platform-core/.../nodeDefinitions/email.py` | + EmailDraft/EmailList-Ports |
|
||
| `platform-core/.../nodeDefinitions/sharepoint.py` | + FileList/DocumentList-Ports |
|
||
| `platform-core/.../nodeDefinitions/clickup.py` | + TaskList/TaskResult-Ports |
|
||
| `platform-core/.../nodeDefinitions/file.py` | + DocumentList-Ports |
|
||
| `platform-core/.../nodeDefinitions/trustee.py` | + Ports |
|
||
| `platform-core/.../nodeDefinitions/__init__.py` | + DATA_NODES Import |
|
||
| `ui-nyla/.../editor/NodeConfigPanel.tsx` | Generischer Renderer statt NODE_CONFIG_REGISTRY |
|
||
|
||
### Geänderte Dateien
|
||
|
||
| Datei | Änderung |
|
||
|-------|----------|
|
||
| `platform-core/.../shared/frontendTypes.py` | FrontendType Enum erweitern (+10 Werte) |
|
||
| `platform-core/.../automation2/graphUtils.py` | `resolveParameterReferences` + SystemVar, `validateGraph` + Port-Check |
|
||
| `platform-core/.../automation2/executors/flowExecutor.py` | ifElse/switch → Transit-Envelope, + _merge |
|
||
| `platform-core/.../automation2/executors/__init__.py` | + DataExecutor Export |
|
||
| `platform-core/.../automation2/executionEngine.py` | + DataExecutor, Transit-_meta, aggregate-Akkumulator, merge-Wartelogik |
|
||
| `platform-core/.../graphicalEditor/nodeRegistry.py` | API-Response + portTypeCatalog, systemVariables |
|
||
| `platform-core/.../graphicalEditor/routeFeatureGraphicalEditor.py` | Response-Struktur erweitern |
|
||
| `ui-nyla/.../api/workflowApi.ts` | Interfaces erweitern (inputPorts, outputPorts, frontendType) |
|
||
| `ui-nyla/.../nodes/shared/dataRef.ts` | + SystemVarRef |
|
||
| `ui-nyla/.../nodes/shared/DataPicker.tsx` | Schema-basiert, Transit-Auflösung, System-Sektion |
|
||
| `ui-nyla/.../nodes/shared/graphUtils.ts` | inputPorts/outputPorts serialisieren |
|
||
| `ui-nyla/.../context/Automation2DataFlowContext.tsx` | + portTypeCatalog, systemVariables |
|
||
| `ui-nyla/.../editor/FlowCanvas` | Verbindungs-Validierung |
|
||
|
||
### Gelöschte Dateien
|
||
|
||
| Datei | Grund |
|
||
|-------|-------|
|
||
| `ui-nyla/.../nodes/configs/index.ts` | Ersetzt durch FRONTEND_TYPE_RENDERERS |
|
||
| `ui-nyla/.../nodes/configs/AiNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/EmailNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/SharePointNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/ClickUpNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/ApprovalNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/UploadNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/CommentNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/ReviewNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/SelectionNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/ConfirmationNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/FileCreateNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/configs/TrusteeNodeConfig.tsx` | Generischer Renderer |
|
||
| `ui-nyla/.../nodes/shared/outputPreviewRegistry.ts` | Schema-basierte Preview |
|