diff --git a/src/data/services.ts b/src/data/services.ts index 23e7c47..5da831b 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -1759,6 +1759,15 @@ export const services: Service[] = [ name: 'Počkat', description: 'Pozastaví běh na zadanou dobu.', fields: ['Doba čekání'], + inputs: [ + { + id: 'seconds', + label: 'Sekund', + kind: 'text', + required: true, + hint: 'Nejvýš 60. Delší čekání patří do plánovače, ne do běhu.', + }, + ], }, ], }, @@ -1779,7 +1788,16 @@ export const services: Service[] = [ id: 'write', name: 'Zapsat zprávu', description: 'Přidá záznam do historie běhu, užitečné při ladění.', - fields: ['Úroveň', 'Zpráva'], + fields: ['Zpráva'], + inputs: [ + { + id: 'message', + label: 'Zpráva', + kind: 'text', + required: true, + hint: 'Může obsahovat odkazy, například {{data.order.code}}.', + }, + ], }, ], }, diff --git a/src/routes/ticketActions.ts b/src/routes/ticketActions.ts index cb6ef43..0b46cf0 100644 --- a/src/routes/ticketActions.ts +++ b/src/routes/ticketActions.ts @@ -270,6 +270,11 @@ async function runAction( tenantId: ticket.tenantId, ticketId: ticket.id, idempotencyKey: `ticket:${ticket.id}:${action.id}`, + /* + * Telo akce nema spoustec, parametry jsou udaje ticketu a odpovedi + * z dialogu. Jmeno se rovna ID, takze podminka najde hodnotu i tady. + */ + fieldNames: Object.fromEntries(Object.keys(context).map((key) => [key, key])), }); if (!run.ok) { diff --git a/src/runtime/builtinSteps.ts b/src/runtime/builtinSteps.ts index a1ad3d7..c26ecd9 100644 --- a/src/runtime/builtinSteps.ts +++ b/src/runtime/builtinSteps.ts @@ -513,20 +513,35 @@ const handlers: Record = { * za tri dny) takhle delat nejde - to patri do fronty jako beh naplanovany * na pozdeji, viz `nextAttemptAt`. Strop je proto minuta. */ - 'flow/pause': async (inputs) => { - const seconds = Math.min(Math.max(Number(inputs.seconds ?? 1) || 1, 0), 60); - await new Promise((resolve) => setTimeout(resolve, seconds * 1_000)); - return { ok: true, summary: `${seconds} s`, detail: null, outputs: {} }; - }, + 'flow/pause': pause, + /* + * Tytéž kroky pod klici z katalogu. + * + * V katalogu jsou jako sluzba `delay` a `log`, ale vykonna cast byla + * registrovana jen pod `flow/...`. Kdo si v builderu vybral Pockat nebo + * Zapsat zpravu, dostal pri behu "operace nema vykonnou cast" - krok slo + * pridat, ale nikdy nefungoval. + */ + 'delay/wait': pause, + 'log/write': write, + 'flow/log': write, +}; - /** Zapis do logu ticketu. Na overeni, ze strom dosel, kam mel. */ - 'flow/log': (inputs) => ({ +async function pause(inputs: Record): Promise { + const seconds = Math.min(Math.max(Number(inputs.seconds ?? 1) || 1, 0), 60); + await new Promise((resolve) => setTimeout(resolve, seconds * 1_000)); + return { ok: true, summary: `${seconds} s`, detail: null, outputs: {} }; +} + +/** Zapis do logu ticketu. Na overeni, ze strom dosel, kam mel. */ +function write(inputs: Record): StepOutcome { + return { ok: true, summary: inputs.message?.trim() || 'zápis', detail: null, outputs: {}, - }), -}; + }; +} function currentTicket(ticketId: string, tenantId: string): Ticket | undefined { return findTicket(ticketId, [tenantId]) ?? findByExternalId(tenantId, ticketId); diff --git a/src/runtime/executor.ts b/src/runtime/executor.ts index ca1094f..b3ce4bc 100644 --- a/src/runtime/executor.ts +++ b/src/runtime/executor.ts @@ -55,6 +55,15 @@ export interface RunOptions { * zapis do udalosti ticketu je potreba puvodni prijata zprava, ne smes. */ trigger?: Record; + /** + * Jmena parametru podle jejich ID. + * + * Podminka si pamatuje **ID** parametru, aby ji prejmenovani nerozbilo. Data + * ale prichazeji pod **jmenem**: parametr `f_9x1` nese hodnotu z klice + * `callSid`, vystup kroku `st_kontakt.idoklad.found` lezi pod `found`. Bez + * teto tabulky se podminka nema o co oprit a tise vychazi jako prazdna. + */ + fieldNames?: Record; } export interface StepResult { @@ -133,11 +142,11 @@ export async function runFlow( } if (step.kind === 'condition') { - const branch = evaluate(step, working) ? 'yes' : 'no'; + const branch = evaluate(step, working, options) ? 'yes' : 'no'; results.push({ stepId: step.id, kind: 'condition', - label: describeCondition(step, working), + label: describeCondition(step, working, options), ok: true, branch, summary: branch === 'yes' ? 'splněno' : 'nesplněno', @@ -459,12 +468,47 @@ function stringify(value: unknown): string { return JSON.stringify(value); } +/** + * Hodnota, na kterou se podminka pta. + * + * Zkousi se v poradi od nejpresnejsiho: presny klic, pak jmeno parametru podle + * jeho ID, pak `krok.jmeno`, nakonec cesta do struktury. Duvod je v tom, ze ID + * parametru a klic v datech nejsou totez - podminka si drzi ID, aby ji + * prejmenovani nerozbilo, ale data chodi pod jmenem. + */ +function conditionValue( + fieldId: string, + context: RunContext, + options: RunOptions, +): unknown { + const candidates = [fieldId]; + + const name = options.fieldNames?.[fieldId]; + if (name) { + candidates.push(name); + // Vystup kroku lezi i pod `krok.jmeno`. Tahle podoba je presnejsi nez + // hole jmeno, ktere pozdejsi krok se stejnym vystupem prepise. + const step = fieldId.split('.')[0]; + if (step !== fieldId) candidates.push(`${step}.${name}`); + } + + candidates.push(nameOf(fieldId)); + + for (const key of candidates) { + const value = context[key]; + if (value !== undefined) return value; + } + + return getPath(context, fieldId); +} + /** Vyhodnoti podminku nad kontextem. */ function evaluate( step: Extract, context: RunContext, + options: RunOptions, ): boolean { - const raw = context[step.fieldId] ?? context[nameOf(step.fieldId)]; + const raw = conditionValue(step.fieldId, context, options); const value = raw === undefined || raw === null ? '' : stringify(raw); const expected = step.value ?? ''; @@ -519,10 +563,12 @@ function compare(operator: ConditionOperator, value: string, expected: string): function describeCondition( step: Extract, context: RunContext, + options: RunOptions, ): string { - const raw = context[step.fieldId] ?? context[nameOf(step.fieldId)]; + const raw = conditionValue(step.fieldId, context, options); const value = raw === undefined || raw === null ? '(prázdné)' : stringify(raw); - const name = nameOf(step.fieldId); + // V logu ma stat jmeno, ktere clovek zna z builderu, ne vnitrni ID. + const name = options.fieldNames?.[step.fieldId] ?? nameOf(step.fieldId); if (isUnary(step.operator)) return `Podmínka: ${name} ${step.operator} (${value})`; return `Podmínka: ${name} ${step.operator} ${step.value ?? ''} (${value})`; diff --git a/src/runtime/worker.ts b/src/runtime/worker.ts index dbffd22..3c8d88a 100644 --- a/src/runtime/worker.ts +++ b/src/runtime/worker.ts @@ -17,6 +17,8 @@ import { createIncident, findOpenIncident } from '../data/incidentStore.js'; import { publish } from '../events/bus.js'; import { withRun } from './context.js'; import { runFlow, type RunResult } from './executor.js'; +import { collectScopes } from '../data/flowScope.js'; +import type { AutomationFlow } from '../data/automationStore.js'; import { claimBatch, markDone, @@ -117,6 +119,9 @@ async function runOne(item: QueueItem): Promise { runFlow(automation.flow.steps, item.payload, { tenantId: item.tenantId, ticketId: item.ticketId, + // Podminka si drzi ID parametru, data chodi pod jmenem. Bez teto + // tabulky by se nemela o co oprit, viz RunOptions.fieldNames. + fieldNames: fieldNamesOf(automation.flow), // Klic je stabilni na beh, takze opakovany pokus nevystavi druhou fakturu. idempotencyKey: `run:${item.id}`, }), @@ -243,3 +248,10 @@ function sleep(ms: number): Promise { export function workerEnabled(): boolean { return config.workerEnabled; } + +/** Jmena parametru podle jejich ID, vcetne vystupu kroku. */ +function fieldNamesOf(flow: AutomationFlow): Record { + const names: Record = {}; + for (const [id, field] of collectScopes(flow).all) names[id] = field.name; + return names; +}