package rules import ( "context" "encoding/json" "fmt" ) type WorkflowSimulationStep struct { NodeID string `json:"node_id"` NodeType string `json:"node_type"` Handle string `json:"handle,omitempty"` } type WorkflowSimulationResult struct { Matched bool `json:"matched"` Steps []WorkflowSimulationStep `json:"steps,omitempty"` Actions []SimulatedActionResult `json:"actions,omitempty"` } func (e *Engine) SimulateWorkflow(ctx context.Context, userID string, wf *Workflow, msg *Message, evt *EventContext) WorkflowSimulationResult { if wf == nil || len(wf.Nodes) == 0 { return WorkflowSimulationResult{Matched: false} } if wf.Kind != RuleKindFunction && !matchesTriggers(wf.Triggers, msg, evt) { return WorkflowSimulationResult{Matched: false} } startID := wf.findStartNode() if startID == "" { return WorkflowSimulationResult{Matched: false} } execCtx := newExecContext(msg, userID, wf.Variables, evt) steps := make([]WorkflowSimulationStep, 0) e.simulateWalk(ctx, userID, msg, wf, startID, execCtx, &steps, 0) simActions := make([]SimulatedActionResult, 0, len(execCtx.Results)) for _, r := range execCtx.Results { simActions = append(simActions, SimulatedActionResult{ActionResult: r}) } return WorkflowSimulationResult{ Matched: true, Steps: steps, Actions: simActions, } } func (e *Engine) simulateWalk(ctx context.Context, userID string, msg *Message, wf *Workflow, nodeID string, execCtx *ExecContext, steps *[]WorkflowSimulationStep, depth int) { if depth > maxWorkflowDepth || nodeID == "" { return } nodes := wf.nodeMap() node, ok := nodes[nodeID] if !ok { return } switch node.Type { case "start": *steps = append(*steps, WorkflowSimulationStep{NodeID: nodeID, NodeType: node.Type}) e.simulateWalk(ctx, userID, msg, wf, wf.nextDefault(nodeID), execCtx, steps, depth+1) case "condition": var data ConditionNodeData json.Unmarshal(node.Data, &data) handle := "false" if matchCondition(Condition{Field: data.Field, Operator: data.Operator, Value: interpolateValue(data.Value, execCtx)}, msg, execCtx.Event) { handle = "true" } *steps = append(*steps, WorkflowSimulationStep{NodeID: nodeID, NodeType: node.Type, Handle: handle}) e.simulateWalk(ctx, userID, msg, wf, wf.nextNode(nodeID, handle), execCtx, steps, depth+1) case "label_check": var data LabelCheckNodeData json.Unmarshal(node.Data, &data) op := "has" if data.Operator == "not_has" { op = "not_has" } handle := "false" if matchCondition(Condition{Field: "label", Operator: op, Value: data.Label}, msg, execCtx.Event) { handle = "true" } *steps = append(*steps, WorkflowSimulationStep{NodeID: nodeID, NodeType: node.Type, Handle: handle}) e.simulateWalk(ctx, userID, msg, wf, wf.nextNode(nodeID, handle), execCtx, steps, depth+1) case "switch": var data SwitchNodeData json.Unmarshal(node.Data, &data) fieldVal := workflowFieldValue(data.Field, msg, execCtx.Event, execCtx) handle := "default" for i, c := range data.Cases { if fieldVal == c.Value { handle = fmt.Sprintf("case-%d", i) break } } *steps = append(*steps, WorkflowSimulationStep{NodeID: nodeID, NodeType: node.Type, Handle: handle}) next := wf.nextNode(nodeID, handle) if next == "" { next = wf.nextNode(nodeID, "default") } e.simulateWalk(ctx, userID, msg, wf, next, execCtx, steps, depth+1) case "llm_check": var data LLMCheckNodeData json.Unmarshal(node.Data, &data) handle := "false" if e.evaluateLLMCheck(ctx, data, msg, execCtx) { handle = "true" } *steps = append(*steps, WorkflowSimulationStep{NodeID: nodeID, NodeType: node.Type, Handle: handle}) e.simulateWalk(ctx, userID, msg, wf, wf.nextNode(nodeID, handle), execCtx, steps, depth+1) case "actions": var data ActionsNodeData json.Unmarshal(node.Data, &data) *steps = append(*steps, WorkflowSimulationStep{NodeID: nodeID, NodeType: node.Type}) for _, item := range data.Actions { action := Action{Type: item.Type, Value: interpolateValue(item.Value, execCtx)} execCtx.Results = append(execCtx.Results, e.simulateAction(ctx, action, msg).ActionResult) } e.simulateWalk(ctx, userID, msg, wf, wf.nextDefault(nodeID), execCtx, steps, depth+1) case "set_var": var data SetVarNodeData json.Unmarshal(node.Data, &data) execCtx.Variables[data.Name] = interpolateValue(data.Value, execCtx) *steps = append(*steps, WorkflowSimulationStep{NodeID: nodeID, NodeType: node.Type}) e.simulateWalk(ctx, userID, msg, wf, wf.nextDefault(nodeID), execCtx, steps, depth+1) case "call_function", "call_rule": var data CallRuleNodeData json.Unmarshal(node.Data, &data) *steps = append(*steps, WorkflowSimulationStep{NodeID: nodeID, NodeType: node.Type}) e.simulateWalk(ctx, userID, msg, wf, wf.nextDefault(nodeID), execCtx, steps, depth+1) case "end": *steps = append(*steps, WorkflowSimulationStep{NodeID: nodeID, NodeType: node.Type}) } }