ultisuite-backend/internal/mail/rules/workflow_simulate.go
R3D347HR4Y 082cac36b2 feat(automation): dispatch rules/webhooks on mail, drive, contacts
Wire automation dispatcher to IMAP sync, drive mutations, and contact CRUD.
Add webhook event_types and mail/drive/contacts scope filters (migration 30).
2026-06-07 15:51:47 +02:00

140 lines
4.8 KiB
Go

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})
}
}