Graphkey:使用占位符彻底解耦函数与工作流

在构建数据处理流水线、规则引擎或任何需要动态编排任务的系统时,我们常常面临一个核心问题:如何让任务节点(函数)与工作流的拓扑结构解耦?

传统的做法是在节点内部硬编码输入数据的来源,例如直接引用上一个节点的输出键。这种方式虽然简单,但会导致节点与特定工作流强绑定,难以复用。一旦工作流调整,节点就必须修改。

Graphkey 是一个轻量级的动态工作流构建库,它通过引入占位符(Placeholder) 机制,将数据依赖关系从节点内部提升到工作流构建层。节点只需声明"我需要什么输入",而由构建器在组装时负责"从哪里获取"。这种设计让节点成为真正的"纯函数",实现了函数与工作流的完全解耦。

核心概念

Graphkey 的工作流由三个核心元素组成:

节点(Node)

节点是一个 IDictionary<string, object> 字典,必须包含一个名为 funcPipelineConfig 的配置字典。配置中定义了该节点的输入参数键列表(ParameterKeys)和输出结果键列表(ResultKeys)。节点还可以包含可选的 NodeId 和用于显示的 Label 或 Symbol。

占位符(Placeholder)

占位符是一个轻量级对象,描述对数据的引用意图,而不是具体的键名。例如 Param.Input("userId") 表示该参数来源于工作流的原始输入,键名为 "userId"。占位符只在构建阶段存在,最终会被解析为普通字符串。

构建器(DynamicWorkflowBuilder)和链(DynamicChainBuilder)

构建器负责管理所有节点和输入键,并提供链式 API 来顺序添加节点。每个链内部维护上一个节点的输出列表,使得 Param.Prev 能够正确解析。

核心功能:占位符如何实现解耦

Graphkey 提供了四种占位符类型,覆盖了常见的数据引用场景:

这些占位符可以灵活组合,并且支持链式索引器。例如,Param.From("nodeA")["resultX"] 明确表示引用节点 nodeA 的输出中键为 "resultX" 的那个值。

当你在链式构建器中添加节点时,构建器会立即解析节点 ParameterKeys 中的每个占位符:

Param.Input 将输入键注册到全局输入集合,并直接返回该键名。

Param.Prev 根据当前链的上一个节点输出列表解析为具体输出键。

Param.From 查询已添加节点的输出列表(通过节点 ID 映射),解析为具体输出键。

Param.Node 根据节点索引查询对应节点的输出列表,解析为具体输出键。

解析成功后,占位符被替换为实际的字符串键。最终生成的工作流中,所有节点的 ParameterKeys 都是普通字符串,不包含任何占位符对象。这意味着最终的工作流 JSON 是纯粹的数据结构,可以被任何下游系统消费,完全不知道占位符的存在。

快速示例

以下示例展示如何创建一个包含两个节点的工作流:第一个节点接收输入 "input1" 和 "input2",输出 "resultA";第二个节点引用第一个节点的输出(通过前序占位符)和另一个输入 "input3",输出 "finalResult"。

csharp 复制代码
using Graphkey;

// 1. 创建构建器
var builder = new DynamicWorkflowBuilder();

// 2. 定义第一个节点(使用 Dictionary)
var node1 = new Dictionary<string, object>
{
    ["Label"] = "First Node",
    ["funcPipelineConfig"] = new Dictionary<string, object>
    {
        ["ParameterKeys"] = new List<object> { Param.Input("input1"), Param.Input("input2") },
        ["ResultKeys"] = new List<string> { "resultA" }
    },
    ["NodeId"] = "nodeA"   // 可选,便于后续引用
};

// 3. 定义第二个节点(使用 ExpandoObject)
dynamic node2 = new ExpandoObject();
node2.Label = "Second Node";
node2.funcPipelineConfig = new ExpandoObject();
node2.funcPipelineConfig.ResultKeys = new List<string> { "finalResult" };
node2.funcPipelineConfig.ParameterKeys = new List<object>
{
    Param.From("nodeA"),          // 自动解析 nodeA 的唯一输出
    Param.Input("input3")
};

// 4. 链式构建
builder.CreateChain("example")
       .Then(node1)
       .Then((object)node2)       // 注意 dynamic 需显式转换为 object
       .EndChain();

// 5. 构建并输出 JSON
string json = builder.BuildJson(format: true);
Console.WriteLine(json);

输出 JSON 示例:

{ "Label": "First Node", "funcPipelineConfig": { "ParameterKeys": \[ "input1", "input2" \], "ResultKeys": \[ "resultA"

}

},

{

"Label": "Second Node",

"funcPipelineConfig": {

"ParameterKeys": [ "resultA", "input3" ],

"ResultKeys": [ "finalResult" ]

}

}

]

可以看到,最终 JSON 中的 ParameterKeys 已经是普通字符串,占位符已完全消失。

占位符解析原理

Graphkey 的占位符解析发生在构建阶段,而不是运行时。这带来了几个好处:

提前验证:如果占位符引用了不存在的节点 ID、索引越界或键名错误,构建器会立即抛出详细异常,包含链名称和节点信息,避免运行时才发现问题。

性能开销为零:运行时工作流只包含纯字符串,无需解析任何对象。

可序列化:最终工作流可以轻松序列化为 JSON/XML 等格式,跨语言、跨系统传输。

解析过程中,构建器维护两个关键数据结构:

全局输入键集合(来自 Param.Input)。

节点 ID 到输出列表的映射(来自节点的 ResultKeys 和可选的 NodeId)。

当解析 Param.Prev 或 Param.From 时,构建器会查询这些集合,确保引用有效。如果引用不明确(例如上一个节点有多个输出但未指定索引或键),也会抛出异常,引导用户明确意图。

独立解析器工具

除了在构建时解析,Graphkey 还提供了一个静态类 PlaceholderResolver,允许你在构建后对已有的节点列表执行占位符解析。这在某些场景下很有用,例如从外部加载带有占位符的工作流定义,然后在内存中解析。

csharp

List nodes = LoadNodesFromJson(); // 节点中可能含有 Placeholder 对象

PlaceholderResolver.ResolvePlaceholders(nodes);

// 现在 nodes 中所有 ParameterKeys 都已解析为字符串

ResolvePlaceholders 会原地修改节点,并移除所有 NodeId,使输出与 Build(true) 的结果一致。

注意事项

节点必须实现 IDictionary<string, object>:Graphkey 默认使用字典操作,因此节点可以是 Dictionary<string, object>、ExpandoObject 或任何实现该接口的类型。

输出键必须全局唯一:在同一工作流中,所有节点的输出键(包括输入键)不能重复,除非你显式关闭验证(不推荐)。这保证了每个数据键在整个工作流中都有唯一来源。

节点 ID 作用域:节点 ID 在整个构建器范围内必须唯一,但在最终输出中通常会被移除,因此仅用于构建时引用。

占位符不能同时设置索引和键:Placeholder 对象禁止同时设置 Index 和 Key,否则会抛出异常。这是为了避免歧义。

链与链之间的依赖:如果需要引用其他链中的节点,必须使用 Param.From 或 Param.Node,因为 Param.Prev 只感知当前链。

总结

Graphkey 通过占位符机制,成功地将数据依赖关系从节点内部剥离出来,交由工作流构建器管理。节点只需声明自己的输入和输出键,而无需关心这些键来自何处。这种设计带来了巨大的灵活性:

节点可复用:同一个节点可以在不同工作流中,通过不同的占位符引用不同的数据源。

工作流可动态组装:可以在构建时决定节点之间的连接关系,甚至支持条件性连接。

验证前置:所有引用错误都在构建阶段被捕获,避免运行时崩溃。

无论你是构建数据处理流水线、规则引擎,还是复杂的编排系统,Graphkey 都能帮助你快速、安全地组装可维护的工作流定义。它的占位符机制,正是实现函数与工作流解耦的关键钥匙。

csharp 复制代码
namespace Graphkey
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Dynamic;
    using System.Linq;
    using System.Text.Encodings.Web;
    using System.Text.Json;
    using static GraphkeyConstants;

    // ========== 常量定义 ==========
    internal static class GraphkeyConstants
    {
        public const string FuncPipelineConfig = "funcPipelineConfig";
        public const string NodeId = "NodeId";
        public const string Symbol = "Symbol";
        public const string Label = "Label";
        public const string FuncName = "FuncName";
        public const string ParameterKeys = "ParameterKeys";
        public const string ResultKeys = "ResultKeys";
        public const string UnknownDisplay = "unknown";
        public const string DefaultResultKeyFormat = "{0}_Result";
    }

    // ========== 节点字典辅助类 ==========
    internal static class NodeDictionaryHelper
    {
        public static IDictionary<string, object> GetConfig(object node) =>
            (node as IDictionary<string, object>)?.TryGetValue(FuncPipelineConfig, out var cfg) == true
                ? cfg as IDictionary<string, object> ?? throw new ArgumentException($"funcPipelineConfig must be IDictionary<string, object>, got {cfg?.GetType()}")
                : throw new ArgumentException("Node must contain a 'funcPipelineConfig' entry.");

        public static string GetNodeId(object node) =>
            (node as IDictionary<string, object>)?.TryGetValue(NodeId, out var id) == true ? id?.ToString() : null;

        public static IEnumerable<object> GetParameterKeys(object node) =>
            GetConfig(node).TryGetValue(ParameterKeys, out var keys)
                ? keys switch
                {
                    IEnumerable<string> strEnum => strEnum.Cast<object>(),
                    IEnumerable<object> objEnum => objEnum,
                    _ => throw new ArgumentException($"ParameterKeys must be enumerable of strings or objects, got {keys?.GetType()}")
                }
                : Enumerable.Empty<object>();

        public static IEnumerable<string> GetResultKeys(object node) =>
            GetConfig(node).TryGetValue(ResultKeys, out var keys) && keys is IEnumerable<string> e ? e : Enumerable.Empty<string>();

        public static void SetParameterKeys(object node, IEnumerable<object> keys)
        {
            var config = GetConfig(node);
            config[ParameterKeys] = config.TryGetValue(ParameterKeys, out var existing)
                ? existing switch
                {
                    List<string> _ => keys.Select(k => k?.ToString()).ToList(),
                    List<object> _ => keys.ToList(),
                    string[] _ => keys.Select(k => k?.ToString()).ToArray(),
                    object[] _ => keys.ToArray(),
                    _ => keys.ToList()
                }
                : keys.ToList();
        }

        public static string GetDisplayName(object node) =>
            node is IDictionary<string, object> dict
                ? dict.TryGetValue(Symbol, out var sym) && sym != null ? sym.ToString()
                : dict.TryGetValue(Label, out var label) && label != null ? label.ToString()
                : UnknownDisplay
                : UnknownDisplay;

        public static object CloneNode(object node)
        {
            if (!(node is IDictionary<string, object> original))
                throw new ArgumentException("Node must be an IDictionary<string, object> for cloning.");

            var copy = new ExpandoObject();
            var nodeCopyDict = (IDictionary<string, object>)copy;
            foreach (var kv in original)
                nodeCopyDict[kv.Key] = kv.Key == FuncPipelineConfig
                    ? kv.Value is IDictionary<string, object> configDict ? CloneDictionary(configDict) : throw new ArgumentException("funcPipelineConfig must be an ExpandoObject.")
                    : kv.Value;
            return copy;
        }

        private static object DeepCloneValue(object value)
        {
            if (value == null) return null;
            if (value is IDictionary<string, object> dict) return CloneDictionary(dict);
            if (value is IList list)
            {
                var type = list.GetType();
                var elemType = type.IsArray ? type.GetElementType() : typeof(object);
                var newList = type.IsArray ? Array.CreateInstance(elemType, list.Count) : (IList)Activator.CreateInstance(list.GetType(), list.Count);
                for (int i = 0; i < list.Count; i++)
                    if (type.IsArray) ((Array)newList).SetValue(DeepCloneValue(list[i]), i);
                    else newList[i] = DeepCloneValue(list[i]);
                return newList;
            }
            return value;
        }

        private static IDictionary<string, object> CloneDictionary(IDictionary<string, object> source)
        {
            var copy = new ExpandoObject();
            var copyDict = (IDictionary<string, object>)copy;
            foreach (var kv in source)
                copyDict[kv.Key] = DeepCloneValue(kv.Value);
            return copyDict;
        }
    }

    // ========== 占位符接口(标记接口) ==========
    public interface IPlaceholder { }

    // ========== 占位符类型枚举 ==========
    public enum PlaceholderKind
    {
        Input,          // 输入占位符,仅包含 Key
        Prev,           // 前一个节点,可包含 Index 或 Key
        NodeReference,  // 指定节点ID,可包含 Index 或 Key
        NodeIndex       // 指定节点索引,可包含 Index 或 Key
    }

    // ========== 通用占位符类 ==========
    public class Placeholder : IPlaceholder
    {
        public PlaceholderKind Kind { get; }
        public string Key { get; }
        public int? Index { get; }
        public string NodeId { get; }
        public int? NodeIndex { get; }

        private Placeholder(PlaceholderKind kind, string key = null, int? index = null, string nodeId = null, int? nodeIndex = null)
        {
            Kind = kind;
            Key = key;
            Index = index;
            NodeId = nodeId;
            NodeIndex = nodeIndex;
        }

        // 静态工厂方法
        public static Placeholder Input(string key) => new Placeholder(PlaceholderKind.Input, key: key);
        public static Placeholder Prev() => new Placeholder(PlaceholderKind.Prev);
        public static Placeholder PrevByIndex(int index) => new Placeholder(PlaceholderKind.Prev, index: index);
        public static Placeholder PrevByKey(string key) => new Placeholder(PlaceholderKind.Prev, key: key);
        public static Placeholder From(string nodeId) => new Placeholder(PlaceholderKind.NodeReference, nodeId: nodeId);
        public static Placeholder From(string nodeId, int index) => new Placeholder(PlaceholderKind.NodeReference, nodeId: nodeId, index: index);
        public static Placeholder From(string nodeId, string key) => new Placeholder(PlaceholderKind.NodeReference, nodeId: nodeId, key: key);
        public static Placeholder Node(int nodeIndex) => new Placeholder(PlaceholderKind.NodeIndex, nodeIndex: nodeIndex);

        // 链式索引器:通过索引进一步限定
        public Placeholder this[int index]
        {
            get
            {
                if (Kind == PlaceholderKind.Prev || Kind == PlaceholderKind.NodeReference || Kind == PlaceholderKind.NodeIndex)
                {
                    if (Key != null) throw new InvalidOperationException("Cannot use both index and key.");
                    if (Index.HasValue) throw new InvalidOperationException("Index already set.");
                    return new Placeholder(Kind, key: null, index: index, nodeId: NodeId, nodeIndex: NodeIndex);
                }
                throw new InvalidOperationException($"Indexer not supported for {Kind}.");
            }
        }

        // 链式索引器:通过键进一步限定
        public Placeholder this[string key]
        {
            get
            {
                if (Kind == PlaceholderKind.Prev || Kind == PlaceholderKind.NodeReference || Kind == PlaceholderKind.NodeIndex)
                {
                    if (Index.HasValue) throw new InvalidOperationException("Cannot use both index and key.");
                    if (Key != null) throw new InvalidOperationException("Key already set.");
                    return new Placeholder(Kind, key: key, index: null, nodeId: NodeId, nodeIndex: NodeIndex);
                }
                throw new InvalidOperationException($"Key indexer not supported for {Kind}.");
            }
        }
    }

    // ========== 静态入口类:生成各种占位符 ==========
    public static class Param
    {
        public static Placeholder Prev => Placeholder.Prev();
        public static Placeholder PrevByIndex(int index) => Placeholder.PrevByIndex(index);
        public static Placeholder PrevByKey(string key) => Placeholder.PrevByKey(key);
        public static Placeholder From(string nodeId) => Placeholder.From(nodeId);
        public static Placeholder From(string nodeId, int index) => Placeholder.From(nodeId, index);
        public static Placeholder From(string nodeId, string key) => Placeholder.From(nodeId, key);
        public static Placeholder Node(int nodeIndex) => Placeholder.Node(nodeIndex);
        public static Placeholder Input(string key) => Placeholder.Input(key);
    }

    // ========== 节点处理器接口 ==========
    public interface INodeHandler
    {
        string GetNodeId(object node);
        IEnumerable<object> GetParameterKeys(object node);
        IEnumerable<string> GetResultKeys(object node);
        void SetParameterKeys(object node, IEnumerable<object> keys);
        string GetDisplayName(object node);
        object CloneNode(object node);
    }

    // ========== 默认处理器 ==========
    public class DefaultNodeHandler : INodeHandler
    {
        public string GetNodeId(object node) => NodeDictionaryHelper.GetNodeId(node);
        public IEnumerable<object> GetParameterKeys(object node) => NodeDictionaryHelper.GetParameterKeys(node);
        public IEnumerable<string> GetResultKeys(object node) => NodeDictionaryHelper.GetResultKeys(node);
        public void SetParameterKeys(object node, IEnumerable<object> keys) => NodeDictionaryHelper.SetParameterKeys(node, keys);
        public string GetDisplayName(object node) => NodeDictionaryHelper.GetDisplayName(node);
        public object CloneNode(object node) => NodeDictionaryHelper.CloneNode(node);
    }

    // ========== 通用工作流构建器 ==========
    public class DynamicWorkflowBuilder
    {
        internal readonly List<object> _nodes = new List<object>();
        private readonly HashSet<string> _nodeIds = new HashSet<string>();
        private readonly HashSet<string> _inputKeys = new HashSet<string>();
        private readonly Dictionary<string, List<string>> _nodeIdToResultKeys = new Dictionary<string, List<string>>();
        private bool _validateOnBuild = true;
        private readonly INodeHandler _handler;

        public DynamicWorkflowBuilder(INodeHandler handler = null) => _handler = handler ?? new DefaultNodeHandler();

        public DynamicWorkflowBuilder WithValidation(bool validate) { _validateOnBuild = validate; return this; }
        public DynamicChainBuilder CreateChain(string chainName = null) => new DynamicChainBuilder(this, _handler, chainName);

        internal void AddNode(object node, IEnumerable<string> resultKeys, string nodeId)
        {
            if (!string.IsNullOrEmpty(nodeId))
            {
                if (!_nodeIds.Add(nodeId))
                    throw new InvalidOperationException($"Duplicate node ID '{nodeId}'.");
                _nodeIdToResultKeys[nodeId] = resultKeys.ToList();
            }
            _nodes.Add(node);
        }

        internal void AddInputKey(string key)
        {
            if (string.IsNullOrEmpty(key)) throw new ArgumentException("Input key cannot be null or empty.");
            _inputKeys.Add(key);
        }

        internal bool TryGetNodeOutputs(string nodeId, out List<string> outputs) => _nodeIdToResultKeys.TryGetValue(nodeId, out outputs);
        internal IReadOnlyList<object> GetNodes() => _nodes.AsReadOnly();

        public List<object> Build(bool stripNodeIds = true)
        {
            if (_validateOnBuild) ValidateWorkflow();
            if (stripNodeIds)
                foreach (var node in _nodes.OfType<IDictionary<string, object>>())
                    node.Remove(NodeId);
            return _nodes;
        }

        private void ValidateWorkflow()
        {
            var knownKeys = new HashSet<string>(_inputKeys);
            int i = 0;
            foreach (var node in _nodes)
            {
                string display = _handler.GetDisplayName(node) ?? $"Node_{i}";
                var paramObjects = _handler.GetParameterKeys(node)?.ToList() ?? new List<object>();
                var paramKeys = new List<string>();
                foreach (var p in paramObjects)
                {
                    if (p == null) throw new InvalidOperationException($"Parameter key cannot be null in node '{display}'.");
                    if (!(p is string s)) throw new InvalidOperationException($"Parameter key must be a string, got {p.GetType()} in node '{display}'.");
                    if (string.IsNullOrEmpty(s)) throw new InvalidOperationException($"Parameter key cannot be empty in node '{display}'.");
                    paramKeys.Add(s);
                }

                string unknown = paramKeys.FirstOrDefault(k => !knownKeys.Contains(k));
                if (unknown != null)
                    throw new InvalidOperationException($"Parameter key '{unknown}' in node '{display}' is not defined.");

                var resultKeys = _handler.GetResultKeys(node)?.ToList() ?? new List<string>();
                foreach (var key in resultKeys)
                {
                    if (string.IsNullOrEmpty(key)) throw new InvalidOperationException($"Output key cannot be null or empty in node '{display}'.");
                    if (knownKeys.Contains(key)) throw new InvalidOperationException($"Output key '{key}' in node '{display}' is already defined.");
                }

                foreach (var key in resultKeys) knownKeys.Add(key);
                i++;
            }
        }

        public string BuildJson(bool format = true) =>
            System.Text.Json.JsonSerializer.Serialize(Build(true), new JsonSerializerOptions { WriteIndented = format, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
    }

    // ========== 链式构建器 ==========
    public class DynamicChainBuilder
    {
        private readonly DynamicWorkflowBuilder _parent;
        private readonly INodeHandler _handler;
        private readonly string _chainName;
        private List<string> _previousOutputKeys = new List<string>();
        private readonly HashSet<string> _chainNodeIds = new HashSet<string>();

        public DynamicChainBuilder(DynamicWorkflowBuilder parent, INodeHandler handler, string chainName)
        {
            _parent = parent ?? throw new ArgumentNullException(nameof(parent));
            _handler = handler ?? throw new ArgumentNullException(nameof(handler));
            _chainName = chainName;
        }

        public DynamicChainBuilder Then(object node)
        {
            var nodeCopy = _handler.CloneNode(node);
            var rawParams = _handler.GetParameterKeys(nodeCopy)?.ToList() ?? new List<object>();
            var resolvedParams = new List<object>();
            for (int i = 0; i < rawParams.Count; i++)
            {
                try
                {
                    var resolved = ResolveParameter(rawParams[i], nodeCopy);
                    if (resolved == null) throw new InvalidOperationException($"Resolved parameter at index {i} is null.");
                    resolvedParams.Add(resolved);
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException(FormatMessage($"Error resolving parameter at index {i} in node '{GetDisplayName(nodeCopy)}': {ex.Message}"), ex);
                }
            }

            ValidateKeysNoNullOrEmpty(resolvedParams.Cast<string>(), "Parameter", nodeCopy);
            _handler.SetParameterKeys(nodeCopy, resolvedParams);

            var resultKeys = _handler.GetResultKeys(nodeCopy)?.ToList() ?? throw new ArgumentException(FormatMessage("Node must define result keys."));
            ValidateKeysNoNullOrEmpty(resultKeys, "Result", nodeCopy);
            if (resultKeys.Count != resultKeys.Distinct().Count())
                throw new ArgumentException(FormatMessage($"Output keys cannot contain duplicates. Node: {GetDisplayName(nodeCopy)}"));

            string nodeId = _handler.GetNodeId(nodeCopy);
            if (!string.IsNullOrEmpty(nodeId) && !_chainNodeIds.Add(nodeId))
                throw new InvalidOperationException(FormatMessage($"Duplicate node ID '{nodeId}' in the same chain."));

            _parent.AddNode(nodeCopy, resultKeys, nodeId);
            _previousOutputKeys = resultKeys;
            return this;
        }

        public DynamicWorkflowBuilder EndChain() => _parent;

        private string FormatMessage(string msg) => string.IsNullOrEmpty(_chainName) ? msg : $"[Chain '{_chainName}'] {msg}";
        private string GetDisplayName(object node) => _handler.GetDisplayName(node) ?? "unknown";

        private void ValidateKeysNoNullOrEmpty(IEnumerable<string> keys, string keyType, object node)
        {
            int idx = 0;
            foreach (var key in keys)
                if (string.IsNullOrEmpty(key))
                    throw new ArgumentException(FormatMessage($"{keyType} key at position {idx} cannot be null or empty. Node: {GetDisplayName(node)}"));
                else idx++;
        }

        // 解析参数(支持字符串和通用占位符)
        private string ResolveParameter(object param, object currentNode)
        {
            switch (param)
            {
                case string s:
                    return s;
                case Placeholder p:
                    switch (p.Kind)
                    {
                        case PlaceholderKind.Input:
                            if (string.IsNullOrEmpty(p.Key))
                                throw new ArgumentException(FormatMessage("Input key cannot be empty."));
                            _parent.AddInputKey(p.Key);
                            return p.Key;

                        case PlaceholderKind.Prev:
                            return ResolvePrev(p, currentNode);

                        case PlaceholderKind.NodeReference:
                            return ResolveNodeRef(p, currentNode);

                        case PlaceholderKind.NodeIndex:
                            return ResolveNodeIndex(p, currentNode);

                        default:
                            throw new ArgumentException(FormatMessage($"Unsupported placeholder kind: {p.Kind}"));
                    }
                default:
                    throw new ArgumentException(FormatMessage($"Parameter must be string or Placeholder, got {param?.GetType()}"));
            }
        }

        private string ResolvePrev(Placeholder p, object currentNode)
        {
            if (p.Index.HasValue && !string.IsNullOrEmpty(p.Key))
                throw new InvalidOperationException(FormatMessage("Placeholder cannot have both Index and Key."));

            if (_previousOutputKeys == null || _previousOutputKeys.Count == 0)
                throw new InvalidOperationException(FormatMessage($"Cannot reference previous node because it has no outputs. Node: {GetDisplayName(currentNode)}"));

            if (!p.Index.HasValue && string.IsNullOrEmpty(p.Key))
            {
                if (_previousOutputKeys.Count == 1)
                    return _previousOutputKeys.First();
                throw new InvalidOperationException(FormatMessage($"Ambiguous reference to previous node: multiple outputs ({string.Join(", ", _previousOutputKeys)}). Use .Index or .Key."));
            }

            if (p.Index.HasValue)
            {
                int idx = p.Index.Value;
                if (idx < 0 || idx >= _previousOutputKeys.Count)
                    throw new ArgumentOutOfRangeException(FormatMessage($"Index {idx} out of range. Available: {string.Join(", ", _previousOutputKeys)}"));
                return _previousOutputKeys[idx];
            }

            if (_previousOutputKeys.Contains(p.Key))
                return p.Key;
            throw new ArgumentException(FormatMessage($"Key '{p.Key}' not found in previous node outputs. Available: {string.Join(", ", _previousOutputKeys)}"));
        }

        private string ResolveNodeRef(Placeholder p, object currentNode)
        {
            if (string.IsNullOrEmpty(p.NodeId))
                throw new ArgumentException(FormatMessage("NodeReference placeholder must have NodeId."));
            if (p.Index.HasValue && !string.IsNullOrEmpty(p.Key))
                throw new InvalidOperationException(FormatMessage("Placeholder cannot have both Index and Key."));

            if (!_parent.TryGetNodeOutputs(p.NodeId, out var outputs))
                throw new ArgumentException(FormatMessage($"Node ID '{p.NodeId}' not found."));

            if (outputs == null || outputs.Count == 0)
                throw new InvalidOperationException(FormatMessage($"Node '{p.NodeId}' has no outputs."));

            if (!p.Index.HasValue && string.IsNullOrEmpty(p.Key))
            {
                if (outputs.Count == 1)
                    return outputs.First();
                throw new InvalidOperationException(FormatMessage($"Ambiguous reference to node '{p.NodeId}': multiple outputs ({string.Join(", ", outputs)}). Use .Index or .Key."));
            }

            if (p.Index.HasValue)
            {
                int idx = p.Index.Value;
                if (idx < 0 || idx >= outputs.Count)
                    throw new ArgumentOutOfRangeException(FormatMessage($"Index {idx} out of range for node '{p.NodeId}'. Available: {string.Join(", ", outputs)}"));
                return outputs[idx];
            }

            if (outputs.Contains(p.Key))
                return p.Key;
            throw new ArgumentException(FormatMessage($"Key '{p.Key}' not found in node '{p.NodeId}' outputs. Available: {string.Join(", ", outputs)}"));
        }

        private string ResolveNodeIndex(Placeholder p, object currentNode)
        {
            if (!p.NodeIndex.HasValue)
                throw new ArgumentException(FormatMessage("NodeIndex placeholder must have NodeIndex."));
            if (p.Index.HasValue && !string.IsNullOrEmpty(p.Key))
                throw new InvalidOperationException(FormatMessage("Placeholder cannot have both Index and Key."));

            var allNodes = _parent.GetNodes();
            int total = allNodes.Count;
            int nodeIdx = p.NodeIndex.Value < 0 ? total + p.NodeIndex.Value : p.NodeIndex.Value;
            if (nodeIdx < 0 || nodeIdx >= total)
                throw new ArgumentOutOfRangeException(FormatMessage($"Node index {p.NodeIndex} out of range. Total nodes: {total}"));

            var outputs = _handler.GetResultKeys(allNodes[nodeIdx])?.ToList()
                ?? throw new InvalidOperationException(FormatMessage($"Node at index {p.NodeIndex} has no outputs."));

            if (!p.Index.HasValue && string.IsNullOrEmpty(p.Key))
            {
                if (outputs.Count == 1)
                    return outputs.First();
                throw new InvalidOperationException(FormatMessage($"Ambiguous reference to node at index {p.NodeIndex}: multiple outputs ({string.Join(", ", outputs)}). Use .Index or .Key."));
            }

            if (p.Index.HasValue)
            {
                int idx = p.Index.Value;
                if (idx < 0 || idx >= outputs.Count)
                    throw new ArgumentOutOfRangeException(FormatMessage($"Index {idx} out of range for node at index {p.NodeIndex}. Available: {string.Join(", ", outputs)}"));
                return outputs[idx];
            }

            if (outputs.Contains(p.Key))
                return p.Key;
            throw new ArgumentException(FormatMessage($"Key '{p.Key}' not found in node at index {p.NodeIndex} outputs. Available: {string.Join(", ", outputs)}"));
        }

        private IEnumerable<string> GetAllNodeIds() =>
            _parent.GetNodes().Select(n => _handler.GetNodeId(n)).Where(id => !string.IsNullOrEmpty(id));
    }

    // ========== 通用占位符解析器(独立工具) ==========
    public static class PlaceholderResolver
    {
        public static string Resolve(
            IPlaceholder placeholder,
            IReadOnlyList<string> previousOutputs,
            IReadOnlyDictionary<string, IReadOnlyList<string>> nodeIdToOutputs,
            IReadOnlyList<IReadOnlyList<string>> allNodeOutputs)
        {
            if (!(placeholder is Placeholder p))
                throw new NotSupportedException($"Unsupported placeholder type: {placeholder.GetType()}");

            switch (p.Kind)
            {
                case PlaceholderKind.Input:
                    return p.Key ?? throw new InvalidOperationException("Input placeholder must have a key.");

                case PlaceholderKind.Prev:
                    return ResolvePrev(p, previousOutputs);

                case PlaceholderKind.NodeReference:
                    return ResolveNodeRef(p, nodeIdToOutputs);

                case PlaceholderKind.NodeIndex:
                    return ResolveNodeIndex(p, allNodeOutputs);

                default:
                    throw new NotSupportedException($"Unsupported placeholder kind: {p.Kind}");
            }
        }

        private static string ResolvePrev(Placeholder p, IReadOnlyList<string> previousOutputs)
        {
            if (previousOutputs == null || previousOutputs.Count == 0)
                throw new InvalidOperationException("Cannot reference previous node because it has no outputs.");

            if (!p.Index.HasValue && string.IsNullOrEmpty(p.Key))
            {
                if (previousOutputs.Count == 1)
                    return previousOutputs[0];
                throw new InvalidOperationException($"Ambiguous reference to previous node: multiple outputs ({string.Join(", ", previousOutputs)}). Use .Index or .Key.");
            }

            if (p.Index.HasValue && !string.IsNullOrEmpty(p.Key))
                throw new InvalidOperationException("Placeholder cannot have both Index and Key.");

            if (p.Index.HasValue)
            {
                int idx = p.Index.Value;
                if (idx < 0 || idx >= previousOutputs.Count)
                    throw new ArgumentOutOfRangeException(nameof(p.Index), $"Index {idx} out of range. Available: {string.Join(", ", previousOutputs)}");
                return previousOutputs[idx];
            }

            if (previousOutputs.Contains(p.Key))
                return p.Key;
            throw new KeyNotFoundException($"Key '{p.Key}' not found in previous node outputs. Available: {string.Join(", ", previousOutputs)}");
        }

        private static string ResolveNodeRef(Placeholder p, IReadOnlyDictionary<string, IReadOnlyList<string>> nodeIdToOutputs)
        {
            if (string.IsNullOrEmpty(p.NodeId))
                throw new ArgumentException("NodeReference placeholder must have NodeId.");
            if (!nodeIdToOutputs.TryGetValue(p.NodeId, out var outputs))
                throw new KeyNotFoundException($"Node ID '{p.NodeId}' not found.");

            if (outputs == null || outputs.Count == 0)
                throw new InvalidOperationException($"Node '{p.NodeId}' has no outputs.");

            if (!p.Index.HasValue && string.IsNullOrEmpty(p.Key))
            {
                if (outputs.Count == 1)
                    return outputs[0];
                throw new InvalidOperationException($"Ambiguous reference to node '{p.NodeId}': multiple outputs ({string.Join(", ", outputs)}). Use .Index or .Key.");
            }

            if (p.Index.HasValue && !string.IsNullOrEmpty(p.Key))
                throw new InvalidOperationException("Placeholder cannot have both Index and Key.");

            if (p.Index.HasValue)
            {
                int idx = p.Index.Value;
                if (idx < 0 || idx >= outputs.Count)
                    throw new ArgumentOutOfRangeException(nameof(p.Index), $"Index {idx} out of range for node '{p.NodeId}'. Available: {string.Join(", ", outputs)}");
                return outputs[idx];
            }

            if (outputs.Contains(p.Key))
                return p.Key;
            throw new KeyNotFoundException($"Key '{p.Key}' not found in node '{p.NodeId}' outputs. Available: {string.Join(", ", outputs)}");
        }

        private static string ResolveNodeIndex(Placeholder p, IReadOnlyList<IReadOnlyList<string>> allNodeOutputs)
        {
            if (!p.NodeIndex.HasValue)
                throw new ArgumentException("NodeIndex placeholder must have NodeIndex.");
            int total = allNodeOutputs.Count;
            int nodeIdx = p.NodeIndex.Value < 0 ? total + p.NodeIndex.Value : p.NodeIndex.Value;
            if (nodeIdx < 0 || nodeIdx >= total)
                throw new ArgumentOutOfRangeException(nameof(p.NodeIndex), $"Node index {p.NodeIndex} out of range. Total nodes: {total}");

            var outputs = allNodeOutputs[nodeIdx];
            if (outputs == null || outputs.Count == 0)
                throw new InvalidOperationException($"Node at index {p.NodeIndex} has no outputs.");

            if (!p.Index.HasValue && string.IsNullOrEmpty(p.Key))
            {
                if (outputs.Count == 1)
                    return outputs[0];
                throw new InvalidOperationException($"Ambiguous reference to node at index {p.NodeIndex}: multiple outputs ({string.Join(", ", outputs)}). Use .Index or .Key.");
            }

            if (p.Index.HasValue && !string.IsNullOrEmpty(p.Key))
                throw new InvalidOperationException("Placeholder cannot have both Index and Key.");

            if (p.Index.HasValue)
            {
                int idx = p.Index.Value;
                if (idx < 0 || idx >= outputs.Count)
                    throw new ArgumentOutOfRangeException(nameof(p.Index), $"Index {idx} out of range for node at index {p.NodeIndex}. Available: {string.Join(", ", outputs)}");
                return outputs[idx];
            }

            if (outputs.Contains(p.Key))
                return p.Key;
            throw new KeyNotFoundException($"Key '{p.Key}' not found in node at index {p.NodeIndex} outputs. Available: {string.Join(", ", outputs)}");
        }

        // 对节点列表执行占位符解析(直接修改传入的节点)
        public static void ResolvePlaceholders(List<object> nodes)
        {
            // 构建节点ID到输出列表的映射,以及所有节点输出列表(按顺序)
            var nodeIdToOutputs = new Dictionary<string, IReadOnlyList<string>>();
            var allNodeOutputs = new List<IReadOnlyList<string>>();

            foreach (var nodeObj in nodes)
            {
                var node = (IDictionary<string, object>)nodeObj;
                string nodeId = node.TryGetValue("NodeId", out var idObj) ? idObj?.ToString() : null;
                var config = (IDictionary<string, object>)node["FuncPipelineConfig"];
                var resultKeys = config.TryGetValue("ResultKeys", out var rk) ? rk as IList<string> : null;
                if (resultKeys == null) resultKeys = new List<string>();
                var outputs = resultKeys.Cast<string>().ToList().AsReadOnly();
                allNodeOutputs.Add(outputs);
                if (!string.IsNullOrEmpty(nodeId))
                    nodeIdToOutputs[nodeId] = outputs;
            }

            // 解析每个节点的参数占位符
            IReadOnlyList<string> previousOutputs = new List<string>().AsReadOnly();
            for (int i = 0; i < nodes.Count; i++)
            {
                var nodeObj = nodes[i];
                var node = (IDictionary<string, object>)nodeObj;
                var config = (IDictionary<string, object>)node["FuncPipelineConfig"];
                var paramKeys = config.TryGetValue("ParameterKeys", out var pk) ? pk as IList<object> : null;
                if (paramKeys != null)
                {
                    var resolvedParams = new List<object>();
                    foreach (var param in paramKeys)
                    {
                        if (param is string str)
                        {
                            resolvedParams.Add(str);
                        }
                        else if (param is IPlaceholder placeholder)
                        {
                            string resolved = Resolve(
                                placeholder,
                                previousOutputs,
                                nodeIdToOutputs,
                                allNodeOutputs);
                            resolvedParams.Add(resolved);
                        }
                        else
                        {
                            throw new InvalidOperationException($"Unexpected parameter type: {param?.GetType()}");
                        }
                    }
                    config["ParameterKeys"] = resolvedParams;
                }
                previousOutputs = allNodeOutputs[i];
            }

            // 移除所有节点的 NodeId(如果需要)
            foreach (var nodeObj in nodes)
            {
                var node = (IDictionary<string, object>)nodeObj;
                node.Remove("NodeId");
            }
        }
    }
}
相关推荐
测绘工程师1 小时前
【排序算法】冒泡排序
数据结构·算法·排序算法
载数而行5202 小时前
算法系列1之最小生成树
c语言·数据结构·c++·算法·贪心算法
重生之后端学习2 小时前
208. 实现 Trie (前缀树)
java·开发语言·数据结构·算法·职场和发展·深度优先
识君啊2 小时前
Java 栈 - 附LeetCode 经典题解
java·数据结构·leetcode·deque··stack·lifo
26岁的学习随笔2 小时前
【Claude Code】我给 Claude Code 做了个桌面启动器 —— 内置道家呼吸引导的悬浮路径工具
c#·开源项目·winforms·桌面工具·claude code
郝学胜-神的一滴2 小时前
Effective Modern C++ 条款39:一次事件通信的优雅解决方案
开发语言·数据结构·c++·算法·多线程·并发
专注VB编程开发20年2 小时前
c#,vb.net Redis vs ODBC/ADO 查库的速度差距,写入json数据和字典数据
redis·c#·.net
仰泳的熊猫2 小时前
题目1514:蓝桥杯算法提高VIP-夺宝奇兵
数据结构·c++·算法·蓝桥杯
_OP_CHEN2 小时前
【算法提高篇】(五)线段树 + 分治:解锁区间问题的终极思路,从最大子段和到复杂序列操作
数据结构·算法·蓝桥杯·线段树·c/c++·分治·acm/icpc