package sop import ( "encoding/json" "fmt" "sort" "git.iwork-ai.com/xdc/iqudo-top1/internal/model" ) var allowedNodeTypes = map[string]bool{"start": true, "message": true, "question": true, "form": true, "choice": true, "condition": true, "knowledge": true, "escalate": true, "finish": true} var allowedConditionOperators = map[string]bool{"equals": true, "not_equals": true, "contains": true, "greater_than": true, "less_than": true, "exists": true, "not_exists": true, "in": true} type ValidationContext struct { Fields []model.ScenarioField PublishedKnowledgeCardIDs map[uint64]bool } func ValidateGraph(startNodeKey string, nodes []model.SOPNode, edges []model.SOPEdge) []string { var problems []string if startNodeKey == "" { problems = append(problems, "未设置开始节点") } nodeMap := make(map[string]model.SOPNode, len(nodes)) startCount := 0 finishCount := 0 for _, node := range nodes { if node.NodeKey == "" { problems = append(problems, "存在没有标识的节点") continue } if _, exists := nodeMap[node.NodeKey]; exists { problems = append(problems, fmt.Sprintf("节点标识重复:%s", node.NodeKey)) } nodeMap[node.NodeKey] = node if !allowedNodeTypes[node.Type] { problems = append(problems, fmt.Sprintf("节点 %s 类型不支持", node.Title)) } if len(node.Config) > 0 && !json.Valid(node.Config) { problems = append(problems, fmt.Sprintf("节点“%s”的配置不是有效 JSON", node.Title)) } if node.Type == "start" { startCount++ } if node.Type == "finish" || node.Type == "escalate" { finishCount++ } } if startCount != 1 { problems = append(problems, "流程必须且只能包含一个开始节点") } if finishCount == 0 { problems = append(problems, "流程至少需要一个结束或转人工节点") } if _, exists := nodeMap[startNodeKey]; !exists && startNodeKey != "" { problems = append(problems, "开始节点不存在") } adjacency := make(map[string][]string) reverse := make(map[string][]string) outgoing := make(map[string]int) for _, edge := range edges { if _, exists := nodeMap[edge.SourceNodeKey]; !exists { problems = append(problems, fmt.Sprintf("连线起点不存在:%s", edge.SourceNodeKey)) } if _, exists := nodeMap[edge.TargetNodeKey]; !exists { problems = append(problems, fmt.Sprintf("连线终点不存在:%s", edge.TargetNodeKey)) } if len(edge.Condition) > 0 && !json.Valid(edge.Condition) { problems = append(problems, fmt.Sprintf("连线 %s -> %s 的条件不是有效 JSON", edge.SourceNodeKey, edge.TargetNodeKey)) } adjacency[edge.SourceNodeKey] = append(adjacency[edge.SourceNodeKey], edge.TargetNodeKey) reverse[edge.TargetNodeKey] = append(reverse[edge.TargetNodeKey], edge.SourceNodeKey) outgoing[edge.SourceNodeKey]++ } for _, node := range nodes { if node.Type != "finish" && node.Type != "escalate" && outgoing[node.NodeKey] == 0 { problems = append(problems, fmt.Sprintf("节点“%s”没有下一步", node.Title)) } } visited := map[string]bool{} var walk func(string) walk = func(key string) { if visited[key] { return } visited[key] = true for _, next := range adjacency[key] { walk(next) } } if startNodeKey != "" { walk(startNodeKey) } for key, node := range nodeMap { if !visited[key] { problems = append(problems, fmt.Sprintf("节点“%s”无法从开始节点到达", node.Title)) } } canFinish := map[string]bool{} var walkReverse func(string) walkReverse = func(key string) { if canFinish[key] { return } canFinish[key] = true for _, previous := range reverse[key] { walkReverse(previous) } } for _, node := range nodes { if node.Type == "finish" || node.Type == "escalate" { walkReverse(node.NodeKey) } } for key, node := range nodeMap { if visited[key] && !canFinish[key] { problems = append(problems, fmt.Sprintf("节点“%s”所在路径无法结束", node.Title)) } } return problems } func ValidateForPublish(startNodeKey string, nodes []model.SOPNode, edges []model.SOPEdge, context ValidationContext) []string { problems := ValidateGraph(startNodeKey, nodes, edges) fieldMap := make(map[string]model.ScenarioField, len(context.Fields)) for _, field := range context.Fields { fieldMap[field.FieldKey] = field } collected := map[string]bool{} nodeMap := make(map[string]model.SOPNode, len(nodes)) adjacency := make(map[string][]string) for _, node := range nodes { nodeMap[node.NodeKey] = node var config map[string]interface{} if err := json.Unmarshal(node.Config, &config); err != nil { continue } switch node.Type { case "question", "choice": fieldKey, _ := config["field_key"].(string) if fieldKey == "" { problems = append(problems, fmt.Sprintf("节点“%s”没有配置采集字段", node.Title)) } else if _, exists := fieldMap[fieldKey]; !exists { problems = append(problems, fmt.Sprintf("节点“%s”引用的字段 %s 不存在", node.Title, fieldKey)) } else { collected[fieldKey] = true } case "form": keys, _ := config["field_keys"].([]interface{}) if len(keys) == 0 { problems = append(problems, fmt.Sprintf("节点“%s”没有配置表单字段", node.Title)) } for _, value := range keys { fieldKey, ok := value.(string) if !ok || fieldKey == "" { problems = append(problems, fmt.Sprintf("节点“%s”包含无效的表单字段", node.Title)) continue } if _, exists := fieldMap[fieldKey]; !exists { problems = append(problems, fmt.Sprintf("节点“%s”引用的字段 %s 不存在", node.Title, fieldKey)) continue } collected[fieldKey] = true } case "knowledge": cardID := uint64FromJSON(config["knowledge_card_id"]) if cardID == 0 || !context.PublishedKnowledgeCardIDs[cardID] { problems = append(problems, fmt.Sprintf("节点“%s”没有关联已发布的知识卡", node.Title)) } } } for _, field := range context.Fields { if field.Required && !collected[field.FieldKey] { problems = append(problems, fmt.Sprintf("必填字段“%s”没有对应的采集节点", field.FieldName)) } } defaultPaths := map[string]int{} for _, edge := range edges { adjacency[edge.SourceNodeKey] = append(adjacency[edge.SourceNodeKey], edge.TargetNodeKey) if isDefaultCondition(edge.Condition) { defaultPaths[edge.SourceNodeKey]++ continue } var rule interface{} if err := json.Unmarshal(edge.Condition, &rule); err != nil { continue } validateCondition(rule, fieldMap, fmt.Sprintf("路径 %s -> %s", edge.SourceNodeKey, edge.TargetNodeKey), &problems) } for source, count := range defaultPaths { if count > 1 { problems = append(problems, fmt.Sprintf("节点 %s 配置了多条默认路径", source)) } } for _, node := range nodes { var config map[string]interface{} _ = json.Unmarshal(node.Config, &config) if config["risk_level"] != "high" { continue } if !canReachType(node.NodeKey, "escalate", nodeMap, adjacency) { problems = append(problems, fmt.Sprintf("高风险节点“%s”没有明确的转人工或转诊路径", node.Title)) } } sort.Strings(problems) return problems } func validateCondition(value interface{}, fields map[string]model.ScenarioField, label string, problems *[]string) { rule, ok := value.(map[string]interface{}) if !ok { *problems = append(*problems, label+"的条件结构不正确") return } for _, group := range []string{"all", "any"} { if raw, exists := rule[group]; exists { items, ok := raw.([]interface{}) if !ok || len(items) == 0 { *problems = append(*problems, label+"的组合条件不能为空") return } for _, item := range items { validateCondition(item, fields, label, problems) } return } } field, _ := rule["field"].(string) operator, _ := rule["operator"].(string) if _, exists := fields[field]; field == "" || !exists { *problems = append(*problems, fmt.Sprintf("%s 引用了不存在的字段 %s", label, field)) } if !allowedConditionOperators[operator] { *problems = append(*problems, fmt.Sprintf("%s 使用了不支持的运算符 %s", label, operator)) } } func isDefaultCondition(value []byte) bool { if len(value) == 0 { return true } var condition interface{} if err := json.Unmarshal(value, &condition); err != nil || condition == nil { return condition == nil && err == nil } object, ok := condition.(map[string]interface{}) return ok && len(object) == 0 } func uint64FromJSON(value interface{}) uint64 { switch typed := value.(type) { case float64: if typed > 0 { return uint64(typed) } case uint64: return typed case int: if typed > 0 { return uint64(typed) } } return 0 } func canReachType(start, nodeType string, nodes map[string]model.SOPNode, adjacency map[string][]string) bool { visited := map[string]bool{} var walk func(string) bool walk = func(key string) bool { if visited[key] { return false } visited[key] = true if nodes[key].Type == nodeType { return true } for _, next := range adjacency[key] { if walk(next) { return true } } return false } return walk(start) }