"""Auktoritativ, maskinläsbar beskrivning av alla stegtyper i ett flöde. Detta är den gemensamma källan för: * webui:t (fältrendering - i dagsläget speglat i index.html:s FIELD_DEFS) * MCP-servern (rpa/mcp_server.py - verktyget describe_steps / resursen rpa://schema) * NL-till-flöde (rpa/assist.py - systemprompt + validering av AI:ns svar) Innehållet är härlett ur rpa/flow_engine.py (_execute_step) och webui/templates/index.html (FIELD_DEFS / I18N). Ändras en stegtyp i motorn ska den speglas här - tests/test_schema.py har en drift-vakt som fångar glapp. Fältens "kind": text - fri text (kan innehålla {variabel} om interpolate=True) number - heltal/decimal select - ett av "options" """ from __future__ import annotations import json import re from typing import Any # -------------------------------------------------------------------------- # Byggstenar # -------------------------------------------------------------------------- # Fälten som pekar ut ett element (samma som SELECTOR_FIELDS i index.html). SELECTOR_FIELDS: list[dict] = [ { "key": "by", "kind": "select", "options": ["text", "css", "role"], "default": "text", "label_sv": "Hitta via", "label_en": "Find by", "help": "text = synlig text (regex, skiftlägesokänsligt), css = CSS-selector, role = ARIA-roll", }, { "key": "value", "kind": "text", "interpolate": True, "label_sv": "Text / CSS-selector / rollnamn", "label_en": "Text / CSS selector / role name", "help": "Tolkas enligt 'by'. {variabel} fungerar för by=text och by=role, inte för by=css.", }, { "key": "role", "kind": "text", "label_sv": "Roll (om by=role, ex. link, button)", "label_en": "Role (if by=role, e.g. link, button)", }, { "key": "scope", "kind": "select", "options": ["page", "item"], "default": "page", "label_sv": "Sökområde", "label_en": "Search scope", "help": "item begränsar sökningen till aktuell loop-post (bara inuti en loop med DOM-poster).", }, { "key": "nth", "kind": "number", "default": 0, "label_sv": "Index (0 = första matchning)", "label_en": "Index (0 = first match)", }, ] _TIMEOUT_FIELD = { "key": "timeout", "kind": "number", "default": 15, "label_sv": "Timeout (sekunder)", "label_en": "Timeout (seconds)", } # Villkorstyper för "if"-grenar (CONDITION_KINDS / CONDITION_FIELD_DEFS i index.html). _VAR_TEXT_FIELDS = [ {"key": "var", "kind": "text", "required": True, "label_sv": "Variabelnamn", "label_en": "Variable name"}, # inte required: att jämföra mot tomt ("är variabeln tom?") är ett giltigt villkor {"key": "text", "kind": "text", "label_sv": "Jämför med (tomt = jämför mot tom sträng)", "label_en": "Compare with (empty = compare to empty string)"}, ] CONDITION_KINDS: list[dict] = [ { "kind": "element_exists", "label_sv": "Element finns", "label_en": "Element exists", "fields": SELECTOR_FIELDS, }, { "kind": "element_not_exists", "label_sv": "Element finns inte", "label_en": "Element does not exist", "fields": SELECTOR_FIELDS, }, { "kind": "var_equals", "label_sv": "Variabel är lika med", "label_en": "Variable equals", "fields": _VAR_TEXT_FIELDS, }, { "kind": "var_not_equals", "label_sv": "Variabel är INTE lika med", "label_en": "Variable does NOT equal", "fields": _VAR_TEXT_FIELDS, }, { "kind": "var_contains", "label_sv": "Variabel innehåller", "label_en": "Variable contains", "fields": _VAR_TEXT_FIELDS, }, ] CONDITION_KIND_NAMES = [c["kind"] for c in CONDITION_KINDS] # Ett flöde kan deklarera indata-variabler (flow["inputs"]) som matas in vid # körning. Detta beskriver ett fälts form för editorn i webui:t. INPUT_FIELDS: list[dict] = [ {"key": "name", "kind": "text", "required": True, "label_sv": "Variabelnamn", "label_en": "Variable name", "help": "Används som {name} i stegen."}, {"key": "label", "kind": "text", "label_sv": "Etikett (visas i kördialogen)", "label_en": "Label (shown in the run dialog)"}, {"key": "default", "kind": "text", "label_sv": "Standardvärde", "label_en": "Default value"}, {"key": "options", "kind": "text", "label_sv": "Alternativ (komma-separerade → rullista)", "label_en": "Options (comma-separated → dropdown)"}, {"key": "description", "kind": "text", "label_sv": "Beskrivning", "label_en": "Description"}, ] # Körningsinställningar (spegel av DEFAULT_SETTINGS i rpa/browser.py + notify). SETTINGS_SCHEMA: list[dict] = [ {"key": "headless", "kind": "bool", "default": False, "label_sv": "Headless (inget synligt fönster)", "label_en": "Headless (no visible window)", "help": "Kräver en redan giltig inloggningssession - BankID/2FA går inte att godkänna headless."}, {"key": "skip_browser", "kind": "bool", "default": False, "label_sv": "Starta ingen webbläsare (rena desktop-flöden)", "label_en": "Don't start a browser (desktop-only flows)", "help": "Tvingar bort webbläsaren. Upptäcks annars automatiskt när flödet saknar webbläsarsteg. Ett webbläsarsteg med detta på ger fel."}, {"key": "viewport_width", "kind": "number", "default": 1280, "label_sv": "Fönsterbredd", "label_en": "Viewport width"}, {"key": "viewport_height", "kind": "number", "default": 900, "label_sv": "Fönsterhöjd", "label_en": "Viewport height"}, {"key": "locale", "kind": "text", "default": "sv-SE", "label_sv": "Locale", "label_en": "Locale"}, {"key": "slow_mo", "kind": "number", "default": 0, "label_sv": "Slow motion (ms mellan åtgärder)", "label_en": "Slow motion (ms between actions)"}, {"key": "notify_on_complete", "kind": "bool", "default": False, "label_sv": "Skrivbordsnotis när körningen är klar", "label_en": "Desktop notification when the run finishes"}, ] CLOSE_MODES = ["always", "on_success", "never"] # Nycklar som får finnas på vilket steg som helst (flow_engine._run_steps). COMMON_STEP_FIELDS: list[dict] = [ {"key": "label", "kind": "text", "label_sv": "Etikett", "label_en": "Label", "help": "Namnger steget som mål för ett 'jump_to_label'-steg."}, {"key": "enabled", "kind": "bool", "default": True, "label_sv": "Aktiverat", "label_en": "Enabled", "help": "false = steget (och ev. inre steg) hoppas över vid körning."}, {"key": "on_error", "kind": "select", "options": ["stop", "continue"], "default": "stop", "label_sv": "Vid fel", "label_en": "On error", "help": "continue loggar felet och kör vidare. download/save_attachment/capture_pdf fortsätter alltid."}, ] def _sel(*extra: dict) -> list[dict]: return [*SELECTOR_FIELDS, *extra] # Selector-fält för Windows UI Automation (pywinauto) - annan modell än webben. DESKTOP_SELECTOR_FIELDS: list[dict] = [ { "key": "by", "kind": "select", "options": ["name", "auto_id", "control_type", "class_name"], "default": "name", "label_sv": "Hitta via", "label_en": "Find by", "help": "name = kontrollens synliga text (regex), auto_id = AutomationId, control_type = t.ex. Button/Edit, class_name = fönsterklass", }, {"key": "value", "kind": "text", "interpolate": True, "label_sv": "Söksträng", "label_en": "Search string"}, {"key": "control_type", "kind": "text", "label_sv": "Kontrolltyp (valfri extra filtrering, t.ex. Button)", "label_en": "Control type (optional extra filter, e.g. Button)"}, {"key": "window", "kind": "text", "label_sv": "Fönstertitel (regex, tomt = aktuellt fönster)", "label_en": "Window title (regex, blank = current window)"}, {"key": "nth", "kind": "number", "default": 0, "label_sv": "Index (0 = första)", "label_en": "Index (0 = first)"}, ] def _dsel(*extra: dict) -> list[dict]: return [*DESKTOP_SELECTOR_FIELDS, *extra] # -------------------------------------------------------------------------- # Stegtyperna # -------------------------------------------------------------------------- STEP_TYPES: list[dict] = [ # ---- Navigering ---- { "type": "goto", "category": "navigation", "label_sv": "Gå till URL", "label_en": "Go to URL", "help": "Navigerar webbläsaren till en URL (wait_until=domcontentloaded).", "fields": [{"key": "url", "kind": "text", "required": True, "label_sv": "URL", "label_en": "URL"}], }, { "type": "wait", "category": "navigation", "label_sv": "Vänta (sekunder)", "label_en": "Wait (seconds)", "help": "Fast paus.", "fields": [{"key": "seconds", "kind": "number", "default": 1, "label_sv": "Sekunder", "label_en": "Seconds"}], }, { "type": "wait_for_load", "category": "navigation", "label_sv": "Vänta på att sidan laddas", "label_en": "Wait for page to load", "help": "Väntar på domcontentloaded.", "fields": [], }, { "type": "go_back", "category": "navigation", "label_sv": "Gå tillbaka (webbläsarhistorik)", "label_en": "Go back (browser history)", "help": "Webbläsarens bakåtknapp - t.ex. för att fortsätta en loop efter att ha klickat in på en post.", "fields": [], }, { "type": "wait_for_login", "category": "navigation", "label_sv": "Vänta på inloggning", "label_en": "Wait for login", "help": "Pausar tills sidan visar inloggad status (för manuell BankID/2FA).", "fields": [ {"key": "timeout", "kind": "number", "default": 180, "label_sv": "Timeout (sekunder)", "label_en": "Timeout (seconds)"}, {"key": "indicators", "kind": "text", "label_sv": "Text som visar inloggad (komma-separerad, tomt = standard)", "label_en": "Logged-in text (comma-separated, empty = default)"}, ], }, { "type": "wait_for_selector", "category": "navigation", "label_sv": "Vänta på element", "label_en": "Wait for element", "help": "Pausar tills ett element via text/CSS/roll finns.", "selector": True, "fields": _sel(dict(_TIMEOUT_FIELD)), }, { "type": "read_url", "category": "navigation", "label_sv": "Läs aktuell URL", "label_en": "Read current URL", "help": "Sparar den aktuella sidans URL i en variabel. Med 'pattern' plockas bara en regex-träff ut (namngiven grupp (?P…) om den finns, annars hela träffen) – t.ex. ett ärende-id ur adressen.", "fields": [ {"key": "var", "kind": "text", "required": True, "label_sv": "Spara som variabel", "label_en": "Save as variable"}, {"key": "pattern", "kind": "text", "label_sv": "Regex-utplock (valfritt)", "label_en": "Regex extraction (optional)"}, ], "produces_vars": ["var"], }, # ---- Interaktion ---- { "type": "click", "category": "interaction", "label_sv": "Klicka", "label_en": "Click", "help": "Hittar ett element och klickar, väntar sedan på domcontentloaded.", "selector": True, "fields": _sel(), }, { "type": "type", "category": "interaction", "label_sv": "Skriv i textfält", "label_en": "Type in field", "help": "Fyller i ett fält (fill). 'text' stödjer {variabel}.", "selector": True, "fields": _sel({"key": "text", "kind": "text", "interpolate": True, "required": True, "label_sv": "Text att skriva", "label_en": "Text to type"}), }, { "type": "press_key", "category": "interaction", "label_sv": "Tryck tangent", "label_en": "Press key", "help": "Trycker en tangent. Utan value/by: mot hela sidan.", "fields": [ {"key": "key", "kind": "text", "default": "Enter", "required": True, "label_sv": "Tangent (ex. Enter, Tab, Escape)", "label_en": "Key (e.g. Enter, Tab, Escape)"}, {"key": "value", "kind": "text", "interpolate": True, "label_sv": "Text / CSS-selector / rollnamn (tomt = hela sidan)", "label_en": "Text / CSS selector / role name (empty = whole page)"}, {"key": "by", "kind": "select", "options": ["text", "css", "role"], "default": "text", "label_sv": "Hitta via (om value ifylld)", "label_en": "Find by (if value is set)"}, {"key": "role", "kind": "text", "label_sv": "Roll (om by=role)", "label_en": "Role (if by=role)"}, {"key": "scope", "kind": "select", "options": ["page", "item"], "default": "page", "label_sv": "Sökområde", "label_en": "Search scope"}, {"key": "nth", "kind": "number", "default": 0, "label_sv": "Index (0 = första)", "label_en": "Index (0 = first)"}, ], }, { "type": "select_option", "category": "interaction", "label_sv": "Välj i dropdown", "label_en": "Select in dropdown", "help": "Väljer ett alternativ (synlig text) i en