feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,782 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/crossdomain/variable"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/adaptor"
|
||||
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
|
||||
"github.com/coze-dev/coze-studio/backend/types/errno"
|
||||
)
|
||||
|
||||
type Issue struct {
|
||||
NodeErr *NodeErr
|
||||
PathErr *PathErr
|
||||
Message string
|
||||
}
|
||||
type NodeErr struct {
|
||||
NodeID string `json:"nodeID"`
|
||||
NodeName string `json:"nodeName"`
|
||||
}
|
||||
type PathErr struct {
|
||||
StartNode string `json:"start"`
|
||||
EndNode string `json:"end"`
|
||||
}
|
||||
|
||||
type reachability struct {
|
||||
reachableNodes map[string]*vo.Node
|
||||
nestedReachability map[string]*reachability
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Canvas *vo.Canvas
|
||||
AppID *int64
|
||||
AgentID *int64
|
||||
VariablesMetaGetter variable.VariablesMetaGetter
|
||||
}
|
||||
|
||||
type CanvasValidator struct {
|
||||
cfg *Config
|
||||
reachability *reachability
|
||||
}
|
||||
|
||||
func NewCanvasValidator(_ context.Context, cfg *Config) (*CanvasValidator, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config is required")
|
||||
}
|
||||
|
||||
if cfg.Canvas == nil {
|
||||
return nil, fmt.Errorf("canvas is required")
|
||||
}
|
||||
|
||||
reachability, err := analyzeCanvasReachability(cfg.Canvas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CanvasValidator{reachability: reachability, cfg: cfg}, nil
|
||||
}
|
||||
|
||||
func (cv *CanvasValidator) DetectCycles(_ context.Context) (issues []*Issue, err error) {
|
||||
issues = make([]*Issue, 0)
|
||||
nodeIDs := make([]string, 0)
|
||||
for _, node := range cv.cfg.Canvas.Nodes {
|
||||
nodeIDs = append(nodeIDs, node.ID)
|
||||
}
|
||||
|
||||
controlSuccessors := map[string][]string{}
|
||||
for _, e := range cv.cfg.Canvas.Edges {
|
||||
controlSuccessors[e.TargetNodeID] = append(controlSuccessors[e.TargetNodeID], e.SourceNodeID)
|
||||
}
|
||||
|
||||
cycles := detectCycles(nodeIDs, controlSuccessors)
|
||||
if len(cycles) == 0 {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
for _, cycle := range cycles {
|
||||
n := len(cycle)
|
||||
for i := 0; i < n; i++ {
|
||||
if cycle[i] == cycle[(i+1)%n] {
|
||||
continue
|
||||
}
|
||||
issues = append(issues, &Issue{
|
||||
PathErr: &PathErr{
|
||||
StartNode: cycle[i],
|
||||
EndNode: cycle[(i+1)%n],
|
||||
},
|
||||
Message: "line connections do not allow parallel lines to intersect and form loops with each other",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (cv *CanvasValidator) ValidateConnections(ctx context.Context) (issues []*Issue, err error) {
|
||||
issues, err = validateConnections(ctx, cv.cfg.Canvas)
|
||||
if err != nil {
|
||||
return issues, err
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (cv *CanvasValidator) CheckRefVariable(ctx context.Context) (issues []*Issue, err error) {
|
||||
issues = make([]*Issue, 0)
|
||||
var checkRefVariable func(reachability *reachability, reachableNodes map[string]bool) error
|
||||
checkRefVariable = func(reachability *reachability, parentReachableNodes map[string]bool) error {
|
||||
currentReachableNodes := make(map[string]bool)
|
||||
combinedReachable := make(map[string]bool)
|
||||
for _, node := range reachability.reachableNodes {
|
||||
currentReachableNodes[node.ID] = true
|
||||
combinedReachable[node.ID] = true
|
||||
}
|
||||
|
||||
if parentReachableNodes != nil {
|
||||
for id := range parentReachableNodes {
|
||||
combinedReachable[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
var inputBlockVerify func(node *vo.Node, ref *vo.BlockInput) error
|
||||
inputBlockVerify = func(node *vo.Node, inputBlock *vo.BlockInput) error {
|
||||
if inputBlock.Value.Type != vo.BlockInputValueTypeRef {
|
||||
return nil
|
||||
}
|
||||
ref, err := parseBlockInputRef(inputBlock.Value.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ref.Source == vo.RefSourceTypeGlobalApp || ref.Source == vo.RefSourceTypeGlobalSystem || ref.Source == vo.RefSourceTypeGlobalUser {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ref.Source == vo.RefSourceTypeBlockOutput && ref.BlockID == "" {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: node.ID,
|
||||
NodeName: node.Data.Meta.Title,
|
||||
},
|
||||
Message: `ref block error,[blockID] is empty`,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, exists := combinedReachable[ref.BlockID]; !exists {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: node.ID,
|
||||
NodeName: node.Data.Meta.Title,
|
||||
},
|
||||
Message: fmt.Sprintf(`the node id "%s" on which node id "%s" depends does not exist`, node.ID, ref.BlockID),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for nodeID, node := range reachability.reachableNodes {
|
||||
if node.Data != nil && node.Data.Inputs != nil && node.Data.Inputs.InputParameters != nil { // only validate InputParameters
|
||||
parameters := node.Data.Inputs.InputParameters
|
||||
for _, p := range parameters {
|
||||
if p.Input != nil {
|
||||
valid := validateInputParameterName(p.Name)
|
||||
if !valid {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: nodeID,
|
||||
NodeName: node.Data.Meta.Title,
|
||||
},
|
||||
Message: fmt.Sprintf(`parameter name only allows number or alphabet, and must begin with alphabet, but it's "%s"`, p.Name),
|
||||
})
|
||||
}
|
||||
err = inputBlockVerify(node, p.Input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if p.Left != nil {
|
||||
err = inputBlockVerify(node, p.Left)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if p.Right != nil {
|
||||
err = inputBlockVerify(node, p.Right)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range reachability.nestedReachability {
|
||||
err := checkRefVariable(r, currentReachableNodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
err = checkRefVariable(cv.reachability, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (cv *CanvasValidator) ValidateNestedFlows(ctx context.Context) (issues []*Issue, err error) {
|
||||
issues = make([]*Issue, 0)
|
||||
for nodeID, node := range cv.reachability.reachableNodes {
|
||||
if nestedReachableNodes, ok := cv.reachability.nestedReachability[nodeID]; ok && len(nestedReachableNodes.nestedReachability) > 0 {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: nodeID,
|
||||
NodeName: node.Data.Meta.Title,
|
||||
},
|
||||
Message: "composite nodes such as batch/loop cannot be nested",
|
||||
})
|
||||
}
|
||||
}
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (cv *CanvasValidator) CheckGlobalVariables(ctx context.Context) (issues []*Issue, err error) {
|
||||
if cv.cfg.AppID == nil && cv.cfg.AgentID == nil {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
type nodeVars struct {
|
||||
node *vo.Node
|
||||
vars map[string]*vo.TypeInfo
|
||||
}
|
||||
|
||||
nVars := make([]*nodeVars, 0)
|
||||
for _, node := range cv.cfg.Canvas.Nodes {
|
||||
if node.Type == vo.BlockTypeBotComment {
|
||||
continue
|
||||
}
|
||||
if node.Type == vo.BlockTypeBotAssignVariable {
|
||||
v := &nodeVars{node: node, vars: make(map[string]*vo.TypeInfo)}
|
||||
for _, p := range node.Data.Inputs.InputParameters {
|
||||
v.vars[p.Name], err = adaptor.CanvasBlockInputToTypeInfo(p.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
nVars = append(nVars, v)
|
||||
}
|
||||
}
|
||||
|
||||
if len(nVars) == 0 {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
var varsMeta map[string]*vo.TypeInfo
|
||||
if cv.cfg.AppID != nil {
|
||||
varsMeta, err = cv.cfg.VariablesMetaGetter.GetAppVariablesMeta(ctx, strconv.FormatInt(*cv.cfg.AppID, 10), "")
|
||||
} else {
|
||||
varsMeta, err = cv.cfg.VariablesMetaGetter.GetAgentVariablesMeta(ctx, *cv.cfg.AgentID, "")
|
||||
}
|
||||
|
||||
for _, nodeVar := range nVars {
|
||||
nodeName := nodeVar.node.Data.Meta.Title
|
||||
nodeID := nodeVar.node.ID
|
||||
for v, info := range nodeVar.vars {
|
||||
vInfo, ok := varsMeta[v]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if vInfo.Type != info.Type {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: nodeID,
|
||||
NodeName: nodeName,
|
||||
},
|
||||
Message: fmt.Sprintf("node name %v,param [%s], type mismatch", nodeName, v),
|
||||
})
|
||||
}
|
||||
|
||||
if vInfo.Type == vo.DataTypeArray && info.Type == vo.DataTypeArray {
|
||||
if vInfo.ElemTypeInfo.Type != info.ElemTypeInfo.Type {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: nodeID,
|
||||
NodeName: nodeName,
|
||||
},
|
||||
Message: fmt.Sprintf("node name %v, param [%s], array element type mismatch", nodeName, v),
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (cv *CanvasValidator) CheckSubWorkFlowTerminatePlanType(ctx context.Context) (issues []*Issue, err error) {
|
||||
issues = make([]*Issue, 0)
|
||||
subWfMap := make([]*vo.Node, 0)
|
||||
var (
|
||||
draftIDs []int64
|
||||
subID2SubVersion = map[int64]string{}
|
||||
)
|
||||
var collectSubWorkFlowNodes func(nodes []*vo.Node)
|
||||
collectSubWorkFlowNodes = func(nodes []*vo.Node) {
|
||||
for _, n := range nodes {
|
||||
if n.Type == vo.BlockTypeBotSubWorkflow {
|
||||
subWfMap = append(subWfMap, n)
|
||||
wID, err := strconv.ParseInt(n.Data.Inputs.WorkflowID, 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(n.Data.Inputs.WorkflowVersion) > 0 {
|
||||
subID2SubVersion[wID] = n.Data.Inputs.WorkflowVersion
|
||||
} else {
|
||||
draftIDs = append(draftIDs, wID)
|
||||
}
|
||||
}
|
||||
if len(n.Blocks) > 0 {
|
||||
collectSubWorkFlowNodes(n.Blocks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectSubWorkFlowNodes(cv.cfg.Canvas.Nodes)
|
||||
|
||||
if len(subWfMap) == 0 {
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
wfID2Canvas := make(map[int64]*vo.Canvas)
|
||||
|
||||
if len(draftIDs) > 0 {
|
||||
wfs, _, err := workflow.GetRepository().MGetDrafts(ctx, &vo.MGetPolicy{
|
||||
MetaQuery: vo.MetaQuery{
|
||||
IDs: draftIDs,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, draft := range wfs {
|
||||
var canvas vo.Canvas
|
||||
if err = sonic.UnmarshalString(draft.Canvas, &canvas); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wfID2Canvas[draft.ID] = &canvas
|
||||
}
|
||||
}
|
||||
|
||||
if len(subID2SubVersion) > 0 {
|
||||
for id, version := range subID2SubVersion {
|
||||
v, err := workflow.GetRepository().GetVersion(ctx, id, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var canvas vo.Canvas
|
||||
if err = sonic.UnmarshalString(v.Canvas, &canvas); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wfID2Canvas[id] = &canvas
|
||||
}
|
||||
}
|
||||
|
||||
for _, node := range subWfMap {
|
||||
wfID, err := strconv.ParseInt(node.Data.Inputs.WorkflowID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c, ok := wfID2Canvas[wfID]; !ok {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: node.ID,
|
||||
NodeName: node.Data.Meta.Title,
|
||||
},
|
||||
Message: "sub workflow has been modified, please refresh the page",
|
||||
})
|
||||
} else {
|
||||
_, endNode, err := findStartAndEndNodes(c.Nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if endNode != nil {
|
||||
if string(*endNode.Data.Inputs.TerminatePlan) != toTerminatePlan(node.Data.Inputs.TerminationType) {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: node.ID,
|
||||
NodeName: node.Data.Meta.Title,
|
||||
},
|
||||
Message: "sub workflow has been modified, please refresh the page",
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func validateConnections(ctx context.Context, c *vo.Canvas) (issues []*Issue, err error) {
|
||||
issues = make([]*Issue, 0)
|
||||
nodeMap := buildNodeMap(c)
|
||||
for _, node := range nodeMap {
|
||||
if len(node.Blocks) > 0 && len(node.Edges) > 0 {
|
||||
n := &vo.Node{
|
||||
ID: node.ID,
|
||||
Type: node.Type,
|
||||
Data: node.Data,
|
||||
}
|
||||
nestedCanvas := &vo.Canvas{
|
||||
Nodes: append(node.Blocks, n),
|
||||
Edges: node.Edges,
|
||||
}
|
||||
|
||||
is, err := validateConnections(ctx, nestedCanvas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issues = append(issues, is...)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
outDegree := make(map[string]int)
|
||||
selectorPorts := make(map[string]map[string]bool)
|
||||
|
||||
for nodeID, node := range nodeMap {
|
||||
switch node.Type {
|
||||
case vo.BlockTypeCondition:
|
||||
branches := node.Data.Inputs.Branches
|
||||
if _, exists := selectorPorts[nodeID]; !exists {
|
||||
selectorPorts[nodeID] = make(map[string]bool)
|
||||
}
|
||||
selectorPorts[nodeID]["false"] = true
|
||||
for index := range branches {
|
||||
if index == 0 {
|
||||
selectorPorts[nodeID]["true"] = true
|
||||
} else {
|
||||
selectorPorts[nodeID][fmt.Sprintf("true_%v", index)] = true
|
||||
}
|
||||
}
|
||||
case vo.BlockTypeBotIntent:
|
||||
intents := node.Data.Inputs.Intents
|
||||
if _, exists := selectorPorts[nodeID]; !exists {
|
||||
selectorPorts[nodeID] = make(map[string]bool)
|
||||
}
|
||||
for index := range intents {
|
||||
selectorPorts[nodeID][fmt.Sprintf("branch_%v", index)] = true
|
||||
}
|
||||
selectorPorts[nodeID]["default"] = true
|
||||
if node.Data.Inputs.SettingOnError != nil && node.Data.Inputs.SettingOnError.ProcessType != nil &&
|
||||
*node.Data.Inputs.SettingOnError.ProcessType == vo.ErrorProcessTypeExceptionBranch {
|
||||
selectorPorts[nodeID]["branch_error"] = true
|
||||
}
|
||||
case vo.BlockTypeQuestion:
|
||||
if node.Data.Inputs.QA.AnswerType == vo.QAAnswerTypeOption {
|
||||
if _, exists := selectorPorts[nodeID]; !exists {
|
||||
selectorPorts[nodeID] = make(map[string]bool)
|
||||
}
|
||||
if node.Data.Inputs.QA.OptionType == vo.QAOptionTypeStatic {
|
||||
for index := range node.Data.Inputs.QA.Options {
|
||||
selectorPorts[nodeID][fmt.Sprintf("branch_%v", index)] = true
|
||||
}
|
||||
}
|
||||
|
||||
if node.Data.Inputs.QA.OptionType == vo.QAOptionTypeDynamic {
|
||||
selectorPorts[nodeID][fmt.Sprintf("branch_%v", 0)] = true
|
||||
}
|
||||
}
|
||||
default:
|
||||
if node.Data.Inputs != nil && node.Data.Inputs.SettingOnError != nil &&
|
||||
node.Data.Inputs.SettingOnError.ProcessType != nil &&
|
||||
*node.Data.Inputs.SettingOnError.ProcessType == vo.ErrorProcessTypeExceptionBranch {
|
||||
if _, exists := selectorPorts[nodeID]; !exists {
|
||||
selectorPorts[nodeID] = make(map[string]bool)
|
||||
}
|
||||
selectorPorts[nodeID]["branch_error"] = true
|
||||
selectorPorts[nodeID]["default"] = true
|
||||
} else {
|
||||
outDegree[node.ID] = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, edge := range c.Edges {
|
||||
outDegree[edge.SourceNodeID]++
|
||||
}
|
||||
|
||||
portOutDegree := make(map[string]map[string]int) // 节点ID -> 端口 -> 出度
|
||||
for _, edge := range c.Edges {
|
||||
|
||||
if _, ok := selectorPorts[edge.SourceNodeID]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, exists := portOutDegree[edge.SourceNodeID]; !exists {
|
||||
portOutDegree[edge.SourceNodeID] = make(map[string]int)
|
||||
}
|
||||
|
||||
portOutDegree[edge.SourceNodeID][edge.SourcePortID]++
|
||||
|
||||
}
|
||||
|
||||
for nodeID, node := range nodeMap {
|
||||
nodeName := node.Data.Meta.Title
|
||||
|
||||
switch node.Type {
|
||||
case vo.BlockTypeBotStart:
|
||||
if outDegree[nodeID] == 0 {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: nodeID,
|
||||
NodeName: nodeName,
|
||||
},
|
||||
Message: `node "start" not connected`,
|
||||
})
|
||||
}
|
||||
case vo.BlockTypeBotEnd:
|
||||
default:
|
||||
if ports, isSelector := selectorPorts[nodeID]; isSelector {
|
||||
selectorIssues := &Issue{NodeErr: &NodeErr{
|
||||
NodeID: node.ID,
|
||||
NodeName: nodeName,
|
||||
}}
|
||||
message := ""
|
||||
for port := range ports {
|
||||
if portOutDegree[nodeID][port] == 0 {
|
||||
message += fmt.Sprintf(`node "%v"'s port "%v" not connected;`, nodeName, port)
|
||||
}
|
||||
}
|
||||
if len(message) > 0 {
|
||||
selectorIssues.Message = message
|
||||
issues = append(issues, selectorIssues)
|
||||
}
|
||||
} else {
|
||||
// break, continue 不检查出度
|
||||
if node.Type == vo.BlockTypeBotBreak || node.Type == vo.BlockTypeBotContinue {
|
||||
continue
|
||||
}
|
||||
if outDegree[nodeID] == 0 {
|
||||
issues = append(issues, &Issue{
|
||||
NodeErr: &NodeErr{
|
||||
NodeID: node.ID,
|
||||
NodeName: nodeName,
|
||||
},
|
||||
Message: fmt.Sprintf(`node "%v" not connected`, nodeName),
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func analyzeCanvasReachability(c *vo.Canvas) (*reachability, error) {
|
||||
nodeMap := buildNodeMap(c)
|
||||
reachable := &reachability{}
|
||||
|
||||
if err := processNestedReachability(c, reachable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startNode, endNode, err := findStartAndEndNodes(c.Nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edgeMap := make(map[string][]string)
|
||||
for _, edge := range c.Edges {
|
||||
edgeMap[edge.SourceNodeID] = append(edgeMap[edge.SourceNodeID], edge.TargetNodeID)
|
||||
}
|
||||
|
||||
reachable.reachableNodes, err = performReachabilityAnalysis(nodeMap, edgeMap, startNode, endNode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reachable, nil
|
||||
}
|
||||
|
||||
func buildNodeMap(c *vo.Canvas) map[string]*vo.Node {
|
||||
nodeMap := make(map[string]*vo.Node, len(c.Nodes))
|
||||
for _, node := range c.Nodes {
|
||||
nodeMap[node.ID] = node
|
||||
}
|
||||
return nodeMap
|
||||
}
|
||||
|
||||
func processNestedReachability(c *vo.Canvas, r *reachability) error {
|
||||
for _, node := range c.Nodes {
|
||||
if len(node.Blocks) > 0 && len(node.Edges) > 0 {
|
||||
nestedCanvas := &vo.Canvas{
|
||||
Nodes: append([]*vo.Node{
|
||||
{
|
||||
ID: node.ID,
|
||||
Type: vo.BlockTypeBotStart,
|
||||
Data: node.Data,
|
||||
},
|
||||
{
|
||||
ID: node.ID,
|
||||
Type: vo.BlockTypeBotEnd,
|
||||
},
|
||||
}, node.Blocks...),
|
||||
Edges: node.Edges,
|
||||
}
|
||||
nestedReachable, err := analyzeCanvasReachability(nestedCanvas)
|
||||
if err != nil {
|
||||
return fmt.Errorf("processing nested canvas for node %s: %w", node.ID, err)
|
||||
}
|
||||
if r.nestedReachability == nil {
|
||||
r.nestedReachability = make(map[string]*reachability)
|
||||
}
|
||||
r.nestedReachability[node.ID] = nestedReachable
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findStartAndEndNodes(nodes []*vo.Node) (*vo.Node, *vo.Node, error) {
|
||||
var startNode, endNode *vo.Node
|
||||
|
||||
for _, node := range nodes {
|
||||
switch node.Type {
|
||||
case vo.BlockTypeBotStart:
|
||||
startNode = node
|
||||
case vo.BlockTypeBotEnd:
|
||||
endNode = node
|
||||
}
|
||||
}
|
||||
|
||||
if startNode == nil {
|
||||
return nil, nil, fmt.Errorf("start node not found")
|
||||
}
|
||||
if endNode == nil {
|
||||
return nil, nil, fmt.Errorf("end node not found")
|
||||
}
|
||||
|
||||
return startNode, endNode, nil
|
||||
}
|
||||
|
||||
func performReachabilityAnalysis(nodeMap map[string]*vo.Node, edgeMap map[string][]string, startNode *vo.Node, endNode *vo.Node) (map[string]*vo.Node, error) {
|
||||
result := make(map[string]*vo.Node)
|
||||
result[startNode.ID] = startNode
|
||||
|
||||
queue := []string{startNode.ID}
|
||||
visited := make(map[string]bool)
|
||||
visited[startNode.ID] = true
|
||||
|
||||
for len(queue) > 0 {
|
||||
currentID := queue[0]
|
||||
queue = queue[1:]
|
||||
for _, targetNodeID := range edgeMap[currentID] {
|
||||
if !visited[targetNodeID] {
|
||||
visited[targetNodeID] = true
|
||||
node, ok := nodeMap[targetNodeID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("node not found for %s in nodeMap", targetNodeID)
|
||||
}
|
||||
result[targetNodeID] = node
|
||||
queue = append(queue, targetNodeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func toTerminatePlan(p int) string {
|
||||
switch p {
|
||||
case 0:
|
||||
return "returnVariables"
|
||||
case 1:
|
||||
return "useAnswerContent"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func detectCycles(nodes []string, controlSuccessors map[string][]string) [][]string {
|
||||
visited := map[string]bool{}
|
||||
var dfs func(path []string) [][]string
|
||||
dfs = func(path []string) [][]string {
|
||||
var ret [][]string
|
||||
pathEnd := path[len(path)-1]
|
||||
successors, ok := controlSuccessors[pathEnd]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, successor := range successors {
|
||||
visited[successor] = true
|
||||
var looped bool
|
||||
for i, node := range path {
|
||||
if node == successor {
|
||||
ret = append(ret, append(path[i:], successor))
|
||||
looped = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if looped {
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, dfs(append(path, successor))...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
var ret [][]string
|
||||
for _, node := range nodes {
|
||||
if !visited[node] {
|
||||
ret = append(ret, dfs([]string{node})...)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseBlockInputRef(content any) (*vo.BlockInputReference, error) {
|
||||
m, ok := content.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid content type: %T when parse BlockInputRef", content)
|
||||
}
|
||||
|
||||
marshaled, err := sonic.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
|
||||
}
|
||||
|
||||
p := &vo.BlockInputReference{}
|
||||
if err = sonic.Unmarshal(marshaled, p); err != nil {
|
||||
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
var validateNameRegex = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
func validateInputParameterName(name string) bool {
|
||||
return validateNameRegex.Match([]byte(name))
|
||||
}
|
||||
Reference in New Issue
Block a user