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 KnowledgeItems []model.KnowledgeItem KnowledgeRelations []model.KnowledgeRelation } 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, "开始节点不存在") } else if startNodeKey != "" && nodeMap[startNodeKey].Type != "start" { problems = append(problems, "开始节点标识必须指向 start 类型节点") } 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)) } 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 } knowledgeByKey := make(map[string]model.KnowledgeItem, len(context.KnowledgeItems)) knowledgeTypes := make(map[string]bool) knowledgeIDs := make(map[uint64]bool, len(context.KnowledgeItems)) for _, item := range context.KnowledgeItems { if item.Status != "active" { continue } knowledgeByKey[item.ItemKey] = item knowledgeTypes[item.Type] = true knowledgeIDs[item.ID] = true } relationTypes := make(map[string]bool) for _, relation := range context.KnowledgeRelations { if knowledgeIDs[relation.FromKnowledgeID] && knowledgeIDs[relation.ToKnowledgeID] { relationTypes[relation.RelationType] = true } } 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 if node.Type == "choice" { validateChoiceOptions(node, config, fieldMap[fieldKey], &problems) } } 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": selector, ok := config["knowledge_selector"].(map[string]interface{}) if !ok { problems = append(problems, fmt.Sprintf("节点“%s”没有配置知识选择器", node.Title)) break } validateKnowledgeSelector(node, selector, knowledgeByKey, knowledgeTypes, relationTypes, &problems) if collection, ok := config["knowledge_collection"].(map[string]interface{}); ok { if keys, ok := collection["context_field_keys"].([]interface{}); ok { for _, value := range keys { if key, ok := value.(string); ok { if _, exists := fieldMap[key]; !exists { problems = append(problems, fmt.Sprintf("节点“%s”引用的采集字段 %s 不存在", node.Title, key)) } else { collected[key] = true } } } } if steps, ok := collection["steps"].([]interface{}); ok { for _, raw := range steps { if step, ok := raw.(map[string]interface{}); ok { if key, ok := step["field_key"].(string); ok && key != "" { collected[key] = true } } } } } } } for _, field := range context.Fields { if field.Required && field.SourcePath == "" && !collected[field.FieldKey] { problems = append(problems, fmt.Sprintf("必填字段“%s”没有对应的采集节点", field.FieldName)) } } defaultPaths := map[string]int{} conditionalPaths := map[string]int{} for _, edge := range edges { adjacency[edge.SourceNodeKey] = append(adjacency[edge.SourceNodeKey], edge.TargetNodeKey) if isDefaultCondition(edge.Condition) { defaultPaths[edge.SourceNodeKey]++ continue } conditionalPaths[edge.SourceNodeKey]++ 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 source := range conditionalPaths { if defaultPaths[source] == 0 { 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 validateChoiceOptions(node model.SOPNode, config map[string]interface{}, field model.ScenarioField, problems *[]string) { values, ok := config["options"].([]interface{}) if !ok || len(values) == 0 { *problems = append(*problems, fmt.Sprintf("选择节点“%s”没有配置选项", node.Title)) return } if field.FieldType != "text" && field.FieldType != "select" { *problems = append(*problems, fmt.Sprintf("选择节点“%s”只能写入文本或单选字段", node.Title)) } seen := map[string]bool{} for _, value := range values { option, ok := value.(string) if !ok || option == "" { *problems = append(*problems, fmt.Sprintf("选择节点“%s”包含无效选项", node.Title)) continue } if seen[option] { *problems = append(*problems, fmt.Sprintf("选择节点“%s”包含重复选项", node.Title)) } seen[option] = true if field.FieldType == "select" && !jsonStringArrayContains(field.Options, option) { *problems = append(*problems, fmt.Sprintf("选择节点“%s”的选项“%s”不在字段选项中", node.Title, option)) } } } func jsonStringArrayContains(raw []byte, expected string) bool { var values []string if json.Unmarshal(raw, &values) != nil { return false } for _, value := range values { if value == expected { return true } } return false } 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 } _, hasAll := rule["all"] _, hasAny := rule["any"] if hasAll && hasAny { *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 validateKnowledgeSelector(node model.SOPNode, selector map[string]interface{}, items map[string]model.KnowledgeItem, availableTypes, availableRelations map[string]bool, problems *[]string) { derivedField, _ := selector["derived_field"].(string) answerField, _ := selector["answer_field"].(string) keys, keysValid := selectorStrings(selector, "knowledge_keys") types, typesValid := selectorStrings(selector, "knowledge_types") relations, relationsValid := selectorStrings(selector, "relation_types") if !keysValid || !typesValid || !relationsValid { *problems = append(*problems, fmt.Sprintf("节点“%s”的知识选择器必须使用字符串数组", node.Title)) return } if derivedField == "" && answerField == "" && len(keys) == 0 { *problems = append(*problems, fmt.Sprintf("节点“%s”没有配置派生知识字段或固定知识 key", node.Title)) } allowedTypes := make(map[string]bool, len(types)) for _, itemType := range types { allowedTypes[itemType] = true if !availableTypes[itemType] { *problems = append(*problems, fmt.Sprintf("节点“%s”引用的知识类型 %s 不存在", node.Title, itemType)) } } for _, key := range keys { item, exists := items[key] if !exists { *problems = append(*problems, fmt.Sprintf("节点“%s”引用的知识 key %s 不存在或未启用", node.Title, key)) continue } if len(allowedTypes) > 0 && !allowedTypes[item.Type] { *problems = append(*problems, fmt.Sprintf("节点“%s”的知识 key %s 不属于允许的根类型", node.Title, key)) } } for _, relationType := range relations { if !availableRelations[relationType] { *problems = append(*problems, fmt.Sprintf("节点“%s”引用的知识关系类型 %s 不存在", node.Title, relationType)) } } } func selectorStrings(selector map[string]interface{}, key string) ([]string, bool) { raw, exists := selector[key] if !exists || raw == nil { return nil, true } values, ok := raw.([]interface{}) if !ok { return nil, false } result := make([]string, 0, len(values)) for _, value := range values { text, ok := value.(string) if !ok || text == "" { return nil, false } result = append(result, text) } return result, true } 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) }