XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEWorkflow

【WinUI】XFE基于windows的工作流

公开
关注 0 Fork 0 Star 0
UTF-8
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;
    }
}