/
h0tnanny
/
IotPlatform
Обзор
Документация
Войти
/
h0tnanny
/
IotPlatform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
src/entities/Workflow.ts
295 строк
9 KB
h0tnanny
Добавление функциональных требований
08 фев 2026, 23:02
08 фев 2026, 23:02
a8a23c5
Код
Авторство
О чём код?
import { Field } from './Field'; import { BaseFunction, ExecutionContext } from '../workflow/BaseFunction'; import { LoopNode } from '../workflow/nodes/LoopNode'; import { ConditionNode } from '../workflow/nodes/ConditionNode'; /** * Информация о переходе между узлами */ interface TransitionInfo { fromNodeId: string; fromNodeName: string; toNodeId: string | null; transitionLabel: string; // Например: "(If: true)", "(Loop: false)", "" } /** * Запись лога выполнения */ export interface LogEntry { timestamp: number; level: 'info' | 'success' | 'warning' | 'error'; message: string; nodeId?: string; } /** * Состояние выполнения workflow */ export interface WorkflowExecutionState { isRunning: boolean; currentNodeId: string | null; executedNodes: string[]; executionPath: string[]; logs: LogEntry[]; startTime?: number; error?: string; } /** * Тип триггера для запуска workflow */ export type TriggerType = 'manual' | 'interval' | 'cron' | 'condition'; /** * Конфигурация триггера */ export interface TriggerConfig { // Для interval: интервал в мс intervalMs?: number; // Для cron: cron-выражение (5 полей: мин час день_месяца месяц день_недели) cronExpression?: string; // Для condition: переменная, оператор, пороговое значение conditionVariable?: string; conditionOperator?: '==' | '!=' | '>' | '<' | '>=' | '<='; conditionValue?: unknown; } /** * Entity "Workflow" - граф логики с узлами и переходами */ export class Workflow { public readonly nodes: Map<string, BaseFunction> = new Map(); public readonly variableList: Map<string, Field> = new Map(); public name?: string; public description?: string; public updateInterval?: number; public triggerType: TriggerType = 'manual'; public triggerConfig: TriggerConfig = {}; // Состояние выполнения для отслеживания public executionState: WorkflowExecutionState = { isRunning: false, currentNodeId: null, executedNodes: [], executionPath: [], logs: [], }; constructor( public readonly id: string, public isActivated: boolean, public startNodeId: string, name?: string, description?: string, updateInterval?: number, triggerType?: TriggerType, triggerConfig?: TriggerConfig ) { this.name = name; this.description = description; this.updateInterval = updateInterval; this.triggerType = triggerType || 'manual'; this.triggerConfig = triggerConfig || {}; } /** * Добавляет узел в workflow */ addNode(node: BaseFunction): void { this.nodes.set(node.id, node); } /** * Добавляет переменную в workflow */ addVariable(field: Field): void { this.variableList.set(field.name, field); } /** * Добавляет запись в лог выполнения */ private addLog(level: 'info' | 'success' | 'warning' | 'error', message: string, nodeId?: string): void { const logEntry: LogEntry = { timestamp: Date.now(), level, message, nodeId, }; this.executionState.logs.push(logEntry); // Ограничиваем размер логов (последние 100 записей) if (this.executionState.logs.length > 100) { this.executionState.logs.shift(); } } /** * Получает информацию о переходе для логирования */ private getTransitionInfo( node: BaseFunction, nextNodeId: string | null ): TransitionInfo { let transitionLabel = ''; if (node instanceof ConditionNode) { // Для ConditionNode определяем путь по nextNodeId if (nextNodeId === node.trueNextId) { transitionLabel = '(If: true)'; } else if (nextNodeId === node.falseNextId) { transitionLabel = '(If: false)'; } } else if (node instanceof LoopNode) { // Для LoopNode определяем путь по nextNodeId if (nextNodeId === node.bodyNodeId) { transitionLabel = '(Loop: true)'; } else if (nextNodeId === node.exitNodeId) { transitionLabel = '(Loop: false)'; } } return { fromNodeId: node.id, fromNodeName: node.name, toNodeId: nextNodeId, transitionLabel, }; } /** * Формирует строку пути выполнения с метками переходов * Формат: "Node A -> Node B (If: true) -> Node D" * Метка условия добавляется после имени узла, из которого идет переход с условием */ private formatExecutionPath(transitions: TransitionInfo[]): string { if (transitions.length === 0) { return ''; } const parts: string[] = []; for (let i = 0; i < transitions.length; i++) { const transition = transitions[i]; // Для первого перехода добавляем имя начального узла if (i === 0) { parts.push(transition.fromNodeName); } // Если есть следующий узел, формируем переход if (transition.toNodeId) { // Добавляем метку перехода после имени текущего узла (если есть) // Это создаст формат: "Node A -> Node B (If: true) -> Node D" if (transition.transitionLabel) { parts.push(transition.transitionLabel); } parts.push('->'); // Добавляем имя следующего узла const nextNode = this.nodes.get(transition.toNodeId); const nextNodeName = nextNode ? nextNode.name : transition.toNodeId; parts.push(nextNodeName); } } return parts.join(' '); } /** * Выполняет workflow, начиная с startNodeId * Переходит по цепочке узлов, пока nextNodeId не станет null/undefined */ async invoke(): Promise<void> { if (!this.isActivated) { return; } // Инициализируем состояние выполнения this.executionState = { isRunning: true, currentNodeId: this.startNodeId, executedNodes: [], executionPath: [], logs: [], startTime: Date.now(), }; this.addLog('info', `🚀 Запуск Workflow "${this.name || this.id}"`); // Сбрасываем счетчики итераций для всех LoopNode for (const node of this.nodes.values()) { if (node instanceof LoopNode) { node.reset(); } } const executionPath: string[] = []; const transitions: TransitionInfo[] = []; let currentNodeId: string | null = this.startNodeId; let stepCount = 0; const maxSteps = 1000; // Защита от бесконечных циклов while (currentNodeId !== null) { if (stepCount >= maxSteps) { this.executionState.error = 'Достигнуто максимальное количество шагов'; break; } const node = this.nodes.get(currentNodeId); if (!node) { const errorMsg = `Узел "${currentNodeId}" не найден`; this.executionState.error = errorMsg; this.addLog('error', `❌ ${errorMsg}`); break; } // Обновляем состояние выполнения this.executionState.currentNodeId = currentNodeId; executionPath.push(currentNodeId); stepCount++; const context: ExecutionContext = { variableList: this.variableList, currentNodeId, executionPath: [...executionPath], addLog: this.addLog.bind(this), }; this.addLog('info', `▶ Выполнение: ${node.name}`, node.id); const nextNodeId = node.execute(context); // Добавляем узел в выполненные this.executionState.executedNodes.push(currentNodeId); this.executionState.executionPath.push(node.name); // Небольшая задержка для визуализации (50мс) await new Promise(resolve => setTimeout(resolve, 50)); // Получаем информацию о переходе const transitionInfo = this.getTransitionInfo(node, nextNodeId); transitions.push(transitionInfo); if (nextNodeId === null) { this.addLog('success', `✓ Узел "${node.name}" завершен`, node.id); break; } currentNodeId = nextNodeId; } const finalPath = this.formatExecutionPath(transitions); const pathString = finalPath || executionPath.join(' -> '); if (pathString) { this.addLog('info', `📊 Путь: ${pathString}`); } this.addLog('success', `✅ Завершено. Шагов: ${stepCount}`); // Завершаем выполнение this.executionState.isRunning = false; this.executionState.currentNodeId = null; } }