Pauza, zapis do logu a podminky nad vystupy konecne funguji

Vyslo najevo pri stavbe ukazkovych automatizaci nad modelem objednavky.

- log/write a delay/wait nemely vykonnou cast. Byly registrovane jen pod
  klici flow/log a flow/pause, ktere v katalogu nejsou. Kdo si v builderu
  vybral Zapsat zpravu nebo Pockat, dostal pri behu "operace nema vykonnou
  cast" - krok slo pridat, ale nikdy nefungoval.
- Obe operace navic nemely nastavitelna pole, takze do nich neslo napsat,
  co se ma zapsat a jak dlouho cekat.
- Podminka nad vystupem kroku se nevyhodnotila. Podminka si drzi ID
  parametru, aby ji prejmenovani nerozbilo, ale data chodi pod jmenem:
  parametr f_9x1 nese hodnotu z klice callSid, vystup kroku
  st_kontakt.idoklad.found lezi pod found. Beh dostava tabulku jmen podle
  ID, takze se ma o co oprit. Bez toho podminka tise vychazela jako prazdna
  a slo se vzdy vetvi NE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-20 07:04:30 +02:00
co-authored by Claude Opus 5
parent 9b18531d3e
commit 99561c4682
5 changed files with 111 additions and 15 deletions
+19 -1
View File
@@ -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}}.',
},
],
},
],
},
+5
View File
@@ -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) {
+24 -9
View File
@@ -513,20 +513,35 @@ const handlers: Record<string, Handler> = {
* 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<string, string>): Promise<StepOutcome> {
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<string, string>): 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);
+51 -5
View File
@@ -55,6 +55,15 @@ export interface RunOptions {
* zapis do udalosti ticketu je potreba puvodni prijata zprava, ne smes.
*/
trigger?: Record<string, unknown>;
/**
* 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<string, string>;
}
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<FlowStep, { kind: 'condition' }>,
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<FlowStep, { kind: 'condition' }>,
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})`;
+12
View File
@@ -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<void> {
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<void> {
export function workerEnabled(): boolean {
return config.workerEnabled;
}
/** Jmena parametru podle jejich ID, vcetne vystupu kroku. */
function fieldNamesOf(flow: AutomationFlow): Record<string, string> {
const names: Record<string, string> = {};
for (const [id, field] of collectScopes(flow).all) names[id] = field.name;
return names;
}