mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-07 04:58:03 +02:00
Add files via upload
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var allowedWorkflowNodeTypes = map[string]bool{
|
||||
"start": true,
|
||||
"tool": true,
|
||||
"agent": true,
|
||||
"condition": true,
|
||||
"hitl": true,
|
||||
"output": true,
|
||||
"end": true,
|
||||
}
|
||||
|
||||
func validateGraphDefinition(g *graphDef, idx *graphIndex) error {
|
||||
if g == nil || idx == nil {
|
||||
return fmt.Errorf("工作流图为空")
|
||||
}
|
||||
if err := validateNodeIDsAndTypes(g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateEdges(g, idx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateNodeTopology(idx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateNodeConfigs(idx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDAG(idx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateReachability(idx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNodeIDsAndTypes(g *graphDef) error {
|
||||
seen := make(map[string]bool, len(g.Nodes))
|
||||
for _, node := range g.Nodes {
|
||||
id := strings.TrimSpace(node.ID)
|
||||
if id == "" {
|
||||
return fmt.Errorf("工作流存在空节点 ID")
|
||||
}
|
||||
if seen[id] {
|
||||
return fmt.Errorf("工作流存在重复节点 ID: %s", id)
|
||||
}
|
||||
seen[id] = true
|
||||
nodeType := strings.ToLower(strings.TrimSpace(node.Type))
|
||||
if nodeType == "" {
|
||||
return fmt.Errorf("节点「%s」缺少节点类型", id)
|
||||
}
|
||||
if !allowedWorkflowNodeTypes[nodeType] {
|
||||
return fmt.Errorf("节点「%s」使用了未知节点类型: %s", id, node.Type)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEdges(g *graphDef, idx *graphIndex) error {
|
||||
seen := make(map[string]bool, len(g.Edges))
|
||||
for _, edge := range g.Edges {
|
||||
if id := strings.TrimSpace(edge.ID); id != "" {
|
||||
if seen[id] {
|
||||
return fmt.Errorf("工作流存在重复连线 ID: %s", id)
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
source := strings.TrimSpace(edge.Source)
|
||||
target := strings.TrimSpace(edge.Target)
|
||||
if source == "" || target == "" {
|
||||
return fmt.Errorf("工作流存在源或目标为空的连线")
|
||||
}
|
||||
if source == target {
|
||||
return fmt.Errorf("连线「%s」不能自环", firstNonEmpty(edge.ID, source))
|
||||
}
|
||||
if _, ok := idx.nodes[source]; !ok {
|
||||
return fmt.Errorf("连线「%s」引用了不存在的源节点: %s", firstNonEmpty(edge.ID, source), source)
|
||||
}
|
||||
if _, ok := idx.nodes[target]; !ok {
|
||||
return fmt.Errorf("连线「%s」引用了不存在的目标节点: %s", firstNonEmpty(edge.ID, target), target)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNodeTopology(idx *graphIndex) error {
|
||||
starts := explicitStartNodeIDs(idx)
|
||||
if len(starts) == 0 {
|
||||
return fmt.Errorf("工作流至少需要一个开始节点")
|
||||
}
|
||||
outputs := outputNodeIDs(idx)
|
||||
if len(outputs) == 0 {
|
||||
return fmt.Errorf("工作流至少需要一个输出节点")
|
||||
}
|
||||
for id, node := range idx.nodes {
|
||||
inDegree := len(idx.incoming[id])
|
||||
outDegree := len(idx.outgoing[id])
|
||||
nodeType := strings.ToLower(strings.TrimSpace(node.Type))
|
||||
switch nodeType {
|
||||
case "start":
|
||||
if inDegree > 0 {
|
||||
return fmt.Errorf("开始节点「%s」不能有入边", firstNonEmpty(node.Label, id))
|
||||
}
|
||||
if outDegree == 0 {
|
||||
return fmt.Errorf("开始节点「%s」至少需要一条出边", firstNonEmpty(node.Label, id))
|
||||
}
|
||||
case "output", "end":
|
||||
if outDegree > 0 {
|
||||
return fmt.Errorf("%s 节点「%s」不能有出边", displayNodeType(nodeType), firstNonEmpty(node.Label, id))
|
||||
}
|
||||
if inDegree == 0 {
|
||||
return fmt.Errorf("%s 节点「%s」至少需要一条入边", displayNodeType(nodeType), firstNonEmpty(node.Label, id))
|
||||
}
|
||||
default:
|
||||
if inDegree == 0 {
|
||||
return fmt.Errorf("节点「%s」不可达:非开始节点必须有入边", firstNonEmpty(node.Label, id))
|
||||
}
|
||||
if outDegree == 0 {
|
||||
return fmt.Errorf("节点「%s」没有出边;请连接到 output/end 节点", firstNonEmpty(node.Label, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNodeConfigs(idx *graphIndex) error {
|
||||
for id, node := range idx.nodes {
|
||||
label := firstNonEmpty(node.Label, id)
|
||||
switch strings.ToLower(strings.TrimSpace(node.Type)) {
|
||||
case "tool":
|
||||
if cfgString(node.Config, "tool_name") == "" {
|
||||
return fmt.Errorf("工具节点「%s」必须选择 MCP 工具", label)
|
||||
}
|
||||
if err := validateToolConfig(node); err != nil {
|
||||
return err
|
||||
}
|
||||
case "agent":
|
||||
if cfgString(node.Config, "instruction") == "" {
|
||||
if _, ok := parseFieldBinding(node.Config, "input_binding"); !ok {
|
||||
return fmt.Errorf("Agent 节点「%s」必须填写节点指令或输入绑定", label)
|
||||
}
|
||||
}
|
||||
if cfgString(node.Config, "output_key") == "" {
|
||||
return fmt.Errorf("Agent 节点「%s」必须填写输出变量名", label)
|
||||
}
|
||||
case "condition":
|
||||
if cfgString(node.Config, "expression") == "" {
|
||||
return fmt.Errorf("条件节点「%s」必须填写表达式", label)
|
||||
}
|
||||
if err := validateConditionExpression(cfgString(node.Config, "expression")); err != nil {
|
||||
return fmt.Errorf("条件节点「%s」表达式非法: %w", label, err)
|
||||
}
|
||||
if n := len(idx.outgoing[id]); n < 1 || n > 2 {
|
||||
return fmt.Errorf("条件节点「%s」需要 1 到 2 条出边(是/否)", label)
|
||||
}
|
||||
if err := validateConditionBranchLabels(idx, id, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case "output":
|
||||
if cfgString(node.Config, "output_key") == "" {
|
||||
return fmt.Errorf("输出节点「%s」必须填写输出变量名", label)
|
||||
}
|
||||
}
|
||||
if err := validateJoinConfig(idx, id, node); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasConditionalOutgoingEdges(idx, id) {
|
||||
if err := validateConditionalOutgoingEdges(idx, id, node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateConditionalOutgoingEdges(idx *graphIndex, nodeID string, node graphNode) error {
|
||||
unconditional := 0
|
||||
for _, edge := range idx.outgoing[nodeID] {
|
||||
cond := firstNonEmpty(cfgString(edge.Config, "condition"), cfgString(edge.Config, "expression"))
|
||||
if cond != "" {
|
||||
if err := validateConditionExpression(cond); err != nil {
|
||||
return fmt.Errorf("节点「%s」的连线条件非法: %w", firstNonEmpty(node.Label, nodeID), err)
|
||||
}
|
||||
}
|
||||
if cond == "" {
|
||||
unconditional++
|
||||
}
|
||||
}
|
||||
if unconditional > 1 {
|
||||
return fmt.Errorf("节点「%s」的条件出边最多只能有一条默认分支", firstNonEmpty(node.Label, nodeID))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateToolConfig(node graphNode) error {
|
||||
rawArgs := cfgString(node.Config, "arguments")
|
||||
if rawArgs != "" {
|
||||
if _, err := resolveToolArguments(node.Config, &WorkflowLocalState{}); err != nil {
|
||||
return fmt.Errorf("工具节点「%s」参数 JSON 非法: %w", firstNonEmpty(node.Label, node.ID), err)
|
||||
}
|
||||
}
|
||||
if timeout := cfgString(node.Config, "timeout_seconds"); timeout != "" {
|
||||
if _, err := parsePositiveInt(timeout); err != nil {
|
||||
return fmt.Errorf("工具节点「%s」超时时间必须是正整数", firstNonEmpty(node.Label, node.ID))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateJoinConfig(idx *graphIndex, nodeID string, node graphNode) error {
|
||||
strategy := joinStrategy(node)
|
||||
if !allowedJoinStrategies[strategy] {
|
||||
return fmt.Errorf("节点「%s」使用了未知汇聚策略: %s", firstNonEmpty(node.Label, nodeID), strategy)
|
||||
}
|
||||
if len(idx.incoming[nodeID]) > 1 && strategy == "" {
|
||||
return fmt.Errorf("节点「%s」有多个上游时必须声明汇聚策略", firstNonEmpty(node.Label, nodeID))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateConditionBranchLabels(idx *graphIndex, nodeID string, node graphNode) error {
|
||||
seen := map[string]bool{}
|
||||
for _, edge := range idx.outgoing[nodeID] {
|
||||
hint := conditionBranchHint(edge)
|
||||
if hint == "" {
|
||||
return fmt.Errorf("条件节点「%s」的出边必须标记为是/否或 true/false", firstNonEmpty(node.Label, nodeID))
|
||||
}
|
||||
if seen[hint] {
|
||||
return fmt.Errorf("条件节点「%s」存在重复分支标签: %s", firstNonEmpty(node.Label, nodeID), hint)
|
||||
}
|
||||
seen[hint] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDAG(idx *graphIndex) error {
|
||||
color := make(map[string]int, len(idx.nodes))
|
||||
var visit func(string) error
|
||||
visit = func(id string) error {
|
||||
switch color[id] {
|
||||
case 1:
|
||||
return fmt.Errorf("工作流存在环路,Workflow 编排必须是 DAG: %s", id)
|
||||
case 2:
|
||||
return nil
|
||||
}
|
||||
color[id] = 1
|
||||
for _, edge := range idx.outgoing[id] {
|
||||
if err := visit(edge.Target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
color[id] = 2
|
||||
return nil
|
||||
}
|
||||
for id := range idx.nodes {
|
||||
if err := visit(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateReachability(idx *graphIndex) error {
|
||||
starts := explicitStartNodeIDs(idx)
|
||||
reached := make(map[string]bool, len(idx.nodes))
|
||||
queue := append([]string(nil), starts...)
|
||||
for len(queue) > 0 {
|
||||
id := queue[0]
|
||||
queue = queue[1:]
|
||||
if reached[id] {
|
||||
continue
|
||||
}
|
||||
reached[id] = true
|
||||
for _, edge := range idx.outgoing[id] {
|
||||
queue = append(queue, edge.Target)
|
||||
}
|
||||
}
|
||||
for id, node := range idx.nodes {
|
||||
if !reached[id] {
|
||||
return fmt.Errorf("节点「%s」不可达:没有从开始节点连通到该节点", firstNonEmpty(node.Label, id))
|
||||
}
|
||||
}
|
||||
|
||||
canReachTerminal := make(map[string]bool, len(idx.nodes))
|
||||
visiting := make(map[string]bool, len(idx.nodes))
|
||||
var reachesTerminal func(string) bool
|
||||
reachesTerminal = func(id string) bool {
|
||||
if canReachTerminal[id] {
|
||||
return true
|
||||
}
|
||||
if visiting[id] {
|
||||
return false
|
||||
}
|
||||
visiting[id] = true
|
||||
node := idx.nodes[id]
|
||||
nodeType := strings.ToLower(strings.TrimSpace(node.Type))
|
||||
if nodeType == "output" || nodeType == "end" {
|
||||
canReachTerminal[id] = true
|
||||
visiting[id] = false
|
||||
return true
|
||||
}
|
||||
for _, edge := range idx.outgoing[id] {
|
||||
if reachesTerminal(edge.Target) {
|
||||
canReachTerminal[id] = true
|
||||
visiting[id] = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
visiting[id] = false
|
||||
return false
|
||||
}
|
||||
for id, node := range idx.nodes {
|
||||
if !reachesTerminal(id) {
|
||||
return fmt.Errorf("节点「%s」无法到达 output/end 终点", firstNonEmpty(node.Label, id))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func explicitStartNodeIDs(idx *graphIndex) []string {
|
||||
var ids []string
|
||||
for id, node := range idx.nodes {
|
||||
if strings.EqualFold(node.Type, "start") {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
sortNodeIDsByCanvas(ids, idx.nodes)
|
||||
return ids
|
||||
}
|
||||
|
||||
func outputNodeIDs(idx *graphIndex) []string {
|
||||
var ids []string
|
||||
for id, node := range idx.nodes {
|
||||
if strings.EqualFold(node.Type, "output") {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
sortNodeIDsByCanvas(ids, idx.nodes)
|
||||
return ids
|
||||
}
|
||||
|
||||
func displayNodeType(nodeType string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(nodeType)) {
|
||||
case "output":
|
||||
return "输出"
|
||||
case "end":
|
||||
return "结束"
|
||||
default:
|
||||
return nodeType
|
||||
}
|
||||
}
|
||||
|
||||
func parsePositiveInt(s string) (int, error) {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(s))
|
||||
if err != nil || n <= 0 {
|
||||
return 0, fmt.Errorf("not positive integer")
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
Reference in New Issue
Block a user