/* * 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 plugin import ( "context" "fmt" "strconv" "github.com/getkin/kin-openapi/openapi3" "golang.org/x/exp/maps" "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/schema" "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin" common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common" workflow3 "github.com/coze-dev/coze-studio/backend/api/model/workflow" "github.com/coze-dev/coze-studio/backend/application/base/pluginutil" "github.com/coze-dev/coze-studio/backend/domain/plugin/entity" "github.com/coze-dev/coze-studio/backend/domain/plugin/service" "github.com/coze-dev/coze-studio/backend/domain/workflow" crossplugin "github.com/coze-dev/coze-studio/backend/domain/workflow/crossdomain/plugin" entity2 "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/infra/contract/storage" "github.com/coze-dev/coze-studio/backend/pkg/errorx" "github.com/coze-dev/coze-studio/backend/pkg/lang/conv" "github.com/coze-dev/coze-studio/backend/pkg/lang/ptr" "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" ) type pluginService struct { client service.PluginService tos storage.Storage } func NewPluginService(client service.PluginService, tos storage.Storage) crossplugin.Service { return &pluginService{client: client, tos: tos} } type pluginInfo struct { *entity.PluginInfo LatestVersion *string } func (t *pluginService) getPluginsWithTools(ctx context.Context, pluginEntity *crossplugin.Entity, toolIDs []int64, isDraft bool) ( _ *pluginInfo, toolsInfo []*entity.ToolInfo, err error) { defer func() { if err != nil { err = vo.WrapIfNeeded(errno.ErrPluginAPIErr, err) } }() var pluginsInfo []*entity.PluginInfo var latestPluginInfo *entity.PluginInfo pluginID := pluginEntity.PluginID if isDraft { plugins, err := t.client.MGetDraftPlugins(ctx, []int64{pluginID}) if err != nil { return nil, nil, err } pluginsInfo = plugins } else if pluginEntity.PluginVersion == nil || (pluginEntity.PluginVersion != nil && *pluginEntity.PluginVersion == "") { plugins, err := t.client.MGetOnlinePlugins(ctx, []int64{pluginID}) if err != nil { return nil, nil, err } pluginsInfo = plugins } else { plugins, err := t.client.MGetVersionPlugins(ctx, []entity.VersionPlugin{ {PluginID: pluginID, Version: *pluginEntity.PluginVersion}, }) if err != nil { return nil, nil, err } pluginsInfo = plugins onlinePlugins, err := t.client.MGetOnlinePlugins(ctx, []int64{pluginID}) if err != nil { return nil, nil, err } for _, pi := range onlinePlugins { if pi.ID == pluginID { latestPluginInfo = pi break } } } var pInfo *entity.PluginInfo for _, p := range pluginsInfo { if p.ID == pluginID { pInfo = p break } } if pInfo == nil { return nil, nil, vo.NewError(errno.ErrPluginIDNotFound, errorx.KV("id", strconv.FormatInt(pluginID, 10))) } if isDraft { tools, err := t.client.MGetDraftTools(ctx, toolIDs) if err != nil { return nil, nil, err } toolsInfo = tools } else if pluginEntity.PluginVersion == nil || (pluginEntity.PluginVersion != nil && *pluginEntity.PluginVersion == "") { tools, err := t.client.MGetOnlineTools(ctx, toolIDs) if err != nil { return nil, nil, err } toolsInfo = tools } else { eVersionTools := slices.Transform(toolIDs, func(tid int64) entity.VersionTool { return entity.VersionTool{ ToolID: tid, Version: *pluginEntity.PluginVersion, } }) tools, err := t.client.MGetVersionTools(ctx, eVersionTools) if err != nil { return nil, nil, err } toolsInfo = tools } if latestPluginInfo != nil { return &pluginInfo{PluginInfo: pInfo, LatestVersion: latestPluginInfo.Version}, toolsInfo, nil } return &pluginInfo{PluginInfo: pInfo}, toolsInfo, nil } func (t *pluginService) GetPluginToolsInfo(ctx context.Context, req *crossplugin.ToolsInfoRequest) ( _ *crossplugin.ToolsInfoResponse, err error) { defer func() { if err != nil { err = vo.WrapIfNeeded(errno.ErrPluginAPIErr, err) } }() var toolsInfo []*entity.ToolInfo isDraft := req.IsDraft || (req.PluginEntity.PluginVersion != nil && *req.PluginEntity.PluginVersion == "0") pInfo, toolsInfo, err := t.getPluginsWithTools(ctx, &crossplugin.Entity{PluginID: req.PluginEntity.PluginID, PluginVersion: req.PluginEntity.PluginVersion}, req.ToolIDs, isDraft) if err != nil { return nil, err } url, err := t.tos.GetObjectUrl(ctx, pInfo.GetIconURI()) if err != nil { return nil, vo.WrapIfNeeded(errno.ErrTOSError, err) } response := &crossplugin.ToolsInfoResponse{ PluginID: pInfo.ID, SpaceID: pInfo.SpaceID, Version: pInfo.GetVersion(), PluginName: pInfo.GetName(), Description: pInfo.GetDesc(), IconURL: url, PluginType: int64(pInfo.PluginType), ToolInfoList: make(map[int64]crossplugin.ToolInfo), LatestVersion: pInfo.LatestVersion, IsOfficial: pInfo.IsOfficial(), AppID: pInfo.GetAPPID(), } for _, tf := range toolsInfo { inputs, err := tf.ToReqAPIParameter() if err != nil { return nil, err } outputs, err := tf.ToRespAPIParameter() if err != nil { return nil, err } toolExample := pInfo.GetToolExample(ctx, tf.GetName()) var ( requestExample string responseExample string ) if toolExample != nil { requestExample = toolExample.RequestExample responseExample = toolExample.ResponseExample } response.ToolInfoList[tf.ID] = crossplugin.ToolInfo{ ToolID: tf.ID, ToolName: tf.GetName(), Inputs: slices.Transform(inputs, toWorkflowAPIParameter), Outputs: slices.Transform(outputs, toWorkflowAPIParameter), Description: tf.GetDesc(), DebugExample: &crossplugin.DebugExample{ ReqExample: requestExample, RespExample: responseExample, }, } } return response, nil } func (t *pluginService) UnwrapArrayItemFieldsInVariable(v *vo.Variable) error { if v == nil { return nil } if v.Type == vo.VariableTypeObject { subVars, ok := v.Schema.([]*vo.Variable) if !ok { return nil } newSubVars := make([]*vo.Variable, 0, len(subVars)) for _, subVar := range subVars { if subVar.Name == "[Array Item]" { if err := t.UnwrapArrayItemFieldsInVariable(subVar); err != nil { return err } // If the array item is an object, append its children if subVar.Type == vo.VariableTypeObject { if innerSubVars, ok := subVar.Schema.([]*vo.Variable); ok { newSubVars = append(newSubVars, innerSubVars...) } } else { // If the array item is a primitive type, clear its name and append it subVar.Name = "" newSubVars = append(newSubVars, subVar) } } else { // For other sub-variables, recursively unwrap and append if err := t.UnwrapArrayItemFieldsInVariable(subVar); err != nil { return err } newSubVars = append(newSubVars, subVar) } } v.Schema = newSubVars } else if v.Type == vo.VariableTypeList { if v.Schema != nil { subVar, ok := v.Schema.(*vo.Variable) if !ok { return nil } if err := t.UnwrapArrayItemFieldsInVariable(subVar); err != nil { return err } // If the array item definition itself has "[Array Item]" name, clear it if subVar.Name == "[Array Item]" { subVar.Name = "" } v.Schema = subVar } } return nil } func (t *pluginService) GetPluginInvokableTools(ctx context.Context, req *crossplugin.ToolsInvokableRequest) ( _ map[int64]crossplugin.InvokableTool, err error) { defer func() { if err != nil { err = vo.WrapIfNeeded(errno.ErrPluginAPIErr, err) } }() var toolsInfo []*entity.ToolInfo isDraft := req.IsDraft || (req.PluginEntity.PluginVersion != nil && *req.PluginEntity.PluginVersion == "0") pInfo, toolsInfo, err := t.getPluginsWithTools(ctx, &crossplugin.Entity{ PluginID: req.PluginEntity.PluginID, PluginVersion: req.PluginEntity.PluginVersion, }, maps.Keys(req.ToolsInvokableInfo), isDraft) if err != nil { return nil, err } result := map[int64]crossplugin.InvokableTool{} for _, tf := range toolsInfo { tl := &pluginInvokeTool{ pluginEntity: crossplugin.Entity{ PluginID: pInfo.ID, PluginVersion: pInfo.Version, }, client: t.client, toolInfo: tf, IsDraft: isDraft, } if r, ok := req.ToolsInvokableInfo[tf.ID]; ok && (r.RequestAPIParametersConfig != nil && r.ResponseAPIParametersConfig != nil) { reqPluginCommonAPIParameters := slices.Transform(r.RequestAPIParametersConfig, toPluginCommonAPIParameter) respPluginCommonAPIParameters := slices.Transform(r.ResponseAPIParametersConfig, toPluginCommonAPIParameter) tl.toolOperation, err = pluginutil.APIParamsToOpenapiOperation(reqPluginCommonAPIParameters, respPluginCommonAPIParameters) if err != nil { return nil, err } tl.toolOperation.OperationID = tf.Operation.OperationID tl.toolOperation.Summary = tf.Operation.Summary } result[tf.ID] = tl } return result, nil } func (t *pluginService) ExecutePlugin(ctx context.Context, input map[string]any, pe *crossplugin.Entity, toolID int64, cfg crossplugin.ExecConfig) (map[string]any, error) { args, err := sonic.MarshalString(input) if err != nil { return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err) } var uID string if cfg.AgentID != nil { uID = cfg.ConnectorUID } else { uID = conv.Int64ToStr(cfg.Operator) } req := &service.ExecuteToolRequest{ UserID: uID, PluginID: pe.PluginID, ToolID: toolID, ExecScene: plugin.ExecSceneOfWorkflow, ArgumentsInJson: args, ExecDraftTool: pe.PluginVersion == nil || *pe.PluginVersion == "0", } execOpts := []entity.ExecuteToolOpt{ plugin.WithInvalidRespProcessStrategy(plugin.InvalidResponseProcessStrategyOfReturnDefault), } if pe.PluginVersion != nil { execOpts = append(execOpts, plugin.WithToolVersion(*pe.PluginVersion)) } r, err := t.client.ExecuteTool(ctx, req, execOpts...) if err != nil { if extra, ok := compose.IsInterruptRerunError(err); ok { pluginTIE, ok := extra.(*plugin.ToolInterruptEvent) if !ok { return nil, vo.WrapError(errno.ErrPluginAPIErr, fmt.Errorf("expects ToolInterruptEvent, got %T", extra)) } var eventType workflow3.EventType switch pluginTIE.Event { case plugin.InterruptEventTypeOfToolNeedOAuth: eventType = workflow3.EventType_WorkflowOauthPlugin default: return nil, vo.WrapError(errno.ErrPluginAPIErr, fmt.Errorf("unsupported interrupt event type: %s", pluginTIE.Event)) } id, err := workflow.GetRepository().GenID(ctx) if err != nil { return nil, vo.WrapError(errno.ErrIDGenError, err) } ie := &entity2.InterruptEvent{ ID: id, InterruptData: pluginTIE.ToolNeedOAuth.Message, EventType: eventType, } // temporarily replace interrupt with real error, until frontend can handle plugin oauth interrupt interruptData := ie.InterruptData return nil, vo.NewError(errno.ErrAuthorizationRequired, errorx.KV("extra", interruptData)) } return nil, err } var output map[string]any err = sonic.UnmarshalString(r.TrimmedResp, &output) if err != nil { return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err) } return output, nil } type pluginInvokeTool struct { pluginEntity crossplugin.Entity client service.PluginService toolInfo *entity.ToolInfo toolOperation *openapi3.Operation IsDraft bool } func (p *pluginInvokeTool) Info(ctx context.Context) (_ *schema.ToolInfo, err error) { defer func() { if err != nil { err = vo.WrapIfNeeded(errno.ErrPluginAPIErr, err) } }() var parameterInfo map[string]*schema.ParameterInfo if p.toolOperation != nil { parameterInfo, err = plugin.NewOpenapi3Operation(p.toolOperation).ToEinoSchemaParameterInfo(ctx) } else { parameterInfo, err = p.toolInfo.Operation.ToEinoSchemaParameterInfo(ctx) } if err != nil { return nil, err } return &schema.ToolInfo{ Name: p.toolInfo.GetName(), Desc: p.toolInfo.GetDesc(), ParamsOneOf: schema.NewParamsOneOfByParams(parameterInfo), }, nil } func (p *pluginInvokeTool) PluginInvoke(ctx context.Context, argumentsInJSON string, cfg crossplugin.ExecConfig) (string, error) { req := &service.ExecuteToolRequest{ UserID: conv.Int64ToStr(cfg.Operator), PluginID: p.pluginEntity.PluginID, ToolID: p.toolInfo.ID, ExecScene: plugin.ExecSceneOfWorkflow, ArgumentsInJson: argumentsInJSON, ExecDraftTool: p.IsDraft, } execOpts := []entity.ExecuteToolOpt{ plugin.WithInvalidRespProcessStrategy(plugin.InvalidResponseProcessStrategyOfReturnDefault), } if p.pluginEntity.PluginVersion != nil { execOpts = append(execOpts, plugin.WithToolVersion(*p.pluginEntity.PluginVersion)) } if p.toolOperation != nil { execOpts = append(execOpts, plugin.WithOpenapiOperation(plugin.NewOpenapi3Operation(p.toolOperation))) } r, err := p.client.ExecuteTool(ctx, req, execOpts...) if err != nil { if extra, ok := compose.IsInterruptRerunError(err); ok { pluginTIE, ok := extra.(*plugin.ToolInterruptEvent) if !ok { return "", vo.WrapError(errno.ErrPluginAPIErr, fmt.Errorf("expects ToolInterruptEvent, got %T", extra)) } var eventType workflow3.EventType switch pluginTIE.Event { case plugin.InterruptEventTypeOfToolNeedOAuth: eventType = workflow3.EventType_WorkflowOauthPlugin default: return "", vo.WrapError(errno.ErrPluginAPIErr, fmt.Errorf("unsupported interrupt event type: %s", pluginTIE.Event)) } id, err := workflow.GetRepository().GenID(ctx) if err != nil { return "", vo.WrapError(errno.ErrIDGenError, err) } ie := &entity2.InterruptEvent{ ID: id, InterruptData: pluginTIE.ToolNeedOAuth.Message, EventType: eventType, } tie := &entity2.ToolInterruptEvent{ ToolCallID: compose.GetToolCallID(ctx), ToolName: p.toolInfo.GetName(), InterruptEvent: ie, } // temporarily replace interrupt with real error, until frontend can handle plugin oauth interrupt _ = tie interruptData := ie.InterruptData return "", vo.NewError(errno.ErrAuthorizationRequired, errorx.KV("extra", interruptData)) } return "", err } return r.TrimmedResp, nil } func toPluginCommonAPIParameter(parameter *workflow3.APIParameter) *common.APIParameter { if parameter == nil { return nil } p := &common.APIParameter{ ID: parameter.ID, Name: parameter.Name, Desc: parameter.Desc, Type: common.ParameterType(parameter.Type), Location: common.ParameterLocation(parameter.Location), IsRequired: parameter.IsRequired, GlobalDefault: parameter.GlobalDefault, GlobalDisable: parameter.GlobalDisable, LocalDefault: parameter.LocalDefault, LocalDisable: parameter.LocalDisable, VariableRef: parameter.VariableRef, } if parameter.SubType != nil { p.SubType = ptr.Of(common.ParameterType(*parameter.SubType)) } if parameter.DefaultParamSource != nil { p.DefaultParamSource = ptr.Of(common.DefaultParamSource(*parameter.DefaultParamSource)) } if parameter.AssistType != nil { p.AssistType = ptr.Of(common.AssistParameterType(*parameter.AssistType)) } if len(parameter.SubParameters) > 0 { p.SubParameters = make([]*common.APIParameter, 0, len(parameter.SubParameters)) for _, subParam := range parameter.SubParameters { p.SubParameters = append(p.SubParameters, toPluginCommonAPIParameter(subParam)) } } return p } func toWorkflowAPIParameter(parameter *common.APIParameter) *workflow3.APIParameter { if parameter == nil { return nil } p := &workflow3.APIParameter{ ID: parameter.ID, Name: parameter.Name, Desc: parameter.Desc, Type: workflow3.ParameterType(parameter.Type), Location: workflow3.ParameterLocation(parameter.Location), IsRequired: parameter.IsRequired, GlobalDefault: parameter.GlobalDefault, GlobalDisable: parameter.GlobalDisable, LocalDefault: parameter.LocalDefault, LocalDisable: parameter.LocalDisable, VariableRef: parameter.VariableRef, } if parameter.SubType != nil { p.SubType = ptr.Of(workflow3.ParameterType(*parameter.SubType)) } if parameter.DefaultParamSource != nil { p.DefaultParamSource = ptr.Of(workflow3.DefaultParamSource(*parameter.DefaultParamSource)) } if parameter.AssistType != nil { p.AssistType = ptr.Of(workflow3.AssistParameterType(*parameter.AssistType)) } if len(parameter.SubParameters) > 0 { p.SubParameters = make([]*workflow3.APIParameter, 0, len(parameter.SubParameters)) for _, subParam := range parameter.SubParameters { p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(subParam)) } } return p }