/* * 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 vo import ( "errors" "fmt" "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/schema" workflowModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/workflow" "github.com/coze-dev/coze-studio/backend/pkg/errorx" "github.com/coze-dev/coze-studio/backend/pkg/sonic" "github.com/coze-dev/coze-studio/backend/types/errno" ) type NodeKey string type FieldInfo struct { Path compose.FieldPath `json:"path"` Source FieldSource `json:"source"` } type Reference struct { FromNodeKey NodeKey `json:"from_node_key,omitempty"` FromPath compose.FieldPath `json:"from_path"` VariableType *GlobalVarType `json:"variable_type,omitempty"` } type FieldSource struct { Ref *Reference `json:"ref,omitempty"` Val any `json:"val,omitempty"` } type TypeInfo struct { Type DataType `json:"type"` ElemTypeInfo *TypeInfo `json:"elem_type_info,omitempty"` FileType *FileSubType `json:"file_type,omitempty"` Required bool `json:"required,omitempty"` Desc string `json:"desc,omitempty"` Properties map[string]*TypeInfo `json:"properties,omitempty"` } type NamedTypeInfo struct { Name string `json:"name"` Type DataType `json:"type"` ElemTypeInfo *NamedTypeInfo `json:"elem_type_info,omitempty"` FileType *FileSubType `json:"file_type,omitempty"` Required bool `json:"required,omitempty"` Desc string `json:"desc,omitempty"` Properties []*NamedTypeInfo `json:"properties,omitempty"` } type ErrorLevel string const ( LevelWarn ErrorLevel = "Warn" LevelError ErrorLevel = "Error" LevelCancel ErrorLevel = "pending" // forget about why it's called 'pending', somebody named it and it's now part of the protocol ) type WorkflowError interface { errorx.StatusError DebugURL() string Level() ErrorLevel OpenAPICode() int AppendDebug(exeID, spaceID, workflowID int64) WorkflowError ChangeErrLevel(newLevel ErrorLevel) WorkflowError } type wfErr struct { errorx.StatusError exeID int64 spaceID int64 workflowID int64 cause error } func (w *wfErr) DebugURL() string { if w.StatusError.Extra() == nil { return fmt.Sprintf(workflowModel.DebugURLTpl, w.exeID, w.spaceID, w.workflowID) } debugURL, ok := w.StatusError.Extra()["debug_url"] if ok { return debugURL } return fmt.Sprintf(workflowModel.DebugURLTpl, w.exeID, w.spaceID, w.workflowID) } func (w *wfErr) Level() ErrorLevel { if w.StatusError.Extra() == nil { return LevelError } level, ok := w.StatusError.Extra()["level"] if ok { return ErrorLevel(level) } return LevelError } func (w *wfErr) Error() string { if w.cause == nil { return w.StatusError.Error() } return fmt.Sprintf("%s, cause: %s", w.StatusError.Error(), w.cause.Error()) } func (w *wfErr) OpenAPICode() int { return errno.CodeForOpenAPI(w) } func (w *wfErr) AppendDebug(exeID, spaceID, workflowID int64) WorkflowError { w.exeID = exeID w.spaceID = spaceID w.workflowID = workflowID return w } func (w *wfErr) Unwrap() error { return w.cause } func (w *wfErr) ChangeErrLevel(newLevel ErrorLevel) WorkflowError { w.StatusError.Extra()["level"] = string(newLevel) return w } func NewError(code int, opts ...errorx.Option) WorkflowError { opts = append(opts, errorx.Extra("level", string(LevelError))) e := errorx.New(int32(code), opts...) var sErr errorx.StatusError _ = errors.As(e, &sErr) wfe := &wfErr{ StatusError: sErr, } return wfe } func WrapError(code int, err error, opts ...errorx.Option) WorkflowError { opts = append(opts, errorx.Extra("level", string(LevelError))) e := errorx.WrapByCode(err, int32(code), opts...) var sErr errorx.StatusError _ = errors.As(e, &sErr) wfe := &wfErr{ StatusError: sErr, cause: err, } return wfe } func WrapWithDebug(code int, err error, exeID, spaceID, workflowID int64, opts ...errorx.Option) WorkflowError { debugURL := fmt.Sprintf(workflowModel.DebugURLTpl, exeID, spaceID, workflowID) opts = append(opts, errorx.Extra("debug_url", debugURL)) return WrapError(code, err, opts...) } func NewWarn(code int, opts ...errorx.Option) WorkflowError { opts = append(opts, errorx.Extra("level", string(LevelWarn))) e := errorx.New(int32(code), opts...) var sErr errorx.StatusError _ = errors.As(e, &sErr) wfe := &wfErr{ StatusError: sErr, } return wfe } func WrapWarn(code int, err error, opts ...errorx.Option) WorkflowError { opts = append(opts, errorx.Extra("level", string(LevelWarn))) e := errorx.WrapByCode(err, int32(code), opts...) var sErr errorx.StatusError _ = errors.As(e, &sErr) wfe := &wfErr{ StatusError: sErr, cause: err, } return wfe } func WrapIfNeeded(code int, err error, opts ...errorx.Option) WorkflowError { var wfe WorkflowError if errors.As(err, &wfe) { return wfe } return WrapError(code, err, opts...) } var CancelErr = newCancel() func newCancel() WorkflowError { e := errorx.New(errno.ErrWorkflowCanceledByUser, errorx.Extra("level", string(LevelCancel))) var sErr errorx.StatusError _ = errors.As(e, &sErr) wfe := &wfErr{ StatusError: sErr, } return wfe } var NodeTimeoutErr = newNodeTimeout() func newNodeTimeout() WorkflowError { e := errorx.New(errno.ErrNodeTimeout, errorx.Extra("level", string(LevelError))) var sErr errorx.StatusError _ = errors.As(e, &sErr) wfe := &wfErr{ StatusError: sErr, } return wfe } var WorkflowTimeoutErr = newWorkflowTimeout() func newWorkflowTimeout() WorkflowError { e := errorx.New(errno.ErrWorkflowTimeout, errorx.Extra("level", string(LevelError))) var sErr errorx.StatusError _ = errors.As(e, &sErr) wfe := &wfErr{ StatusError: sErr, } return wfe } func UnwrapRootErr(err error) error { var ( rootE = err currentE error ) for { currentE = errors.Unwrap(rootE) if currentE == nil { break } rootE = currentE } return rootE } type DataType string const ( DataTypeString DataType = "string" // string DataTypeInteger DataType = "integer" // int64 DataTypeNumber DataType = "number" // float64 DataTypeBoolean DataType = "boolean" // bool DataTypeTime DataType = "time" // time.Time DataTypeObject DataType = "object" // map[string]any DataTypeArray DataType = "list" // []any DataTypeFile DataType = "file" // string (url) ) // Zero creates a zero value func (t *TypeInfo) Zero() any { switch t.Type { case DataTypeString: return "" case DataTypeInteger: return int64(0) case DataTypeNumber: return float64(0) case DataTypeBoolean: return false case DataTypeTime: return "" case DataTypeObject: var m map[string]any return m case DataTypeArray: var a []any return a case DataTypeFile: return "" default: panic("impossible") } } func (n *NamedTypeInfo) ToParameterInfo() (*schema.ParameterInfo, error) { param := &schema.ParameterInfo{ Type: convertDataType(n.Type), Desc: n.Desc, Required: n.Required, } if n.Type == DataTypeObject { param.SubParams = make(map[string]*schema.ParameterInfo, len(n.Properties)) for _, subT := range n.Properties { subParam, err := subT.ToParameterInfo() if err != nil { return nil, err } param.SubParams[subT.Name] = subParam } } else if n.Type == DataTypeArray { elemParam, err := n.ElemTypeInfo.ToParameterInfo() if err != nil { return nil, err } param.ElemInfo = elemParam } return param, nil } func (n *NamedTypeInfo) ToVariable() (*Variable, error) { variableType, err := convertVariableType(n.Type) if err != nil { return nil, err } v := &Variable{ Name: n.Name, Type: variableType, Required: n.Required, } if n.Type == DataTypeFile && n.FileType != nil { v.AssistType = toAssistType(*n.FileType) } if n.Type == DataTypeArray && n.ElemTypeInfo != nil { ele, err := n.ElemTypeInfo.ToVariable() if err != nil { return nil, err } v.Schema = ele } if n.Type == DataTypeObject && len(n.Properties) > 0 { varList := make([]*Variable, 0, len(n.Properties)) for _, p := range n.Properties { v, err := p.ToVariable() if err != nil { return nil, err } varList = append(varList, v) } v.Schema = varList } return v, nil } func toAssistType(f FileSubType) AssistType { switch f { case FileTypeDefault: return AssistTypeDefault case FileTypeImage: return AssistTypeImage case FileTypeSVG: return AssistTypeSvg case FileTypeAudio: return AssistTypeAudio case FileTypeVideo: return AssistTypeVideo case FileTypeDocument: return AssistTypeDoc case FileTypePPT: return AssistTypePPT case FileTypeExcel: return AssistTypeExcel case FileTypeTxt: return AssistTypeTXT case FileTypeCode: return AssistTypeCode case FileTypeZip: return AssistTypeZip default: return AssistTypeNotSet } } func convertVariableType(d DataType) (VariableType, error) { switch d { case DataTypeString, DataTypeTime, DataTypeFile: return VariableTypeString, nil case DataTypeNumber: return VariableTypeFloat, nil case DataTypeInteger: return VariableTypeInteger, nil case DataTypeBoolean: return VariableTypeBoolean, nil case DataTypeObject: return VariableTypeObject, nil case DataTypeArray: return VariableTypeList, nil default: return "", fmt.Errorf("unknown variable type: %v", d) } } func convertDataType(d DataType) schema.DataType { switch d { case DataTypeString, DataTypeTime, DataTypeFile: return schema.String case DataTypeNumber: return schema.Number case DataTypeInteger: return schema.Integer case DataTypeBoolean: return schema.Boolean case DataTypeObject: return schema.Object case DataTypeArray: return schema.Array default: panic("unknown data type") } } func TypeInfoToJSONSchema(tis map[string]*TypeInfo, structName *string) (string, error) { schema_ := map[string]any{ "type": "object", "properties": make(map[string]any), "required": []string{}, } if structName != nil { schema_["title"] = *structName } properties := schema_["properties"].(map[string]any) for key, typeInfo := range tis { if typeInfo == nil { continue } sc, err := typeInfoToJSONSchema(typeInfo) if err != nil { return "", err } properties[key] = sc if typeInfo.Required { schema_["required"] = append(schema_["required"].([]string), key) } } jsonBytes, err := sonic.Marshal(schema_) if err != nil { return "", err } return string(jsonBytes), nil } func typeInfoToJSONSchema(info *TypeInfo) (map[string]interface{}, error) { sc := make(map[string]interface{}) switch info.Type { case DataTypeString: sc["type"] = "string" case DataTypeInteger: sc["type"] = "integer" case DataTypeNumber: sc["type"] = "number" case DataTypeBoolean: sc["type"] = "boolean" case DataTypeTime: sc["type"] = "string" sc["format"] = "date-time" case DataTypeObject: sc["type"] = "object" case DataTypeArray: sc["type"] = "array" case DataTypeFile: sc["type"] = "string" if info.FileType != nil { sc["contentMediaType"] = string(*info.FileType) } default: return nil, fmt.Errorf("impossible") } if info.Desc != "" { sc["description"] = info.Desc } if info.Type == DataTypeArray && info.ElemTypeInfo != nil { itemsSchema, err := typeInfoToJSONSchema(info.ElemTypeInfo) if err != nil { return nil, fmt.Errorf("failed to convert array element type: %v", err) } sc["items"] = itemsSchema } if info.Type == DataTypeObject && info.Properties != nil { properties := make(map[string]interface{}) required := make([]string, 0) for name, propInfo := range info.Properties { propSchema, err := typeInfoToJSONSchema(propInfo) if err != nil { return nil, fmt.Errorf("failed to convert property %s: %v", name, err) } properties[name] = propSchema if propInfo.Required { required = append(required, name) } } sc["properties"] = properties if len(required) > 0 { sc["required"] = required } } return sc, nil } type FileSubType string const ( FileTypeDefault FileSubType = "default" FileTypeImage FileSubType = "image" FileTypeSVG FileSubType = "svg" FileTypeAudio FileSubType = "audio" FileTypeVideo FileSubType = "video" FileTypeVoice FileSubType = "voice" FileTypeDocument FileSubType = "doc" FileTypePPT FileSubType = "ppt" FileTypeExcel FileSubType = "excel" FileTypeTxt FileSubType = "txt" FileTypeCode FileSubType = "code" FileTypeZip FileSubType = "zip" ) type NodeProperty struct { Type string IsEnableChatHistory bool IsEnableUserQuery bool IsRefGlobalVariable bool SubWorkflow map[string]*NodeProperty } func (f *FieldInfo) IsRefGlobalVariable() bool { if f.Source.Ref != nil && f.Source.Ref.VariableType != nil { return *f.Source.Ref.VariableType == GlobalUser || *f.Source.Ref.VariableType == GlobalSystem || *f.Source.Ref.VariableType == GlobalAPP } return false } func ParseVariable(v any) (*Variable, error) { if va, ok := v.(*Variable); ok { return va, nil } m, ok := v.(map[string]any) if !ok { return nil, fmt.Errorf("invalid content type: %T when parse Variable", v) } marshaled, err := sonic.Marshal(m) if err != nil { return nil, err } p := &Variable{} if err := sonic.Unmarshal(marshaled, p); err != nil { return nil, err } return p, nil } type GlobalVarType string const ( ParentIntermediate GlobalVarType = "parent_intermediate" GlobalUser GlobalVarType = "global_user" GlobalSystem GlobalVarType = "global_system" GlobalAPP GlobalVarType = "global_app" )