coze-studio/backend/domain/workflow/internal/nodes/httprequester/adapt.go

415 lines
12 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 httprequester
import (
"fmt"
"regexp"
"strings"
"github.com/cloudwego/eino/compose"
"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/convert"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
"github.com/coze-dev/coze-studio/backend/pkg/lang/crypto"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
)
var extractBracesRegexp = regexp.MustCompile(`\{\{(.*?)}}`)
func extractBracesContent(s string) []string {
matches := extractBracesRegexp.FindAllStringSubmatch(s, -1)
var result []string
for _, match := range matches {
if len(match) >= 2 {
result = append(result, match[1])
}
}
return result
}
type ImplicitNodeDependency struct {
NodeID string
FieldPath compose.FieldPath
TypeInfo *vo.TypeInfo
IsIntermediateVar bool
}
func extractImplicitDependency(node *vo.Node, canvas *vo.Canvas) ([]*ImplicitNodeDependency, error) {
dependencies := make([]*ImplicitNodeDependency, 0, len(canvas.Nodes))
url := node.Data.Inputs.APIInfo.URL
urlVars := extractBracesContent(url)
hasReferred := make(map[string]bool)
extractDependenciesFromVars := func(vars []string) error {
for _, v := range vars {
if strings.HasPrefix(v, "block_output_") {
paths := strings.Split(strings.TrimPrefix(v, "block_output_"), ".")
if len(paths) < 2 {
return fmt.Errorf("invalid block_output_ variable: %s", v)
}
if hasReferred[v] {
continue
}
hasReferred[v] = true
dependencies = append(dependencies, &ImplicitNodeDependency{
NodeID: paths[0],
FieldPath: paths[1:],
})
}
}
return nil
}
err := extractDependenciesFromVars(urlVars)
if err != nil {
return nil, err
}
if node.Data.Inputs.Body.BodyType == string(BodyTypeJSON) {
jsonVars := extractBracesContent(node.Data.Inputs.Body.BodyData.Json)
err = extractDependenciesFromVars(jsonVars)
if err != nil {
return nil, err
}
}
if node.Data.Inputs.Body.BodyType == string(BodyTypeRawText) {
rawTextVars := extractBracesContent(node.Data.Inputs.Body.BodyData.RawText)
err = extractDependenciesFromVars(rawTextVars)
if err != nil {
return nil, err
}
}
nodeID2ParentID := make(map[string]string)
for _, n := range canvas.Nodes {
if len(n.Blocks) > 0 {
for _, block := range n.Blocks {
nodeID2ParentID[block.ID] = n.ID
}
}
}
var nodeFinder func(nodes []*vo.Node, nodeID string) *vo.Node
nodeFinder = func(nodes []*vo.Node, nodeID string) *vo.Node {
for i := range nodes {
if nodes[i].ID == nodeID {
return nodes[i]
}
if len(nodes[i].Blocks) > 0 {
if n := nodeFinder(nodes[i].Blocks, nodeID); n != nil {
return n
}
}
}
return nil
}
for _, ds := range dependencies {
fNode := nodeFinder(canvas.Nodes, ds.NodeID)
if fNode == nil {
continue
}
intermediateVars := map[string]bool{}
tInfoMap := make(map[string]*vo.TypeInfo)
parentID, ok := nodeID2ParentID[node.ID]
if ok && parentID == fNode.ID {
// when referencing a composite node, the index field type is available by default
tInfoMap["index"] = &vo.TypeInfo{
Type: vo.DataTypeInteger,
}
for _, p := range fNode.Data.Inputs.InputParameters {
tInfo, err := convert.CanvasBlockInputToTypeInfo(p.Input)
if err != nil {
return nil, err
}
if tInfo.Type != vo.DataTypeArray {
return nil, fmt.Errorf("when referencing a composite node, the input parameter must be an array type")
}
tInfoMap[p.Name] = tInfo.ElemTypeInfo
}
if fNode.Data.Inputs.Loop != nil {
for _, vAny := range fNode.Data.Inputs.Loop.VariableParameters {
v, err := convert.ParseParam(vAny)
if err != nil {
return nil, err
}
tInfo, err := convert.CanvasBlockInputToTypeInfo(v.Input)
if err != nil {
return nil, err
}
tInfoMap[v.Name] = tInfo
intermediateVars[v.Name] = true
}
}
} else if entity.NodeTypeMetas[entity.IDStrToNodeType(fNode.Type)].IsComposite {
for _, vAny := range fNode.Data.Outputs {
v, err := convert.ParseParam(vAny)
if err != nil {
return nil, err
}
tInfo, err := convert.CanvasBlockInputToTypeInfo(v.Input)
if err != nil {
return nil, err
}
tInfoMap[v.Name] = tInfo
}
} else {
for _, vAny := range fNode.Data.Outputs {
v, err := vo.ParseVariable(vAny)
if err != nil {
return nil, err
}
tInfo, err := convert.CanvasVariableToTypeInfo(v)
if err != nil {
return nil, err
}
tInfoMap[v.Name] = tInfo
}
}
tInfo, ok := getTypeInfoByPath(ds.FieldPath[0], ds.FieldPath[1:], tInfoMap)
if !ok {
return nil, fmt.Errorf("cannot find type info for dependency: %s", ds.FieldPath)
}
ds.TypeInfo = tInfo
if len(intermediateVars) > 0 {
ds.IsIntermediateVar = intermediateVars[strings.Join(ds.FieldPath, ".")]
}
}
return dependencies, nil
}
func getTypeInfoByPath(root string, properties []string, tInfoMap map[string]*vo.TypeInfo) (*vo.TypeInfo, bool) {
if len(properties) == 0 {
if tInfo, ok := tInfoMap[root]; ok {
return tInfo, true
}
return nil, false
}
tInfo, ok := tInfoMap[root]
if !ok {
return nil, false
}
return getTypeInfoByPath(properties[0], properties[1:], tInfo.Properties)
}
var globalVariableRegex = regexp.MustCompile(`global_variable_\w+\s*\["(.*?)"]`)
func setHttpRequesterInputsForNodeSchema(n *vo.Node, ns *schema.NodeSchema, implicitNodeDependencies []*ImplicitNodeDependency) (err error) {
inputs := n.Data.Inputs
implicitPathVars := make(map[string]bool)
addImplicitVarsSources := func(prefix string, vars []string, parent *vo.Node) error {
for _, v := range vars {
if strings.HasPrefix(v, "block_output_") {
paths := strings.Split(strings.TrimPrefix(v, "block_output_"), ".")
if len(paths) < 2 {
return fmt.Errorf("invalid implicit var : %s", v)
}
for _, dep := range implicitNodeDependencies {
if dep.NodeID == paths[0] && strings.Join(dep.FieldPath, ".") == strings.Join(paths[1:], ".") {
pathValue := prefix + crypto.MD5HexValue(v)
if _, visited := implicitPathVars[pathValue]; visited {
continue
}
implicitPathVars[pathValue] = true
ns.SetInputType(pathValue, dep.TypeInfo)
filedInfo := &vo.FieldInfo{
Path: []string{pathValue},
Source: vo.FieldSource{
Ref: &vo.Reference{
FromNodeKey: vo.NodeKey(dep.NodeID),
FromPath: dep.FieldPath,
},
},
}
if dep.IsIntermediateVar && parent != nil {
filedInfo.Source.Ref.VariableType = ptr.Of(vo.ParentIntermediate)
}
ns.AddInputSource(filedInfo)
}
}
}
if strings.HasPrefix(v, "global_variable_") {
matches := globalVariableRegex.FindStringSubmatch(v)
if len(matches) < 2 {
continue
}
var varType vo.GlobalVarType
if strings.HasPrefix(v, string(vo.RefSourceTypeGlobalApp)) {
varType = vo.GlobalAPP
} else if strings.HasPrefix(v, string(vo.RefSourceTypeGlobalUser)) {
varType = vo.GlobalUser
} else if strings.HasPrefix(v, string(vo.RefSourceTypeGlobalSystem)) {
varType = vo.GlobalSystem
} else {
return fmt.Errorf("invalid global variable type: %s", v)
}
source := vo.FieldSource{
Ref: &vo.Reference{
VariableType: &varType,
FromPath: []string{matches[1]},
},
}
ns.AddInputSource(&vo.FieldInfo{
Path: []string{prefix + crypto.MD5HexValue(v)},
Source: source,
})
}
}
return nil
}
urlVars := extractBracesContent(inputs.APIInfo.URL)
err = addImplicitVarsSources("__apiInfo_url_", urlVars, n.Parent())
if err != nil {
return err
}
err = applyParamsToSchema(ns, "__headers_", inputs.Headers, n.Parent())
if err != nil {
return err
}
err = applyParamsToSchema(ns, "__params_", inputs.Params, n.Parent())
if err != nil {
return err
}
if inputs.Auth != nil && inputs.Auth.AuthOpen {
authData := inputs.Auth.AuthData
const bearerTokenKey = "__auth_authData_bearerTokenData_token"
if inputs.Auth.AuthType == "BEARER_AUTH" {
bearTokenParam := authData.BearerTokenData[0]
tInfo, err := convert.CanvasBlockInputToTypeInfo(bearTokenParam.Input)
if err != nil {
return err
}
ns.SetInputType(bearerTokenKey, tInfo)
sources, err := convert.CanvasBlockInputToFieldInfo(bearTokenParam.Input, compose.FieldPath{bearerTokenKey}, n.Parent())
if err != nil {
return err
}
ns.AddInputSource(sources...)
}
if inputs.Auth.AuthType == "CUSTOM_AUTH" {
const (
customDataDataKey = "__auth_authData_customData_data_Key"
customDataDataValue = "__auth_authData_customData_data_Value"
)
dataParams := authData.CustomData.Data
keyParam := dataParams[0]
keyTypeInfo, err := convert.CanvasBlockInputToTypeInfo(keyParam.Input)
if err != nil {
return err
}
ns.SetInputType(customDataDataKey, keyTypeInfo)
sources, err := convert.CanvasBlockInputToFieldInfo(keyParam.Input, compose.FieldPath{customDataDataKey}, n.Parent())
if err != nil {
return err
}
ns.AddInputSource(sources...)
valueParam := dataParams[1]
valueTypeInfo, err := convert.CanvasBlockInputToTypeInfo(valueParam.Input)
if err != nil {
return err
}
ns.SetInputType(customDataDataValue, valueTypeInfo)
sources, err = convert.CanvasBlockInputToFieldInfo(valueParam.Input, compose.FieldPath{customDataDataValue}, n.Parent())
if err != nil {
return err
}
ns.AddInputSource(sources...)
}
}
switch BodyType(inputs.Body.BodyType) {
case BodyTypeFormData:
err = applyParamsToSchema(ns, "__body_bodyData_formData_", inputs.Body.BodyData.FormData.Data, n.Parent())
if err != nil {
return err
}
case BodyTypeFormURLEncoded:
err = applyParamsToSchema(ns, "__body_bodyData_formURLEncoded_", inputs.Body.BodyData.FormURLEncoded, n.Parent())
if err != nil {
return err
}
case BodyTypeBinary:
const fileURLName = "__body_bodyData_binary_fileURL"
fileURLInput := inputs.Body.BodyData.Binary.FileURL
ns.SetInputType(fileURLName, &vo.TypeInfo{
Type: vo.DataTypeString,
})
sources, err := convert.CanvasBlockInputToFieldInfo(fileURLInput, compose.FieldPath{fileURLName}, n.Parent())
if err != nil {
return err
}
ns.AddInputSource(sources...)
case BodyTypeJSON:
jsonVars := extractBracesContent(inputs.Body.BodyData.Json)
err = addImplicitVarsSources("__body_bodyData_json_", jsonVars, n.Parent())
if err != nil {
return err
}
case BodyTypeRawText:
rawTextVars := extractBracesContent(inputs.Body.BodyData.RawText)
err = addImplicitVarsSources("__body_bodyData_rawText_", rawTextVars, n.Parent())
if err != nil {
return err
}
}
return nil
}
func applyParamsToSchema(ns *schema.NodeSchema, prefix string, params []*vo.Param, parentNode *vo.Node) error {
for i := range params {
param := params[i]
name := param.Name
tInfo, err := convert.CanvasBlockInputToTypeInfo(param.Input)
if err != nil {
return err
}
fieldName := prefix + crypto.MD5HexValue(name)
ns.SetInputType(fieldName, tInfo)
sources, err := convert.CanvasBlockInputToFieldInfo(param.Input, compose.FieldPath{fieldName}, parentNode)
if err != nil {
return err
}
ns.AddInputSource(sources...)
}
return nil
}