w4c-workflows-api/Services/Nodes/NodeGraphRunner.Invocation.cs
Vitali sharp8n 42ffcb9adc workflows
2026-09-13 19:28:47 +03:00

141 lines
5.3 KiB
C#

using System.Text.Json.Nodes;
using w4c_workflows.Models.Credentials;
using w4c_workflows.Models.Nodes;
using w4c_workflows.Services.Nodes.Interpolation;
using w4c_workflows.Services.Security;
namespace w4c_workflows.Services.Nodes;
// Invocation half of the graph runner: resolving a node's parameters per item,
// building its execution context and running the executor (one call per item, or
// the whole array at once for all-items nodes).
public sealed partial class NodeGraphRunner
{
private async Task<NodeExecutionOutcome> InvokeAsync(
NodeGraphNode node,
IReadOnlyList<IReadOnlyList<FlowItem>> inputs,
NodeRunEnvironment environment,
IReadOnlyDictionary<string, List<IReadOnlyList<FlowItem>>> outputs,
NodeGraph graph,
int runIndex,
CancellationToken ct)
{
var executor = _executors.Resolve(node.Blueprint.Type)
?? throw new InvalidOperationException($"no executor for '{node.Blueprint.Type}'");
var loopBackInput = graph.EdgesTo(node.Id).Any(e => e.IsLoopBack);
// Nodes with several inputs, or an explicit all-items run mode, receive the
// whole array at once; the default is one invocation per input item.
var allItems = node.EffectiveRunMode == NodeRunMode.AllItems || node.Blueprint.Inputs.Count > 1;
if (allItems)
{
var parameters = ResolveParameters(node, null, 0, environment, outputs, graph, runIndex);
return await executor.RunAsync(
BuildContext(node, parameters, inputs, environment, 0, runIndex, loopBackInput), ct);
}
var mainItems = inputs.Count > 0 ? inputs[0] : Array.Empty<FlowItem>();
// A node with no incoming items is skipped, except the entry node: like a
// trigger/source it runs once with an empty item so it can generate data.
var isEntry = string.Equals(node.Id, graph.EntryNodeId, StringComparison.Ordinal);
if (mainItems.Count == 0 && !isEntry)
return NodeExecutionOutcome.Empty;
if (mainItems.Count == 0)
mainItems = new[] { new FlowItem { Json = new JsonObject() } };
var accumulated = new List<List<FlowItem>>();
for (var index = 0; index < mainItems.Count; index++)
{
var parameters = ResolveParameters(node, mainItems[index], index, environment, outputs, graph, runIndex);
var outcome = await executor.RunAsync(
BuildContext(node, parameters, new[] { (IReadOnlyList<FlowItem>)new[] { mainItems[index] } }, environment, index, runIndex, loopBackInput),
ct);
if (!outcome.Succeeded)
return outcome;
while (accumulated.Count < outcome.Outputs.Count)
accumulated.Add(new List<FlowItem>());
for (var port = 0; port < outcome.Outputs.Count; port++)
accumulated[port].AddRange(outcome.Outputs[port]);
}
return new NodeExecutionOutcome
{
Outputs = accumulated.Select(list => (IReadOnlyList<FlowItem>)list).ToList(),
};
}
private NodeExecutionContext BuildContext(
NodeGraphNode node,
JsonObject parameters,
IReadOnlyList<IReadOnlyList<FlowItem>> inputs,
NodeRunEnvironment environment,
int itemIndex,
int runIndex,
bool loopBackInput = false)
=> new()
{
Blueprint = node.Blueprint,
Parameters = parameters,
Inputs = inputs,
Credentials = environment.Credentials,
Environment = environment.Variables,
TenantId = environment.TenantId,
RunId = environment.RunId,
TaskId = node.Id,
NodeName = node.Id,
ItemIndex = itemIndex,
RunIndex = runIndex,
WorkingDirectory = environment.WorkingDirectory,
LoopBackInput = loopBackInput,
State = environment.State,
SubWorkflows = environment.SubWorkflows,
Depth = environment.Depth,
Services = environment.Services,
};
private JsonObject ResolveParameters(
NodeGraphNode node,
FlowItem? item,
int itemIndex,
NodeRunEnvironment environment,
IReadOnlyDictionary<string, List<IReadOnlyList<FlowItem>>> outputs,
NodeGraph graph,
int runIndex)
{
var scope = new InterpolationScope
{
Item = item?.Json,
Parameters = node.Parameters,
Environment = environment.Variables,
ItemIndex = itemIndex,
RunIndex = runIndex,
NodeItems = name => ResolveNodeItems(name, outputs, graph),
};
return _interpolator.ResolveObject(node.Parameters, scope);
}
private static IReadOnlyList<FlowItem> ResolveNodeItems(
string name,
IReadOnlyDictionary<string, List<IReadOnlyList<FlowItem>>> outputs,
NodeGraph graph)
{
if (outputs.TryGetValue(name, out var direct) && direct.Count > 0)
return direct[0];
// Fall back to the blueprint display name, so $("Send message") also works.
var byName = graph.FindByDisplayName(name);
if (byName != null && outputs.TryGetValue(byName.Id, out var named) && named.Count > 0)
return named[0];
return Array.Empty<FlowItem>();
}
}