357 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			357 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
|  * 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 service
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	cloudworkflow "github.com/coze-dev/coze-studio/backend/api/model/ocean/cloud/workflow"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/crossdomain/variable"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
 | |
| 	"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/domain/workflow/internal/canvas/validate"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/sonic"
 | |
| 	"github.com/coze-dev/coze-studio/backend/types/errno"
 | |
| )
 | |
| 
 | |
| func validateWorkflowTree(ctx context.Context, config vo.ValidateTreeConfig) ([]*validate.Issue, error) {
 | |
| 	c := &vo.Canvas{}
 | |
| 	err := sonic.UnmarshalString(config.CanvasSchema, &c)
 | |
| 	if err != nil {
 | |
| 		return nil, vo.WrapError(errno.ErrSerializationDeserializationFail,
 | |
| 			fmt.Errorf("failed to unmarshal canvas schema: %w", err))
 | |
| 	}
 | |
| 
 | |
| 	c.Nodes, c.Edges = adaptor.PruneIsolatedNodes(c.Nodes, c.Edges, nil)
 | |
| 	validator, err := validate.NewCanvasValidator(ctx, &validate.Config{
 | |
| 		Canvas:              c,
 | |
| 		AppID:               config.AppID,
 | |
| 		AgentID:             config.AgentID,
 | |
| 		VariablesMetaGetter: variable.GetVariablesMetaGetter(),
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to new canvas validate : %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var issues []*validate.Issue
 | |
| 	issues, err = validator.ValidateConnections(ctx)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to check connectivity : %w", err)
 | |
| 	}
 | |
| 	if len(issues) > 0 {
 | |
| 		return issues, nil
 | |
| 	}
 | |
| 
 | |
| 	issues, err = validator.DetectCycles(ctx)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to check loops: %w", err)
 | |
| 	}
 | |
| 	if len(issues) > 0 {
 | |
| 		return issues, nil
 | |
| 	}
 | |
| 
 | |
| 	issues, err = validator.ValidateNestedFlows(ctx)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to check nested batch or recurse: %w", err)
 | |
| 	}
 | |
| 	if len(issues) > 0 {
 | |
| 		return issues, nil
 | |
| 	}
 | |
| 
 | |
| 	issues, err = validator.CheckRefVariable(ctx)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to check ref variable: %w", err)
 | |
| 	}
 | |
| 	if len(issues) > 0 {
 | |
| 		return issues, nil
 | |
| 	}
 | |
| 
 | |
| 	issues, err = validator.CheckGlobalVariables(ctx)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to check global variables: %w", err)
 | |
| 	}
 | |
| 	if len(issues) > 0 {
 | |
| 		return issues, nil
 | |
| 	}
 | |
| 
 | |
| 	issues, err = validator.CheckSubWorkFlowTerminatePlanType(ctx)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to check sub workflow terminate plan type: %w", err)
 | |
| 	}
 | |
| 	if len(issues) > 0 {
 | |
| 		return issues, nil
 | |
| 	}
 | |
| 
 | |
| 	return issues, nil
 | |
| }
 | |
| 
 | |
| func convertToValidationError(issue *validate.Issue) *cloudworkflow.ValidateErrorData {
 | |
| 	e := &cloudworkflow.ValidateErrorData{}
 | |
| 	e.Message = issue.Message
 | |
| 	if issue.NodeErr != nil {
 | |
| 		e.Type = cloudworkflow.ValidateErrorType_BotValidateNodeErr
 | |
| 		e.NodeError = &cloudworkflow.NodeError{
 | |
| 			NodeID: issue.NodeErr.NodeID,
 | |
| 		}
 | |
| 	} else if issue.PathErr != nil {
 | |
| 		e.Type = cloudworkflow.ValidateErrorType_BotValidatePathErr
 | |
| 		e.PathError = &cloudworkflow.PathError{
 | |
| 			Start: issue.PathErr.StartNode,
 | |
| 			End:   issue.PathErr.EndNode,
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return e
 | |
| }
 | |
| 
 | |
| func toValidateErrorData(issues []*validate.Issue) []*cloudworkflow.ValidateErrorData {
 | |
| 	validateErrors := make([]*cloudworkflow.ValidateErrorData, 0, len(issues))
 | |
| 	for _, issue := range issues {
 | |
| 		validateErrors = append(validateErrors, convertToValidationError(issue))
 | |
| 	}
 | |
| 	return validateErrors
 | |
| }
 | |
| 
 | |
| func toValidateIssue(id int64, name string, issues []*validate.Issue) *vo.ValidateIssue {
 | |
| 	vIssue := &vo.ValidateIssue{
 | |
| 		WorkflowID:   id,
 | |
| 		WorkflowName: name,
 | |
| 	}
 | |
| 	for _, issue := range issues {
 | |
| 		vIssue.IssueMessages = append(vIssue.IssueMessages, issue.Message)
 | |
| 	}
 | |
| 	return vIssue
 | |
| }
 | |
| 
 | |
| type version struct {
 | |
| 	Prefix string
 | |
| 	Major  int
 | |
| 	Minor  int
 | |
| 	Patch  int
 | |
| }
 | |
| 
 | |
| func parseVersion(versionString string) (_ version, err error) {
 | |
| 	defer func() {
 | |
| 		if err != nil {
 | |
| 			err = vo.WrapError(errno.ErrInvalidVersionName, err)
 | |
| 		}
 | |
| 	}()
 | |
| 	if !strings.HasPrefix(versionString, "v") {
 | |
| 		return version{}, fmt.Errorf("invalid prefix format: %s", versionString)
 | |
| 	}
 | |
| 	versionString = strings.TrimPrefix(versionString, "v")
 | |
| 	parts := strings.Split(versionString, ".")
 | |
| 	if len(parts) != 3 {
 | |
| 		return version{}, fmt.Errorf("invalid version format: %s", versionString)
 | |
| 	}
 | |
| 
 | |
| 	major, err := strconv.Atoi(parts[0])
 | |
| 	if err != nil {
 | |
| 		return version{}, fmt.Errorf("invalid major version: %s", parts[0])
 | |
| 	}
 | |
| 
 | |
| 	minor, err := strconv.Atoi(parts[1])
 | |
| 	if err != nil {
 | |
| 		return version{}, fmt.Errorf("invalid minor version: %s", parts[1])
 | |
| 	}
 | |
| 
 | |
| 	patch, err := strconv.Atoi(parts[2])
 | |
| 	if err != nil {
 | |
| 		return version{}, fmt.Errorf("invalid patch version: %s", parts[2])
 | |
| 	}
 | |
| 
 | |
| 	return version{Major: major, Minor: minor, Patch: patch}, nil
 | |
| }
 | |
| 
 | |
| func isIncremental(prev version, next version) bool {
 | |
| 	if next.Major < prev.Major {
 | |
| 		return false
 | |
| 	}
 | |
| 	if next.Major > prev.Major {
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	if next.Minor < prev.Minor {
 | |
| 		return false
 | |
| 	}
 | |
| 	if next.Minor > prev.Minor {
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	return next.Patch > prev.Patch
 | |
| }
 | |
| 
 | |
| func replaceRelatedWorkflowOrPluginInWorkflowNodes(nodes []*vo.Node, relatedWorkflows map[int64]entity.IDVersionPair, relatedPlugins map[int64]vo.PluginEntity) error {
 | |
| 	for _, node := range nodes {
 | |
| 		if node.Type == vo.BlockTypeBotSubWorkflow {
 | |
| 			workflowID, err := strconv.ParseInt(node.Data.Inputs.WorkflowID, 10, 64)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			if wf, ok := relatedWorkflows[workflowID]; ok {
 | |
| 				node.Data.Inputs.WorkflowID = strconv.FormatInt(wf.ID, 10)
 | |
| 				node.Data.Inputs.WorkflowVersion = wf.Version
 | |
| 			}
 | |
| 		}
 | |
| 		if node.Type == vo.BlockTypeBotAPI {
 | |
| 			apiParams := slices.ToMap(node.Data.Inputs.APIParams, func(e *vo.Param) (string, *vo.Param) {
 | |
| 				return e.Name, e
 | |
| 			})
 | |
| 			pluginIDParam, ok := apiParams["pluginID"]
 | |
| 			if !ok {
 | |
| 				return fmt.Errorf("plugin id param is not found")
 | |
| 			}
 | |
| 
 | |
| 			pID, err := strconv.ParseInt(pluginIDParam.Input.Value.Content.(string), 10, 64)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			pluginVersionParam, ok := apiParams["pluginVersion"]
 | |
| 			if !ok {
 | |
| 				return fmt.Errorf("plugin version param is not found")
 | |
| 			}
 | |
| 
 | |
| 			if refPlugin, ok := relatedPlugins[pID]; ok {
 | |
| 				pluginIDParam.Input.Value.Content = refPlugin.PluginID
 | |
| 				if refPlugin.PluginVersion != nil {
 | |
| 					pluginVersionParam.Input.Value.Content = *refPlugin.PluginVersion
 | |
| 				}
 | |
| 
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if node.Type == vo.BlockTypeBotLLM {
 | |
| 			if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
 | |
| 				for idx := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
 | |
| 					wf := node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList[idx]
 | |
| 					workflowID, err := strconv.ParseInt(wf.WorkflowID, 10, 64)
 | |
| 					if err != nil {
 | |
| 						return err
 | |
| 					}
 | |
| 					if refWf, ok := relatedWorkflows[workflowID]; ok {
 | |
| 						wf.WorkflowID = strconv.FormatInt(refWf.ID, 10)
 | |
| 						wf.WorkflowVersion = refWf.Version
 | |
| 					}
 | |
| 
 | |
| 				}
 | |
| 			}
 | |
| 			if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.PluginFCParam != nil {
 | |
| 				for idx := range node.Data.Inputs.FCParam.PluginFCParam.PluginList {
 | |
| 					pl := node.Data.Inputs.FCParam.PluginFCParam.PluginList[idx]
 | |
| 					pluginID, err := strconv.ParseInt(pl.PluginID, 10, 64)
 | |
| 					if err != nil {
 | |
| 						return err
 | |
| 					}
 | |
| 					if refPlugin, ok := relatedPlugins[pluginID]; ok {
 | |
| 						pl.PluginID = strconv.FormatInt(refPlugin.PluginID, 10)
 | |
| 						if refPlugin.PluginVersion != nil {
 | |
| 							pl.PluginVersion = *refPlugin.PluginVersion
 | |
| 						}
 | |
| 
 | |
| 					}
 | |
| 
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 		}
 | |
| 		if len(node.Blocks) > 0 {
 | |
| 			err := replaceRelatedWorkflowOrPluginInWorkflowNodes(node.Blocks, relatedWorkflows, relatedPlugins)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // entityNodeTypeToBlockType converts an entity.NodeType to the corresponding vo.BlockType.
 | |
| func entityNodeTypeToBlockType(nodeType entity.NodeType) (vo.BlockType, error) {
 | |
| 	switch nodeType {
 | |
| 	case entity.NodeTypeEntry:
 | |
| 		return vo.BlockTypeBotStart, nil
 | |
| 	case entity.NodeTypeExit:
 | |
| 		return vo.BlockTypeBotEnd, nil
 | |
| 	case entity.NodeTypeLLM:
 | |
| 		return vo.BlockTypeBotLLM, nil
 | |
| 	case entity.NodeTypePlugin:
 | |
| 		return vo.BlockTypeBotAPI, nil
 | |
| 	case entity.NodeTypeCodeRunner:
 | |
| 		return vo.BlockTypeBotCode, nil
 | |
| 	case entity.NodeTypeKnowledgeRetriever:
 | |
| 		return vo.BlockTypeBotDataset, nil
 | |
| 	case entity.NodeTypeSelector:
 | |
| 		return vo.BlockTypeCondition, nil
 | |
| 	case entity.NodeTypeSubWorkflow:
 | |
| 		return vo.BlockTypeBotSubWorkflow, nil
 | |
| 	case entity.NodeTypeDatabaseCustomSQL:
 | |
| 		return vo.BlockTypeDatabase, nil
 | |
| 	case entity.NodeTypeOutputEmitter:
 | |
| 		return vo.BlockTypeBotMessage, nil
 | |
| 	case entity.NodeTypeTextProcessor:
 | |
| 		return vo.BlockTypeBotText, nil
 | |
| 	case entity.NodeTypeQuestionAnswer:
 | |
| 		return vo.BlockTypeQuestion, nil
 | |
| 	case entity.NodeTypeBreak:
 | |
| 		return vo.BlockTypeBotBreak, nil
 | |
| 	case entity.NodeTypeVariableAssigner:
 | |
| 		return vo.BlockTypeBotAssignVariable, nil
 | |
| 	case entity.NodeTypeVariableAssignerWithinLoop:
 | |
| 		return vo.BlockTypeBotLoopSetVariable, nil
 | |
| 	case entity.NodeTypeLoop:
 | |
| 		return vo.BlockTypeBotLoop, nil
 | |
| 	case entity.NodeTypeIntentDetector:
 | |
| 		return vo.BlockTypeBotIntent, nil
 | |
| 	case entity.NodeTypeKnowledgeIndexer:
 | |
| 		return vo.BlockTypeBotDatasetWrite, nil
 | |
| 	case entity.NodeTypeBatch:
 | |
| 		return vo.BlockTypeBotBatch, nil
 | |
| 	case entity.NodeTypeContinue:
 | |
| 		return vo.BlockTypeBotContinue, nil
 | |
| 	case entity.NodeTypeInputReceiver:
 | |
| 		return vo.BlockTypeBotInput, nil
 | |
| 	case entity.NodeTypeDatabaseUpdate:
 | |
| 		return vo.BlockTypeDatabaseUpdate, nil
 | |
| 	case entity.NodeTypeDatabaseQuery:
 | |
| 		return vo.BlockTypeDatabaseSelect, nil
 | |
| 	case entity.NodeTypeDatabaseDelete:
 | |
| 		return vo.BlockTypeDatabaseDelete, nil
 | |
| 	case entity.NodeTypeHTTPRequester:
 | |
| 		return vo.BlockTypeBotHttp, nil
 | |
| 	case entity.NodeTypeDatabaseInsert:
 | |
| 		return vo.BlockTypeDatabaseInsert, nil
 | |
| 	case entity.NodeTypeVariableAggregator:
 | |
| 		return vo.BlockTypeBotVariableMerge, nil
 | |
| 	case entity.NodeTypeJsonSerialization:
 | |
| 		return vo.BlockTypeJsonSerialization, nil
 | |
| 	case entity.NodeTypeJsonDeserialization:
 | |
| 		return vo.BlockTypeJsonDeserialization, nil
 | |
| 	case entity.NodeTypeKnowledgeDeleter:
 | |
| 		return vo.BlockTypeBotDatasetDelete, nil
 | |
| 
 | |
| 	default:
 | |
| 		return "", vo.WrapError(errno.ErrSchemaConversionFail,
 | |
| 			fmt.Errorf("cannot map entity node type '%s' to a workflow.NodeTemplateType", nodeType))
 | |
| 	}
 | |
| }
 |