package com.agent.management.engine; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import java.util.*; /** * 节点输入变量解析器。 * *

对节点 data.inputs[] 中的每个字段: *

    *
  1. 优先使用显式 mapping(sourceNodeId/sourceField/sourcePath)
  2. *
  3. 若未配置或来源不可达,进入四级隐式关联 fallback: * 精确同名 → 忽略分隔符/大小写同名 → JSON 嵌套路径精确匹配 → JSON 嵌套路径模糊匹配
  4. *
  5. 按目标 type 做类型转换
  6. *
*/ @Slf4j public class NodeInputResolver { private static final ObjectMapper MAPPER = new ObjectMapper(); /** * 解析后的输入结果。 */ public static class ResolvedInputs { private final Map values = new LinkedHashMap<>(); private final List missing = new ArrayList<>(); private final List warnings = new ArrayList<>(); public Map getValues() { return values; } public List getMissing() { return missing; } public List getWarnings() { return warnings; } public boolean hasMissing() { return !missing.isEmpty(); } } /** * 解析节点输入。 * * @param nodeData 节点 data(JSON) * @param workspace 当前节点工作区 * @return 解析结果(值、缺失项、警告) */ public static ResolvedInputs resolveInputs(JsonNode nodeData, NodeWorkspace workspace) { ResolvedInputs result = new ResolvedInputs(); JsonNode inputsNode = nodeData.path("inputs"); if (!inputsNode.isArray() || inputsNode.isEmpty()) { return result; } for (JsonNode field : inputsNode) { String name = field.path("name").asText(""); if (name.isEmpty()) { continue; } String type = field.path("type").asText("string"); boolean required = field.path("required").asBoolean(false); Object value = resolveInputValue(name, type, field.path("mapping"), workspace, result); if (value == null) { if (required) { result.missing.add(name); } } else { result.values.put(name, value); } } return result; } private static Object resolveInputValue(String name, String type, JsonNode mappingNode, NodeWorkspace workspace, ResolvedInputs result) { // 1. 显式 mapping if (mappingNode != null && !mappingNode.isMissingNode()) { String sourceNodeId = mappingNode.path("sourceNodeId").asText(""); String sourceField = mappingNode.path("sourceField").asText(""); String sourcePath = mappingNode.path("sourcePath").asText(null); if (!sourceNodeId.isEmpty()) { if (workspace.getScopedOutputs().containsKey(sourceNodeId)) { if (sourceField.isEmpty()) { sourceField = name; } Object value = workspace.getScopedOutput(sourceNodeId, sourceField); if (value != null && sourcePath != null && !sourcePath.isBlank()) { value = JsonPathExtractor.extract(value, sourcePath); } if (value != null) { return VariableConverter.convert(value, type); } result.warnings.add(name + " 显式映射到 " + sourceNodeId + "." + sourceField + (sourcePath != null ? "/" + sourcePath : "") + " 但值为空"); } else { result.warnings.add(name + " 显式映射来源 " + sourceNodeId + " 不可达,已忽略"); } } } // 2. 直接变量表(覆盖初始输入与向后兼容的扁平变量) Object direct = workspace.getVariable(name); if (direct != null) { return VariableConverter.convert(direct, type); } // 3. 隐式关联 fallback:按拓扑逆序(最近前驱优先) List ordered = new ArrayList<>(workspace.getScopedOutputs().keySet()); Collections.reverse(ordered); // 3.1 精确同名 for (String nodeId : ordered) { Object value = workspace.getScopedOutput(nodeId, name); if (value != null) { return VariableConverter.convert(value, type); } } // 3.2 忽略 [-_] 与大小写同名 String normName = normalizeForMatch(name); for (String nodeId : ordered) { Map scoped = workspace.getScopedOutputs().get(nodeId); if (scoped == null) continue; for (Map.Entry e : scoped.entrySet()) { if (normalizeForMatch(e.getKey()).equals(normName)) { return VariableConverter.convert(e.getValue(), type); } } } // 3.3 JSON 嵌套路径精确同名:以整个节点输出 map 为根 for (String nodeId : ordered) { Map scoped = workspace.getScopedOutputs().get(nodeId); if (scoped == null) continue; Object extracted = JsonPathExtractor.extract(scoped, name); if (extracted != null) { return VariableConverter.convert(extracted, type); } } // 3.4 JSON 嵌套路径模糊同名:以整个节点输出 map 为根,忽略 [-_] 与大小写 for (String nodeId : ordered) { Map scoped = workspace.getScopedOutputs().get(nodeId); if (scoped == null) continue; Object extracted = extractByNormalizedPath(scoped, name); if (extracted != null) { return VariableConverter.convert(extracted, type); } } return null; } /** * 忽略 [-_] 与大小写的归一化,用于模糊匹配。 */ static String normalizeForMatch(String s) { if (s == null) return ""; return s.replaceAll("[-_]", "").toLowerCase(Locale.ROOT); } /** * 按归一化路径从对象中取值:路径每一节都忽略 [-_] 与大小写匹配键名。 */ private static Object extractByNormalizedPath(Object root, String path) { if (path == null || path.isBlank()) { return root; } Object current = root; String[] segments = path.split("\\."); for (String segment : segments) { if (segment.isEmpty()) { continue; } current = stepFuzzy(current, segment); if (current == null) { return null; } } return current; } private static Object stepFuzzy(Object current, String segment) { String normSeg = normalizeForMatch(segment); if (current instanceof Map map) { // 先精确匹配,再模糊匹配 if (map.containsKey(segment)) { return map.get(segment); } for (Map.Entry e : map.entrySet()) { Object key = e.getKey(); if (key instanceof String k && normalizeForMatch(k).equals(normSeg)) { return e.getValue(); } } return null; } if (current instanceof JsonNode node) { if (node.isArray()) { Integer idx = parseIndex(segment); return idx != null && idx >= 0 && idx < node.size() ? jsonNodeToValue(node.get(idx)) : null; } JsonNode child = node.get(segment); if (child != null) { return jsonNodeToValue(child); } for (Iterator it = node.fieldNames(); it.hasNext(); ) { String field = it.next(); if (normalizeForMatch(field).equals(normSeg)) { return jsonNodeToValue(node.get(field)); } } return null; } if (current instanceof Collection collection) { Integer idx = parseIndex(segment); if (idx == null || idx < 0) { return null; } int i = 0; for (Object item : collection) { if (i == idx) { return item; } i++; } return null; } // POJO:先转 JsonNode 再递归 try { JsonNode node = MAPPER.valueToTree(current); return stepFuzzy(node, segment); } catch (Exception e) { return null; } } private static Integer parseIndex(String segment) { try { return Integer.parseInt(segment); } catch (NumberFormatException e) { return null; } } private static Object jsonNodeToValue(JsonNode node) { if (node == null || node.isNull()) { return null; } if (node.isTextual()) { return node.asText(); } if (node.isNumber()) { return node.numberValue(); } if (node.isBoolean()) { return node.booleanValue(); } try { return MAPPER.treeToValue(node, Object.class); } catch (Exception e) { return node.toString(); } } }