using System.Text.Json.Nodes;
namespace XFEWorkflow.Abstractions;
public static class WorkflowTriggerNodes
{
public static readonly IReadOnlyList<CapabilityDescriptor> BuiltIns = new[]
{
Define(TriggerKind.Interval, "间隔定时", "每隔一段时间,从此节点开始执行。"),
Define(TriggerKind.Daily, "每日定时", "每天在指定的本机时间,从此节点开始执行。"),
Define(TriggerKind.FileChanged, "文件变化", "监听目录,将变化文件的路径传给后续节点。"),
Define(TriggerKind.WorkflowCompleted, "工作流完成", "监听另一个工作流的结果,将其输出传给后续节点。")
};
public static string CapabilityId(TriggerKind kind) => "core.trigger." + kind.ToString().ToLowerInvariant();
private static CapabilityDescriptor Define(TriggerKind kind, string name, string description) => AsNodeDescriptor(new()
{ Id = CapabilityId(kind), PluginId = "core", Name = name, Description = description, IsTrigger = true });
public static CapabilityDescriptor AsNodeDescriptor(CapabilityDescriptor descriptor)
{
if (!descriptor.IsTrigger) return descriptor;
var outputs = WorkflowJson.Clone(descriptor.Outputs);
// Older SDKs exposed inputs/event for every trigger, even if its manifest
// declared named fields. Keep those bindings until a plugin opts into paths.
if (!outputs.Any(x => x.EventPath is not null))
{
if (!outputs.Any(x => x.Name == "inputs")) outputs.Insert(0, new() { Name = "inputs", Label = "工作流输入", Type = WorkflowValueType.Object });
if (!outputs.Any(x => x.Name == "event")) outputs.Add(new() { Name = "event", Label = "事件数据", Type = WorkflowValueType.Object });
outputs.Single(x => x.Name == "event").EventPath = "";
}
return new() { Id = descriptor.Id, PluginId = descriptor.PluginId, Name = descriptor.Name, Description = descriptor.Description,
Category = "触发器", IsTrigger = true, ExecutionOutputs = ["next"], Outputs = outputs };
}
public static bool IsTrigger(NodeDefinition node) => node.Trigger is not null || node.CapabilityId.StartsWith("core.trigger.", StringComparison.Ordinal);
public static TriggerDefinition Create(CapabilityDescriptor descriptor)
{
var kind = BuiltIns.Any(x => x.Id == descriptor.Id)
? Enum.GetValues<TriggerKind>().Single(x => CapabilityId(x) == descriptor.Id) : TriggerKind.Plugin;
var trigger = new TriggerDefinition { Kind = kind, Enabled = true, CapabilityId = kind == TriggerKind.Plugin ? descriptor.Id : "" };
foreach (var port in descriptor.Inputs) if (port.DefaultValue is not null) trigger.Parameters[port.Name] = port.DefaultValue.DeepClone();
return trigger;
}
public static IEnumerable<TriggerDefinition> Enumerate(WorkflowDefinition workflow)
{
foreach (var node in workflow.Graph.Nodes.Where(x => x.Trigger is not null))
{ node.Trigger!.Id = node.Id; yield return node.Trigger; }
}
// Legacy rules are converted once into entries pointing to the original
// first step. The manual entry and all data bindings keep their IDs.
public static void Migrate(WorkflowDefinition workflow)
{
workflow.Enabled ??= workflow.Triggers.Any(x => x.Enabled) || workflow.Graph.Nodes.Any(x => x.Trigger?.Enabled == true);
var start = workflow.Graph.Nodes.FirstOrDefault(x => x.CapabilityId == "core.start");
var next = start is null ? null : workflow.Graph.Edges.FirstOrDefault(x => x.Kind == PortKind.Execution && x.FromNode == start.Id && x.FromPort == "next");
foreach (var trigger in workflow.Triggers.Where(x => x.Kind != TriggerKind.Manual))
{
if (workflow.Graph.Nodes.Any(x => x.Trigger?.Id == trigger.Id || x.Id == trigger.Id)) continue;
var node = new NodeDefinition { Id = trigger.Id, CapabilityId = trigger.Kind == TriggerKind.Plugin ? trigger.CapabilityId : CapabilityId(trigger.Kind),
Name = BuiltIns.FirstOrDefault(x => x.Id == CapabilityId(trigger.Kind))?.Name ?? "插件事件", Trigger = WorkflowJson.Clone(trigger),
X = start?.X ?? 120, Y = (start?.Y ?? 120) + 240 * (workflow.Graph.Nodes.Count(IsTrigger) + 1) };
workflow.Graph.Nodes.Add(node);
if (next is not null) workflow.Graph.Edges.Add(new() { FromNode = node.Id, ToNode = next.ToNode, ToPort = next.ToPort });
}
workflow.Triggers.Clear();
foreach (var node in workflow.Graph.Nodes.Where(x => x.Trigger is not null)) { node.Trigger!.Id = node.Id; node.Trigger.Enabled = true; }
}
public static string? ConfigurationError(TriggerDefinition trigger, IReadOnlyDictionary<string, CapabilityDescriptor> catalog, string workflowId)
{
if (!trigger.Enabled) return null;
if (trigger.Kind == TriggerKind.Interval && trigger.IntervalSeconds < 1) return "间隔必须至少为 1 秒";
if (trigger.Kind == TriggerKind.Daily && !TimeOnly.TryParseExact(trigger.DailyTime, "HH:mm", out _)) return "每日时间必须为 HH:mm";
if (trigger.Kind == TriggerKind.FileChanged && (string.IsNullOrWhiteSpace(trigger.Path) || string.IsNullOrWhiteSpace(trigger.Filter))) return "请配置监听目录与文件筛选";
if (trigger.Kind == TriggerKind.Plugin && (!catalog.TryGetValue(trigger.CapabilityId, out var plugin) || !plugin.IsTrigger)) return "插件触发能力缺失或已禁用";
if (trigger.Kind == TriggerKind.WorkflowCompleted && (string.IsNullOrWhiteSpace(trigger.SourceWorkflowId) || trigger.SourceWorkflowId == workflowId)) return "请选择其他工作流作为上游";
if (trigger.InputBindings.ContainsKey("trigger")) return "trigger 为系统保留的事件信息";
return null;
}
}
using System.Text.Json.Nodes;
namespace XFEWorkflow.Abstractions;
public static class WorkflowTriggerNodes
{
public static readonly IReadOnlyList<CapabilityDescriptor> BuiltIns = new[]
{
Define(TriggerKind.Interval, "间隔定时", "每隔一段时间,从此节点开始执行。"),
Define(TriggerKind.Daily, "每日定时", "每天在指定的本机时间,从此节点开始执行。"),
Define(TriggerKind.FileChanged, "文件变化", "监听目录,将变化文件的路径传给后续节点。"),
Define(TriggerKind.WorkflowCompleted, "工作流完成", "监听另一个工作流的结果,将其输出传给后续节点。")
};
public static string CapabilityId(TriggerKind kind) => "core.trigger." + kind.ToString().ToLowerInvariant();
private static CapabilityDescriptor Define(TriggerKind kind, string name, string description) => AsNodeDescriptor(new()
{ Id = CapabilityId(kind), PluginId = "core", Name = name, Description = description, IsTrigger = true });
public static CapabilityDescriptor AsNodeDescriptor(CapabilityDescriptor descriptor)
{
if (!descriptor.IsTrigger) return descriptor;
var outputs = WorkflowJson.Clone(descriptor.Outputs);
// Older SDKs exposed inputs/event for every trigger, even if its manifest
// declared named fields. Keep those bindings until a plugin opts into paths.
if (!outputs.Any(x => x.EventPath is not null))
{
if (!outputs.Any(x => x.Name == "inputs")) outputs.Insert(0, new() { Name = "inputs", Label = "工作流输入", Type = WorkflowValueType.Object });
if (!outputs.Any(x => x.Name == "event")) outputs.Add(new() { Name = "event", Label = "事件数据", Type = WorkflowValueType.Object });
outputs.Single(x => x.Name == "event").EventPath = "";
}
return new() { Id = descriptor.Id, PluginId = descriptor.PluginId, Name = descriptor.Name, Description = descriptor.Description,
Category = "触发器", IsTrigger = true, ExecutionOutputs = ["next"], Outputs = outputs };
}
public static bool IsTrigger(NodeDefinition node) => node.Trigger is not null || node.CapabilityId.StartsWith("core.trigger.", StringComparison.Ordinal);
public static TriggerDefinition Create(CapabilityDescriptor descriptor)
{
var kind = BuiltIns.Any(x => x.Id == descriptor.Id)
? Enum.GetValues<TriggerKind>().Single(x => CapabilityId(x) == descriptor.Id) : TriggerKind.Plugin;
var trigger = new TriggerDefinition { Kind = kind, Enabled = true, CapabilityId = kind == TriggerKind.Plugin ? descriptor.Id : "" };
foreach (var port in descriptor.Inputs) if (port.DefaultValue is not null) trigger.Parameters[port.Name] = port.DefaultValue.DeepClone();
return trigger;
}
public static IEnumerable<TriggerDefinition> Enumerate(WorkflowDefinition workflow)
{
foreach (var node in workflow.Graph.Nodes.Where(x => x.Trigger is not null))
{ node.Trigger!.Id = node.Id; yield return node.Trigger; }
}
// Legacy rules are converted once into entries pointing to the original
// first step. The manual entry and all data bindings keep their IDs.
public static void Migrate(WorkflowDefinition workflow)
{
workflow.Enabled ??= workflow.Triggers.Any(x => x.Enabled) || workflow.Graph.Nodes.Any(x => x.Trigger?.Enabled == true);
var start = workflow.Graph.Nodes.FirstOrDefault(x => x.CapabilityId == "core.start");
var next = start is null ? null : workflow.Graph.Edges.FirstOrDefault(x => x.Kind == PortKind.Execution && x.FromNode == start.Id && x.FromPort == "next");
foreach (var trigger in workflow.Triggers.Where(x => x.Kind != TriggerKind.Manual))
{
if (workflow.Graph.Nodes.Any(x => x.Trigger?.Id == trigger.Id || x.Id == trigger.Id)) continue;
var node = new NodeDefinition { Id = trigger.Id, CapabilityId = trigger.Kind == TriggerKind.Plugin ? trigger.CapabilityId : CapabilityId(trigger.Kind),
Name = BuiltIns.FirstOrDefault(x => x.Id == CapabilityId(trigger.Kind))?.Name ?? "插件事件", Trigger = WorkflowJson.Clone(trigger),
X = start?.X ?? 120, Y = (start?.Y ?? 120) + 240 * (workflow.Graph.Nodes.Count(IsTrigger) + 1) };
workflow.Graph.Nodes.Add(node);
if (next is not null) workflow.Graph.Edges.Add(new() { FromNode = node.Id, ToNode = next.ToNode, ToPort = next.ToPort });
}
workflow.Triggers.Clear();
foreach (var node in workflow.Graph.Nodes.Where(x => x.Trigger is not null)) { node.Trigger!.Id = node.Id; node.Trigger.Enabled = true; }
}
public static string? ConfigurationError(TriggerDefinition trigger, IReadOnlyDictionary<string, CapabilityDescriptor> catalog, string workflowId)
{
if (!trigger.Enabled) return null;
if (trigger.Kind == TriggerKind.Interval && trigger.IntervalSeconds < 1) return "间隔必须至少为 1 秒";
if (trigger.Kind == TriggerKind.Daily && !TimeOnly.TryParseExact(trigger.DailyTime, "HH:mm", out _)) return "每日时间必须为 HH:mm";
if (trigger.Kind == TriggerKind.FileChanged && (string.IsNullOrWhiteSpace(trigger.Path) || string.IsNullOrWhiteSpace(trigger.Filter))) return "请配置监听目录与文件筛选";
if (trigger.Kind == TriggerKind.Plugin && (!catalog.TryGetValue(trigger.CapabilityId, out var plugin) || !plugin.IsTrigger)) return "插件触发能力缺失或已禁用";
if (trigger.Kind == TriggerKind.WorkflowCompleted && (string.IsNullOrWhiteSpace(trigger.SourceWorkflowId) || trigger.SourceWorkflowId == workflowId)) return "请选择其他工作流作为上游";
if (trigger.InputBindings.ContainsKey("trigger")) return "trigger 为系统保留的事件信息";
return null;
}
}