From c7118efbdc2927ee3a00f018f7b4e38c9368d5a5 Mon Sep 17 00:00:00 2001 From: biao Date: Sun, 1 Mar 2026 23:12:12 +0800 Subject: [PATCH] fix(workflow): avoid panic when object schema is map --- .../internal/canvas/convert/type_convert.go | 129 +++++++++++++-- .../canvas/convert/type_convert_test.go | 147 ++++++++++++++++++ 2 files changed, 267 insertions(+), 9 deletions(-) create mode 100644 backend/domain/workflow/internal/canvas/convert/type_convert_test.go diff --git a/backend/domain/workflow/internal/canvas/convert/type_convert.go b/backend/domain/workflow/internal/canvas/convert/type_convert.go index 8535aa486d..7607cc0554 100644 --- a/backend/domain/workflow/internal/canvas/convert/type_convert.go +++ b/backend/domain/workflow/internal/canvas/convert/type_convert.go @@ -18,6 +18,7 @@ package convert import ( "fmt" + "sort" "strconv" "strings" @@ -63,7 +64,11 @@ func CanvasVariableToTypeInfo(v *vo.Variable) (*vo.TypeInfo, error) { tInfo.Type = vo.DataTypeObject tInfo.Properties = make(map[string]*vo.TypeInfo) if v.Schema != nil { - for _, subVAny := range v.Schema.([]any) { + subVariables, err := normalizeObjectSchemaItems(v.Schema) + if err != nil { + return nil, err + } + for _, subVAny := range subVariables { subV, err := vo.ParseVariable(subVAny) if err != nil { return nil, err @@ -134,7 +139,14 @@ func CanvasBlockInputToTypeInfo(b *vo.BlockInput) (tInfo *vo.TypeInfo, err error tInfo.Type = vo.DataTypeObject tInfo.Properties = make(map[string]*vo.TypeInfo) if b.Schema != nil { - for _, subVAny := range b.Schema.([]any) { + subItems, err := normalizeObjectSchemaItems(b.Schema) + if err != nil { + return nil, err + } + if b.Value == nil { + break + } + for _, subVAny := range subItems { if b.Value.Type == vo.BlockInputValueTypeRef { subV, err := vo.ParseVariable(subVAny) if err != nil { @@ -193,9 +205,9 @@ func CanvasBlockInputToFieldInfo(b *vo.BlockInput, path einoCompose.FieldPath, p return nil, fmt.Errorf("input %v has no schema, type= %s", path, b.Type) } - paramList, ok := sc.([]any) - if !ok { - return nil, fmt.Errorf("input %v schema not []any, type= %T", path, sc) + paramList, err := normalizeObjectSchemaItems(sc) + if err != nil { + return nil, fmt.Errorf("input %v schema invalid, err=%w", path, err) } for i := range paramList { @@ -259,7 +271,11 @@ func CanvasBlockInputToFieldInfo(b *vo.BlockInput, path einoCompose.FieldPath, p FileNames: make([]string, 0, len(filenames)), } for _, filename := range filenames { - fileExtra.FileNames = append(fileExtra.FileNames, filename.(string)) + filenameStr, ok := filename.(string) + if !ok { + return nil, fmt.Errorf("invalid filename type: %T", filename) + } + fileExtra.FileNames = append(fileExtra.FileNames, filenameStr) } } @@ -416,6 +432,90 @@ func ParseParam(v any) (*vo.Param, error) { return p, nil } +func normalizeObjectSchemaItems(sc any) ([]any, error) { + switch v := sc.(type) { + case nil: + return nil, nil + case []any: + return v, nil + case []*vo.Variable: + items := make([]any, 0, len(v)) + for _, item := range v { + items = append(items, item) + } + return items, nil + case []*vo.Param: + items := make([]any, 0, len(v)) + for _, item := range v { + items = append(items, item) + } + return items, nil + case map[string]any: + if nested, ok := v["schema"]; ok { + if typeStr, ok := asVariableTypeString(v["type"]); ok && typeStr == string(vo.VariableTypeObject) { + return normalizeObjectSchemaItems(nested) + } + } + + keys := make([]string, 0, len(v)) + for k := range v { + keys = append(keys, k) + } + sort.Strings(keys) + + items := make([]any, 0, len(keys)) + for _, key := range keys { + items = append(items, withSchemaItemName(v[key], key)) + } + return items, nil + default: + return nil, fmt.Errorf("unsupported object schema type: %T", sc) + } +} + +func withSchemaItemName(v any, name string) any { + switch item := v.(type) { + case map[string]any: + if _, ok := item["name"]; ok { + return item + } + + copied := make(map[string]any, len(item)+1) + for k, val := range item { + copied[k] = val + } + copied["name"] = name + return copied + case *vo.Variable: + if item == nil || len(item.Name) > 0 { + return item + } + cloned := *item + cloned.Name = name + return &cloned + case *vo.Param: + if item == nil || len(item.Name) > 0 { + return item + } + cloned := *item + cloned.Name = name + return &cloned + default: + return v + } +} + +func asVariableTypeString(v any) (string, bool) { + switch t := v.(type) { + case string: + return t, true + case vo.VariableType: + return string(t), true + default: + return "", false + } +} + func CanvasBlockInputRefToFieldSource(r *vo.BlockInputReference) (*vo.FieldSource, error) { switch r.Source { case vo.RefSourceTypeBlockOutput: @@ -602,8 +702,15 @@ func BlockInputToNamedTypeInfo(name string, b *vo.BlockInput) (*vo.NamedTypeInfo case vo.VariableTypeObject: tInfo.Type = vo.DataTypeObject if b.Schema != nil { - tInfo.Properties = make([]*vo.NamedTypeInfo, 0, len(b.Schema.([]any))) - for _, subVAny := range b.Schema.([]any) { + subItems, err := normalizeObjectSchemaItems(b.Schema) + if err != nil { + return nil, err + } + tInfo.Properties = make([]*vo.NamedTypeInfo, 0, len(subItems)) + if b.Value == nil { + break + } + for _, subVAny := range subItems { if b.Value.Type == vo.BlockInputValueTypeRef { subV, err := vo.ParseVariable(subVAny) if err != nil { @@ -678,8 +785,12 @@ func VariableToNamedTypeInfo(v *vo.Variable) (*vo.NamedTypeInfo, error) { case vo.VariableTypeObject: nInfo.Type = vo.DataTypeObject if v.Schema != nil { + subVariables, err := normalizeObjectSchemaItems(v.Schema) + if err != nil { + return nil, err + } nInfo.Properties = make([]*vo.NamedTypeInfo, 0) - for _, subVAny := range v.Schema.([]any) { + for _, subVAny := range subVariables { subV, err := vo.ParseVariable(subVAny) if err != nil { return nil, err diff --git a/backend/domain/workflow/internal/canvas/convert/type_convert_test.go b/backend/domain/workflow/internal/canvas/convert/type_convert_test.go new file mode 100644 index 0000000000..42124a1982 --- /dev/null +++ b/backend/domain/workflow/internal/canvas/convert/type_convert_test.go @@ -0,0 +1,147 @@ +/* + * 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 convert + +import ( + "testing" + + einoCompose "github.com/cloudwego/eino/compose" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo" +) + +func TestCanvasBlockInputToTypeInfo_ObjectSchemaMap(t *testing.T) { + b := &vo.BlockInput{ + Type: vo.VariableTypeObject, + Schema: map[string]any{ + "foo": map[string]any{ + "type": vo.VariableTypeString, + }, + "bar": map[string]any{ + "type": vo.VariableTypeInteger, + }, + }, + Value: &vo.BlockInputValue{ + Type: vo.BlockInputValueTypeRef, + }, + } + + tInfo, err := CanvasBlockInputToTypeInfo(b) + require.NoError(t, err) + require.NotNil(t, tInfo) + + assert.Equal(t, vo.DataTypeObject, tInfo.Type) + require.Len(t, tInfo.Properties, 2) + assert.Equal(t, vo.DataTypeString, tInfo.Properties["foo"].Type) + assert.Equal(t, vo.DataTypeInteger, tInfo.Properties["bar"].Type) +} + +func TestCanvasBlockInputToTypeInfo_ObjectSchemaWrappedMap(t *testing.T) { + b := &vo.BlockInput{ + Type: vo.VariableTypeObject, + Schema: map[string]any{ + "type": vo.VariableTypeObject, + "schema": []any{ + map[string]any{ + "name": "foo", + "type": vo.VariableTypeString, + }, + }, + }, + Value: &vo.BlockInputValue{ + Type: vo.BlockInputValueTypeRef, + }, + } + + tInfo, err := CanvasBlockInputToTypeInfo(b) + require.NoError(t, err) + require.NotNil(t, tInfo) + + assert.Equal(t, vo.DataTypeObject, tInfo.Type) + require.Len(t, tInfo.Properties, 1) + assert.Equal(t, vo.DataTypeString, tInfo.Properties["foo"].Type) +} + +func TestCanvasBlockInputToFieldInfo_ObjectRefWithMapSchema(t *testing.T) { + b := &vo.BlockInput{ + Type: vo.VariableTypeObject, + Schema: map[string]any{ + "foo": map[string]any{ + "input": map[string]any{ + "type": vo.VariableTypeString, + "value": map[string]any{ + "type": vo.BlockInputValueTypeLiteral, + "content": "abc", + }, + }, + }, + }, + Value: &vo.BlockInputValue{ + Type: vo.BlockInputValueTypeObjectRef, + }, + } + + sources, err := CanvasBlockInputToFieldInfo(b, einoCompose.FieldPath{"root"}, nil) + require.NoError(t, err) + require.Len(t, sources, 1) + + assert.Equal(t, einoCompose.FieldPath{"root", "foo"}, sources[0].Path) + assert.Equal(t, "abc", sources[0].Source.Val) +} + +func TestVariableToNamedTypeInfo_ObjectSchemaMap(t *testing.T) { + v := &vo.Variable{ + Name: "obj", + Type: vo.VariableTypeObject, + Schema: map[string]any{ + "foo": map[string]any{ + "type": vo.VariableTypeString, + }, + }, + } + + nInfo, err := VariableToNamedTypeInfo(v) + require.NoError(t, err) + require.NotNil(t, nInfo) + require.Len(t, nInfo.Properties, 1) + + assert.Equal(t, "foo", nInfo.Properties[0].Name) + assert.Equal(t, vo.DataTypeString, nInfo.Properties[0].Type) +} + +func TestCanvasBlockInputToFieldInfo_ListFileMetaInvalidFilenameType(t *testing.T) { + b := &vo.BlockInput{ + Type: vo.VariableTypeList, + Schema: map[string]any{ + "type": vo.VariableTypeString, + "assistType": vo.AssistTypeImage, + }, + Value: &vo.BlockInputValue{ + Type: vo.BlockInputValueTypeLiteral, + Content: []any{"a"}, + RawMeta: map[string]any{ + "fileName": []any{1}, + }, + }, + } + + _, err := CanvasBlockInputToFieldInfo(b, einoCompose.FieldPath{"files"}, nil) + require.Error(t, err) + require.ErrorContains(t, err, "invalid filename type") +}