platform-core/modules/features/graphicalEditor/entryPoints.py
ValueOn AG e800bc0b71
Some checks failed
Deploy Plattform-Core / test (push) Failing after 45s
Deploy Plattform-Core / deploy (push) Has been skipped
Sync: full codebase from GitHub gateway main
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 23:54:29 +02:00

147 lines
4.8 KiB
Python

# Copyright (c) 2025 Patrick Motsch
"""
Workflow entry points (Starts) — configuration outside the flow editor.
Kinds align with run envelope trigger.type where applicable.
"""
import uuid
from typing import Any, Dict, List, Optional
# On-demand (gear: Manueller Trigger, Formular)
KINDS_ON_DEMAND = frozenset({"manual", "form", "api"})
# Always-on (gear: Zeitplan, Immer aktiv, plus legacy listener kinds)
KINDS_ALWAYS_ON = frozenset({"schedule", "always_on", "email", "webhook", "event"})
ALL_KINDS = KINDS_ON_DEMAND | KINDS_ALWAYS_ON
def category_for_kind(kind: str) -> str:
if kind in KINDS_ALWAYS_ON:
return "always_on"
return "on_demand"
def default_manual_entry_point() -> Dict[str, Any]:
"""Single default manual start when a workflow has no invocations yet."""
return {
"id": str(uuid.uuid4()),
"kind": "manual",
"category": "on_demand",
"enabled": True,
"title": "Jetzt ausführen",
"description": {},
"config": {},
}
def _normalize_title(title: Any) -> str:
"""Extract a plain string from a title value for storage (not display)."""
if isinstance(title, dict):
picked = title.get("xx") or next((v for v in title.values() if v), None)
return str(picked).strip() if picked else "Start"
if isinstance(title, str) and title.strip():
return title.strip()
return "Start"
def normalize_invocation_entry(raw: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and normalize a single entry point dict."""
kind = (raw.get("kind") or "manual").strip()
if kind not in ALL_KINDS:
kind = "manual"
cat = raw.get("category")
if cat not in ("on_demand", "always_on"):
cat = category_for_kind(kind)
eid = raw.get("id") or str(uuid.uuid4())
enabled = raw.get("enabled", True)
if not isinstance(enabled, bool):
enabled = bool(enabled)
config = raw.get("config") if isinstance(raw.get("config"), dict) else {}
desc = raw.get("description") if isinstance(raw.get("description"), dict) else {}
return {
"id": str(eid),
"kind": kind,
"category": cat,
"enabled": enabled,
"title": _normalize_title(raw.get("title")),
"description": desc,
"config": config,
}
def normalize_invocations_list(items: Optional[List[Any]]) -> List[Dict[str, Any]]:
if not items:
return [default_manual_entry_point()]
out: List[Dict[str, Any]] = []
for raw in items:
if isinstance(raw, dict):
out.append(normalize_invocation_entry(raw))
if not out:
return [default_manual_entry_point()]
return out
_NODE_TYPE_TO_KIND = {
"trigger.manual": "manual",
"trigger.form": "form",
"trigger.schedule": "schedule",
}
def invocations_synced_with_graph(
graph: Optional[Dict[str, Any]],
stored_invocations: Optional[List[Any]],
) -> List[Dict[str, Any]]:
"""Derive primary invocation (index 0) from the first start node in ``graph``.
If the graph has no start node, only non-primary stored invocations are kept
(no injected default). Document order in ``nodes`` defines which start wins.
"""
from modules.workflows.automation2.graphUtils import getTriggerNodes
g = graph if isinstance(graph, dict) else {}
nodes = g.get("nodes") or []
stored = list(stored_invocations or [])
rest: List[Dict[str, Any]] = []
for raw in stored[1:]:
if isinstance(raw, dict):
rest.append(normalize_invocation_entry(raw))
triggers = getTriggerNodes(nodes)
if not triggers:
return rest
node = triggers[0]
nt = str(node.get("type", "")).strip()
kind = _NODE_TYPE_TO_KIND.get(nt, "manual")
nid = node.get("id")
if not nid:
nid = str(uuid.uuid4())
raw_title = node.get("title") or node.get("label") or "Start"
old_primary = stored[0] if stored and isinstance(stored[0], dict) else {}
config: Dict[str, Any] = {}
if isinstance(old_primary.get("config"), dict) and old_primary.get("kind") == kind:
config = dict(old_primary["config"])
desc = old_primary.get("description") if isinstance(old_primary.get("description"), dict) else {}
primary_raw: Dict[str, Any] = {
"id": str(nid),
"kind": kind,
"enabled": True,
"title": raw_title,
"description": desc,
"config": config,
}
primary = normalize_invocation_entry(primary_raw)
return [primary] + rest
# POST .../execute with entryPointId set to a schedule entry — no separate in-process scheduler here yet.
def find_invocation(workflow: Dict[str, Any], entry_point_id: str) -> Optional[Dict[str, Any]]:
for inv in workflow.get("invocations") or []:
if isinstance(inv, dict) and inv.get("id") == entry_point_id:
return inv
return None