feat: add schema check for workflow_list on release

This commit is contained in:
lvxinyu.1117
2025-07-31 12:41:11 +08:00
parent e678c24b0c
commit 0d5c330b51
4 changed files with 121 additions and 0 deletions

View File

@ -2121,6 +2121,11 @@ func (w *ApplicationService) ListWorkflow(ctx context.Context, req *workflow.Get
},
}
ww.CheckResult, err = GetWorkflowDomainSVC().WorkflowSchemaCheck(ctx, w, req.Checker)
if err != nil {
return nil, err
}
if qType == vo.FromDraft {
ww.UpdateTime = w.DraftMeta.Timestamp.Unix()
} else if qType == vo.FromLatestVersion || qType == vo.FromSpecificVersion {

View File

@ -38,6 +38,7 @@ type Service interface {
Publish(ctx context.Context, policy *vo.PublishPolicy) (err error)
UpdateMeta(ctx context.Context, id int64, metaUpdate *vo.MetaUpdate) (err error)
CopyWorkflow(ctx context.Context, workflowID int64, policy vo.CopyWorkflowPolicy) (*entity.Workflow, error)
WorkflowSchemaCheck(ctx context.Context, wf *entity.Workflow, checks []workflow.CheckType) ([]*workflow.CheckResult, error)
QueryNodeProperties(ctx context.Context, id int64) (map[string]*vo.NodeProperty, error) // only draft
ValidateTree(ctx context.Context, id int64, validateConfig vo.ValidateTreeConfig) ([]*workflow.ValidateTreeInfo, error)

View File

@ -1593,6 +1593,55 @@ func (i *impl) GetWorkflowDependenceResource(ctx context.Context, workflowID int
}
// checkBotAgentNodes checks for rules related to BotAgent.
// It returns an error with the reason if the check fails.
func (i *impl) checkBotAgentNodes(nodes []*vo.Node) error {
for _, node := range nodes {
if node.Type == vo.BlockTypeCreateConversation || node.Type == vo.BlockTypeConversationDelete || node.Type == vo.BlockTypeConversationUpdate || node.Type == vo.BlockTypeConversationList {
return errors.New("不支持在对话流内添加会话相关节点")
}
}
return nil
}
func (i *impl) WorkflowSchemaCheck(ctx context.Context, wf *entity.Workflow, checks []cloudworkflow.CheckType) ([]*cloudworkflow.CheckResult, error) {
if len(checks) == 0 {
return nil, nil
}
nodeList, err := GetAllNodesRecursively(ctx, wf, i.repo)
if err != nil {
return nil, err
}
checkResults := make([]*cloudworkflow.CheckResult, 0, len(checks))
for _, checkType := range checks {
var checkErr error
switch checkType {
case cloudworkflow.CheckType_BotAgent:
checkErr = i.checkBotAgentNodes(nodeList)
// TODO: Add other cases here for new check types
default:
continue
}
if checkErr != nil {
checkResults = append(checkResults, &cloudworkflow.CheckResult{
IsPass: false,
Reason: checkErr.Error(),
Type: checkType,
})
} else {
checkResults = append(checkResults, &cloudworkflow.CheckResult{
IsPass: true,
Type: checkType,
Reason: "",
})
}
}
return checkResults, nil
}
func (i *impl) MGet(ctx context.Context, policy *vo.MGetPolicy) ([]*entity.Workflow, int64, error) {
if policy.MetaOnly {
metas, total, err := i.repo.MGetMetas(ctx, &policy.MetaQuery)

View File

@ -23,12 +23,14 @@ import (
"strings"
cloudworkflow "github.com/coze-dev/coze-studio/backend/api/model/ocean/cloud/workflow"
"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"
"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/lang/ternary"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
@ -285,6 +287,70 @@ func replaceRelatedWorkflowOrPluginInWorkflowNodes(nodes []*vo.Node, relatedWork
return nil
}
func GetAllNodesRecursively(ctx context.Context, wfEntity *entity.Workflow, repo workflow.Repository) ([]*vo.Node, error) {
visited := make(map[string]struct{})
allNodes := make([]*vo.Node, 0)
err := getAllNodesRecursiveHelper(ctx, wfEntity, repo, visited, &allNodes)
return allNodes, err
}
func getAllNodesRecursiveHelper(ctx context.Context, wfEntity *entity.Workflow, repo workflow.Repository, visited map[string]struct{}, allNodes *[]*vo.Node) error {
visitedKey := fmt.Sprintf("%d:%s", wfEntity.ID, wfEntity.GetVersion())
if _, ok := visited[visitedKey]; ok {
return nil
}
visited[visitedKey] = struct{}{}
var canvas vo.Canvas
if err := sonic.UnmarshalString(wfEntity.Canvas, &canvas); err != nil {
return fmt.Errorf("failed to unmarshal canvas for workflow %d: %w", wfEntity.ID, err)
}
return collectNodes(ctx, canvas.Nodes, repo, visited, allNodes)
}
func collectNodes(ctx context.Context, nodes []*vo.Node, repo workflow.Repository, visited map[string]struct{}, allNodes *[]*vo.Node) error {
for _, node := range nodes {
if node == nil {
continue
}
*allNodes = append(*allNodes, node)
if node.Type == vo.BlockTypeBotSubWorkflow && node.Data != nil && node.Data.Inputs != nil {
workflowIDStr := node.Data.Inputs.WorkflowID
if workflowIDStr == "" {
continue
}
workflowID, err := strconv.ParseInt(workflowIDStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid workflow ID in sub-workflow node %s: %w", node.ID, err)
}
subWfEntity, err := repo.GetEntity(ctx, &vo.GetPolicy{
ID: workflowID,
QType: ternary.IFElse(len(node.Data.Inputs.WorkflowVersion) == 0, vo.FromDraft, vo.FromSpecificVersion),
Version: node.Data.Inputs.WorkflowVersion,
})
if err != nil {
return fmt.Errorf("failed to get sub-workflow entity %d: %w", workflowID, err)
}
if err := getAllNodesRecursiveHelper(ctx, subWfEntity, repo, visited, allNodes); err != nil {
return err
}
}
if len(node.Blocks) > 0 {
if err := collectNodes(ctx, node.Blocks, repo, visited, allNodes); 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 {