refactor(workflow): Move the plugin component in the Workflow package into the common crossdomain package (#717)

This commit is contained in:
Ryo
2025-08-13 11:06:53 +08:00
committed by GitHub
parent b38ab95623
commit 99c759addc
47 changed files with 1330 additions and 1407 deletions

View File

@@ -20,6 +20,7 @@ import (
"context"
eino "github.com/cloudwego/eino/components/model"
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/modelmgr"
"github.com/coze-dev/coze-studio/backend/infra/contract/modelmgr"
)

View File

@@ -19,6 +19,8 @@ package plugin
import (
"context"
"github.com/cloudwego/eino/schema"
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
)
@@ -35,6 +37,14 @@ type PluginService interface {
PublishAPPPlugins(ctx context.Context, req *model.PublishAPPPluginsRequest) (resp *model.PublishAPPPluginsResponse, err error)
GetAPPAllPlugins(ctx context.Context, appID int64) (plugins []*model.PluginInfo, err error)
MGetVersionTools(ctx context.Context, versionTools []model.VersionTool) (tools []*model.ToolInfo, err error)
GetPluginToolsInfo(ctx context.Context, req *model.ToolsInfoRequest) (*model.ToolsInfoResponse, error)
GetPluginInvokableTools(ctx context.Context, req *model.ToolsInvokableRequest) (map[int64]InvokableTool, error)
ExecutePlugin(ctx context.Context, input map[string]any, pe *model.PluginEntity, toolID int64, cfg model.ExecuteConfig) (map[string]any, error)
}
type InvokableTool interface {
Info(ctx context.Context) (*schema.ToolInfo, error)
PluginInvoke(ctx context.Context, argumentsInJSON string, cfg model.ExecuteConfig) (string, error)
}
var defaultSVC PluginService

View File

@@ -0,0 +1,323 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: plugin.go
//
// Generated by this command:
//
// mockgen -destination pluginmock/plugin_mock.go --package pluginmock -source plugin.go
//
// Package pluginmock is a generated GoMock package.
package pluginmock
import (
context "context"
reflect "reflect"
schema "github.com/cloudwego/eino/schema"
plugin "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
plugin0 "github.com/coze-dev/coze-studio/backend/crossdomain/contract/plugin"
gomock "go.uber.org/mock/gomock"
)
// MockPluginService is a mock of PluginService interface.
type MockPluginService struct {
ctrl *gomock.Controller
recorder *MockPluginServiceMockRecorder
isgomock struct{}
}
// MockPluginServiceMockRecorder is the mock recorder for MockPluginService.
type MockPluginServiceMockRecorder struct {
mock *MockPluginService
}
// NewMockPluginService creates a new mock instance.
func NewMockPluginService(ctrl *gomock.Controller) *MockPluginService {
mock := &MockPluginService{ctrl: ctrl}
mock.recorder = &MockPluginServiceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPluginService) EXPECT() *MockPluginServiceMockRecorder {
return m.recorder
}
// BindAgentTools mocks base method.
func (m *MockPluginService) BindAgentTools(ctx context.Context, agentID int64, toolIDs []int64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BindAgentTools", ctx, agentID, toolIDs)
ret0, _ := ret[0].(error)
return ret0
}
// BindAgentTools indicates an expected call of BindAgentTools.
func (mr *MockPluginServiceMockRecorder) BindAgentTools(ctx, agentID, toolIDs any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindAgentTools", reflect.TypeOf((*MockPluginService)(nil).BindAgentTools), ctx, agentID, toolIDs)
}
// DeleteDraftPlugin mocks base method.
func (m *MockPluginService) DeleteDraftPlugin(ctx context.Context, PluginID int64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDraftPlugin", ctx, PluginID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDraftPlugin indicates an expected call of DeleteDraftPlugin.
func (mr *MockPluginServiceMockRecorder) DeleteDraftPlugin(ctx, PluginID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDraftPlugin", reflect.TypeOf((*MockPluginService)(nil).DeleteDraftPlugin), ctx, PluginID)
}
// DuplicateDraftAgentTools mocks base method.
func (m *MockPluginService) DuplicateDraftAgentTools(ctx context.Context, fromAgentID, toAgentID int64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DuplicateDraftAgentTools", ctx, fromAgentID, toAgentID)
ret0, _ := ret[0].(error)
return ret0
}
// DuplicateDraftAgentTools indicates an expected call of DuplicateDraftAgentTools.
func (mr *MockPluginServiceMockRecorder) DuplicateDraftAgentTools(ctx, fromAgentID, toAgentID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DuplicateDraftAgentTools", reflect.TypeOf((*MockPluginService)(nil).DuplicateDraftAgentTools), ctx, fromAgentID, toAgentID)
}
// ExecutePlugin mocks base method.
func (m *MockPluginService) ExecutePlugin(ctx context.Context, input map[string]any, pe *plugin.PluginEntity, toolID int64, cfg plugin.ExecuteConfig) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecutePlugin", ctx, input, pe, toolID, cfg)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecutePlugin indicates an expected call of ExecutePlugin.
func (mr *MockPluginServiceMockRecorder) ExecutePlugin(ctx, input, pe, toolID, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecutePlugin", reflect.TypeOf((*MockPluginService)(nil).ExecutePlugin), ctx, input, pe, toolID, cfg)
}
// ExecuteTool mocks base method.
func (m *MockPluginService) ExecuteTool(ctx context.Context, req *plugin.ExecuteToolRequest, opts ...plugin.ExecuteToolOpt) (*plugin.ExecuteToolResponse, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, req}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecuteTool", varargs...)
ret0, _ := ret[0].(*plugin.ExecuteToolResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteTool indicates an expected call of ExecuteTool.
func (mr *MockPluginServiceMockRecorder) ExecuteTool(ctx, req any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, req}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteTool", reflect.TypeOf((*MockPluginService)(nil).ExecuteTool), varargs...)
}
// GetAPPAllPlugins mocks base method.
func (m *MockPluginService) GetAPPAllPlugins(ctx context.Context, appID int64) ([]*plugin.PluginInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAPPAllPlugins", ctx, appID)
ret0, _ := ret[0].([]*plugin.PluginInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAPPAllPlugins indicates an expected call of GetAPPAllPlugins.
func (mr *MockPluginServiceMockRecorder) GetAPPAllPlugins(ctx, appID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPPAllPlugins", reflect.TypeOf((*MockPluginService)(nil).GetAPPAllPlugins), ctx, appID)
}
// GetPluginInvokableTools mocks base method.
func (m *MockPluginService) GetPluginInvokableTools(ctx context.Context, req *plugin.ToolsInvokableRequest) (map[int64]plugin0.InvokableTool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPluginInvokableTools", ctx, req)
ret0, _ := ret[0].(map[int64]plugin0.InvokableTool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPluginInvokableTools indicates an expected call of GetPluginInvokableTools.
func (mr *MockPluginServiceMockRecorder) GetPluginInvokableTools(ctx, req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginInvokableTools", reflect.TypeOf((*MockPluginService)(nil).GetPluginInvokableTools), ctx, req)
}
// GetPluginToolsInfo mocks base method.
func (m *MockPluginService) GetPluginToolsInfo(ctx context.Context, req *plugin.ToolsInfoRequest) (*plugin.ToolsInfoResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPluginToolsInfo", ctx, req)
ret0, _ := ret[0].(*plugin.ToolsInfoResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPluginToolsInfo indicates an expected call of GetPluginToolsInfo.
func (mr *MockPluginServiceMockRecorder) GetPluginToolsInfo(ctx, req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginToolsInfo", reflect.TypeOf((*MockPluginService)(nil).GetPluginToolsInfo), ctx, req)
}
// MGetAgentTools mocks base method.
func (m *MockPluginService) MGetAgentTools(ctx context.Context, req *plugin.MGetAgentToolsRequest) ([]*plugin.ToolInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MGetAgentTools", ctx, req)
ret0, _ := ret[0].([]*plugin.ToolInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// MGetAgentTools indicates an expected call of MGetAgentTools.
func (mr *MockPluginServiceMockRecorder) MGetAgentTools(ctx, req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MGetAgentTools", reflect.TypeOf((*MockPluginService)(nil).MGetAgentTools), ctx, req)
}
// MGetPluginLatestVersion mocks base method.
func (m *MockPluginService) MGetPluginLatestVersion(ctx context.Context, pluginIDs []int64) (*plugin.MGetPluginLatestVersionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MGetPluginLatestVersion", ctx, pluginIDs)
ret0, _ := ret[0].(*plugin.MGetPluginLatestVersionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// MGetPluginLatestVersion indicates an expected call of MGetPluginLatestVersion.
func (mr *MockPluginServiceMockRecorder) MGetPluginLatestVersion(ctx, pluginIDs any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MGetPluginLatestVersion", reflect.TypeOf((*MockPluginService)(nil).MGetPluginLatestVersion), ctx, pluginIDs)
}
// MGetVersionPlugins mocks base method.
func (m *MockPluginService) MGetVersionPlugins(ctx context.Context, versionPlugins []plugin.VersionPlugin) ([]*plugin.PluginInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MGetVersionPlugins", ctx, versionPlugins)
ret0, _ := ret[0].([]*plugin.PluginInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// MGetVersionPlugins indicates an expected call of MGetVersionPlugins.
func (mr *MockPluginServiceMockRecorder) MGetVersionPlugins(ctx, versionPlugins any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MGetVersionPlugins", reflect.TypeOf((*MockPluginService)(nil).MGetVersionPlugins), ctx, versionPlugins)
}
// MGetVersionTools mocks base method.
func (m *MockPluginService) MGetVersionTools(ctx context.Context, versionTools []plugin.VersionTool) ([]*plugin.ToolInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MGetVersionTools", ctx, versionTools)
ret0, _ := ret[0].([]*plugin.ToolInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// MGetVersionTools indicates an expected call of MGetVersionTools.
func (mr *MockPluginServiceMockRecorder) MGetVersionTools(ctx, versionTools any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MGetVersionTools", reflect.TypeOf((*MockPluginService)(nil).MGetVersionTools), ctx, versionTools)
}
// PublishAPPPlugins mocks base method.
func (m *MockPluginService) PublishAPPPlugins(ctx context.Context, req *plugin.PublishAPPPluginsRequest) (*plugin.PublishAPPPluginsResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PublishAPPPlugins", ctx, req)
ret0, _ := ret[0].(*plugin.PublishAPPPluginsResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PublishAPPPlugins indicates an expected call of PublishAPPPlugins.
func (mr *MockPluginServiceMockRecorder) PublishAPPPlugins(ctx, req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishAPPPlugins", reflect.TypeOf((*MockPluginService)(nil).PublishAPPPlugins), ctx, req)
}
// PublishAgentTools mocks base method.
func (m *MockPluginService) PublishAgentTools(ctx context.Context, agentID int64, agentVersion string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PublishAgentTools", ctx, agentID, agentVersion)
ret0, _ := ret[0].(error)
return ret0
}
// PublishAgentTools indicates an expected call of PublishAgentTools.
func (mr *MockPluginServiceMockRecorder) PublishAgentTools(ctx, agentID, agentVersion any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishAgentTools", reflect.TypeOf((*MockPluginService)(nil).PublishAgentTools), ctx, agentID, agentVersion)
}
// PublishPlugin mocks base method.
func (m *MockPluginService) PublishPlugin(ctx context.Context, req *plugin.PublishPluginRequest) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PublishPlugin", ctx, req)
ret0, _ := ret[0].(error)
return ret0
}
// PublishPlugin indicates an expected call of PublishPlugin.
func (mr *MockPluginServiceMockRecorder) PublishPlugin(ctx, req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishPlugin", reflect.TypeOf((*MockPluginService)(nil).PublishPlugin), ctx, req)
}
// MockInvokableTool is a mock of InvokableTool interface.
type MockInvokableTool struct {
ctrl *gomock.Controller
recorder *MockInvokableToolMockRecorder
isgomock struct{}
}
// MockInvokableToolMockRecorder is the mock recorder for MockInvokableTool.
type MockInvokableToolMockRecorder struct {
mock *MockInvokableTool
}
// NewMockInvokableTool creates a new mock instance.
func NewMockInvokableTool(ctrl *gomock.Controller) *MockInvokableTool {
mock := &MockInvokableTool{ctrl: ctrl}
mock.recorder = &MockInvokableToolMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInvokableTool) EXPECT() *MockInvokableToolMockRecorder {
return m.recorder
}
// Info mocks base method.
func (m *MockInvokableTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Info", ctx)
ret0, _ := ret[0].(*schema.ToolInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Info indicates an expected call of Info.
func (mr *MockInvokableToolMockRecorder) Info(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockInvokableTool)(nil).Info), ctx)
}
// PluginInvoke mocks base method.
func (m *MockInvokableTool) PluginInvoke(ctx context.Context, argumentsInJSON string, cfg plugin.ExecuteConfig) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PluginInvoke", ctx, argumentsInJSON, cfg)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PluginInvoke indicates an expected call of PluginInvoke.
func (mr *MockInvokableToolMockRecorder) PluginInvoke(ctx, argumentsInJSON, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginInvoke", reflect.TypeOf((*MockInvokableTool)(nil).PluginInvoke), ctx, argumentsInJSON, cfg)
}

View File

@@ -23,6 +23,7 @@ import (
einoCompose "github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
"github.com/coze-dev/coze-studio/backend/domain/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
workflowEntity "github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
@@ -38,13 +39,13 @@ type Workflow interface {
allInterruptEvents map[string]*workflowEntity.ToolInterruptEvent) einoCompose.Option
ReleaseApplicationWorkflows(ctx context.Context, appID int64, config *ReleaseWorkflowConfig) ([]*vo.ValidateIssue, error)
GetWorkflowIDsByAppID(ctx context.Context, appID int64) ([]int64, error)
SyncExecuteWorkflow(ctx context.Context, config vo.ExecuteConfig, input map[string]any) (*workflowEntity.WorkflowExecution, vo.TerminatePlan, error)
WithExecuteConfig(cfg vo.ExecuteConfig) einoCompose.Option
SyncExecuteWorkflow(ctx context.Context, config model.ExecuteConfig, input map[string]any) (*workflowEntity.WorkflowExecution, vo.TerminatePlan, error)
WithExecuteConfig(cfg model.ExecuteConfig) einoCompose.Option
WithMessagePipe() (compose.Option, *schema.StreamReader[*entity.Message])
}
type ExecuteConfig = vo.ExecuteConfig
type ExecuteMode = vo.ExecuteMode
type ExecuteConfig = model.ExecuteConfig
type ExecuteMode = model.ExecuteMode
type NodeType = entity.NodeType
type WorkflowMessage = entity.Message
@@ -59,14 +60,14 @@ const (
ExecuteModeNodeDebug ExecuteMode = "node_debug"
)
type TaskType = vo.TaskType
type TaskType = model.TaskType
const (
TaskTypeForeground TaskType = "foreground"
TaskTypeBackground TaskType = "background"
)
type BizType = vo.BizType
type BizType = model.BizType
const (
BizTypeAgent BizType = "agent"

View File

@@ -21,6 +21,7 @@ import (
"fmt"
eino "github.com/cloudwego/eino/components/model"
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/modelmgr"
crossmodelmgr "github.com/coze-dev/coze-studio/backend/crossdomain/contract/modelmgr"
"github.com/coze-dev/coze-studio/backend/infra/contract/chatmodel"

View File

@@ -18,23 +18,45 @@ package plugin
import (
"context"
"fmt"
"strconv"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
"github.com/getkin/kin-openapi/openapi3"
"golang.org/x/exp/maps"
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
"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"
crossplugin "github.com/coze-dev/coze-studio/backend/crossdomain/contract/plugin"
"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
"github.com/coze-dev/coze-studio/backend/domain/plugin/service"
plugin "github.com/coze-dev/coze-studio/backend/domain/plugin/service"
"github.com/coze-dev/coze-studio/backend/domain/workflow"
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"
)
var defaultSVC crossplugin.PluginService
type impl struct {
DomainSVC plugin.PluginService
tos storage.Storage
}
func InitDomainService(c plugin.PluginService) crossplugin.PluginService {
func InitDomainService(c plugin.PluginService, tos storage.Storage) crossplugin.PluginService {
defaultSVC = &impl{
DomainSVC: c,
tos: tos,
}
return defaultSVC
@@ -105,3 +127,479 @@ func (s *impl) GetAPPAllPlugins(ctx context.Context, appID int64) (plugins []*mo
return plugins, nil
}
type pluginInfo struct {
*entity.PluginInfo
LatestVersion *string
}
func (s *impl) getPluginsWithTools(ctx context.Context, pluginEntity *model.PluginEntity, 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 := s.DomainSVC.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 := s.DomainSVC.MGetOnlinePlugins(ctx, []int64{pluginID})
if err != nil {
return nil, nil, err
}
pluginsInfo = plugins
} else {
plugins, err := s.DomainSVC.MGetVersionPlugins(ctx, []entity.VersionPlugin{
{PluginID: pluginID, Version: *pluginEntity.PluginVersion},
})
if err != nil {
return nil, nil, err
}
pluginsInfo = plugins
onlinePlugins, err := s.DomainSVC.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 := s.DomainSVC.MGetDraftTools(ctx, toolIDs)
if err != nil {
return nil, nil, err
}
toolsInfo = tools
} else if pluginEntity.PluginVersion == nil || (pluginEntity.PluginVersion != nil && *pluginEntity.PluginVersion == "") {
tools, err := s.DomainSVC.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 := s.DomainSVC.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 (s *impl) GetPluginToolsInfo(ctx context.Context, req *model.ToolsInfoRequest) (
_ *model.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 := s.getPluginsWithTools(ctx, &model.PluginEntity{PluginID: req.PluginEntity.PluginID, PluginVersion: req.PluginEntity.PluginVersion}, req.ToolIDs, isDraft)
if err != nil {
return nil, err
}
url, err := s.tos.GetObjectUrl(ctx, pInfo.GetIconURI())
if err != nil {
return nil, vo.WrapIfNeeded(errno.ErrTOSError, err)
}
response := &model.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]model.ToolInfoW),
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] = model.ToolInfoW{
ToolID: tf.ID,
ToolName: tf.GetName(),
Inputs: slices.Transform(inputs, toWorkflowAPIParameter),
Outputs: slices.Transform(outputs, toWorkflowAPIParameter),
Description: tf.GetDesc(),
DebugExample: &model.DebugExample{
ReqExample: requestExample,
RespExample: responseExample,
},
}
}
return response, nil
}
func (s *impl) GetPluginInvokableTools(ctx context.Context, req *model.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 := s.getPluginsWithTools(ctx, &model.PluginEntity{
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: model.PluginEntity{
PluginID: pInfo.ID,
PluginVersion: pInfo.Version,
},
client: s.DomainSVC,
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 (s *impl) ExecutePlugin(ctx context.Context, input map[string]any, pe *model.PluginEntity,
toolID int64, cfg model.ExecuteConfig) (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: model.ExecSceneOfWorkflow,
ArgumentsInJson: args,
ExecDraftTool: pe.PluginVersion == nil || *pe.PluginVersion == "0",
}
execOpts := []entity.ExecuteToolOpt{
model.WithInvalidRespProcessStrategy(model.InvalidResponseProcessStrategyOfReturnDefault),
}
if pe.PluginVersion != nil {
execOpts = append(execOpts, model.WithToolVersion(*pe.PluginVersion))
}
r, err := s.DomainSVC.ExecuteTool(ctx, req, execOpts...)
if err != nil {
if extra, ok := compose.IsInterruptRerunError(err); ok {
pluginTIE, ok := extra.(*model.ToolInterruptEvent)
if !ok {
return nil, vo.WrapError(errno.ErrPluginAPIErr, fmt.Errorf("expects ToolInterruptEvent, got %T", extra))
}
var eventType workflow3.EventType
switch pluginTIE.Event {
case model.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 model.PluginEntity
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 = model.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 model.ExecuteConfig) (string, error) {
req := &service.ExecuteToolRequest{
UserID: conv.Int64ToStr(cfg.Operator),
PluginID: p.pluginEntity.PluginID,
ToolID: p.toolInfo.ID,
ExecScene: model.ExecSceneOfWorkflow,
ArgumentsInJson: argumentsInJSON,
ExecDraftTool: p.IsDraft,
}
execOpts := []entity.ExecuteToolOpt{
model.WithInvalidRespProcessStrategy(model.InvalidResponseProcessStrategyOfReturnDefault),
}
if p.pluginEntity.PluginVersion != nil {
execOpts = append(execOpts, model.WithToolVersion(*p.pluginEntity.PluginVersion))
}
if p.toolOperation != nil {
execOpts = append(execOpts, model.WithOpenapiOperation(model.NewOpenapi3Operation(p.toolOperation)))
}
r, err := p.client.ExecuteTool(ctx, req, execOpts...)
if err != nil {
if extra, ok := compose.IsInterruptRerunError(err); ok {
pluginTIE, ok := extra.(*model.ToolInterruptEvent)
if !ok {
return "", vo.WrapError(errno.ErrPluginAPIErr, fmt.Errorf("expects ToolInterruptEvent, got %T", extra))
}
var eventType workflow3.EventType
switch pluginTIE.Event {
case model.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))
}
// Check if it's an array that needs unwrapping.
if parameter.Type == common.ParameterType_Array && len(parameter.SubParameters) == 1 && parameter.SubParameters[0].Name == "[Array Item]" {
arrayItem := parameter.SubParameters[0]
p.SubType = ptr.Of(workflow3.ParameterType(arrayItem.Type))
// If the "[Array Item]" is an object, its sub-parameters become the array's sub-parameters.
if arrayItem.Type == common.ParameterType_Object {
p.SubParameters = make([]*workflow3.APIParameter, 0, len(arrayItem.SubParameters))
for _, subParam := range arrayItem.SubParameters {
p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(subParam))
}
} else {
// The array's SubType is the Type of the "[Array Item]".
p.SubParameters = make([]*workflow3.APIParameter, 0, 1)
p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(arrayItem))
p.SubParameters[0].Name = "" // Remove the "[Array Item]" name.
}
} else if len(parameter.SubParameters) > 0 { // A simple object or a non-wrapped array.
p.SubParameters = make([]*workflow3.APIParameter, 0, len(parameter.SubParameters))
for _, subParam := range parameter.SubParameters {
p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(subParam))
}
}
return p
}

View File

@@ -23,6 +23,7 @@ import (
einoCompose "github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
crossworkflow "github.com/coze-dev/coze-studio/backend/crossdomain/contract/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
@@ -67,11 +68,11 @@ func (i *impl) ReleaseApplicationWorkflows(ctx context.Context, appID int64, con
func (i *impl) WithResumeToolWorkflow(resumingEvent *workflowEntity.ToolInterruptEvent, resumeData string, allInterruptEvents map[string]*workflowEntity.ToolInterruptEvent) einoCompose.Option {
return i.DomainSVC.WithResumeToolWorkflow(resumingEvent, resumeData, allInterruptEvents)
}
func (i *impl) SyncExecuteWorkflow(ctx context.Context, config vo.ExecuteConfig, input map[string]any) (*workflowEntity.WorkflowExecution, vo.TerminatePlan, error) {
func (i *impl) SyncExecuteWorkflow(ctx context.Context, config model.ExecuteConfig, input map[string]any) (*workflowEntity.WorkflowExecution, vo.TerminatePlan, error) {
return i.DomainSVC.SyncExecute(ctx, config, input)
}
func (i *impl) WithExecuteConfig(cfg vo.ExecuteConfig) einoCompose.Option {
func (i *impl) WithExecuteConfig(cfg model.ExecuteConfig) einoCompose.Option {
return i.DomainSVC.WithExecuteConfig(cfg)
}

View File

@@ -1,533 +0,0 @@
/*
* 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) 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))
}
// Check if it's an array that needs unwrapping.
if parameter.Type == common.ParameterType_Array && len(parameter.SubParameters) == 1 && parameter.SubParameters[0].Name == "[Array Item]" {
arrayItem := parameter.SubParameters[0]
p.SubType = ptr.Of(workflow3.ParameterType(arrayItem.Type))
// If the "[Array Item]" is an object, its sub-parameters become the array's sub-parameters.
if arrayItem.Type == common.ParameterType_Object {
p.SubParameters = make([]*workflow3.APIParameter, 0, len(arrayItem.SubParameters))
for _, subParam := range arrayItem.SubParameters {
p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(subParam))
}
} else {
// The array's SubType is the Type of the "[Array Item]".
p.SubParameters = make([]*workflow3.APIParameter, 0, 1)
p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(arrayItem))
p.SubParameters[0].Name = "" // Remove the "[Array Item]" name.
}
} else if len(parameter.SubParameters) > 0 { // A simple object or a non-wrapped array.
p.SubParameters = make([]*workflow3.APIParameter, 0, len(parameter.SubParameters))
for _, subParam := range parameter.SubParameters {
p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(subParam))
}
}
return p
}

View File

@@ -1,189 +0,0 @@
/*
* 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 (
"testing"
"github.com/stretchr/testify/assert"
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/pkg/lang/ptr"
)
func TestToWorkflowAPIParameter(t *testing.T) {
cases := []struct {
name string
param *common.APIParameter
expected *workflow3.APIParameter
}{
{
name: "nil parameter",
param: nil,
expected: nil,
},
{
name: "simple string parameter",
param: &common.APIParameter{
Name: "prompt",
Type: common.ParameterType_String,
Desc: "User's prompt",
},
expected: &workflow3.APIParameter{
Name: "prompt",
Type: workflow3.ParameterType_String,
Desc: "User's prompt",
},
},
{
name: "simple object parameter",
param: &common.APIParameter{
Name: "user_info",
Type: common.ParameterType_Object,
SubParameters: []*common.APIParameter{
{
Name: "name",
Type: common.ParameterType_String,
},
{
Name: "age",
Type: common.ParameterType_Number,
},
},
},
expected: &workflow3.APIParameter{
Name: "user_info",
Type: workflow3.ParameterType_Object,
SubParameters: []*workflow3.APIParameter{
{
Name: "name",
Type: workflow3.ParameterType_String,
},
{
Name: "age",
Type: workflow3.ParameterType_Number,
},
},
},
},
{
name: "array of strings",
param: &common.APIParameter{
Name: "tags",
Type: common.ParameterType_Array,
SubParameters: []*common.APIParameter{
{
Name: "[Array Item]",
Type: common.ParameterType_String,
},
},
},
expected: &workflow3.APIParameter{
Name: "tags",
Type: workflow3.ParameterType_Array,
SubType: ptr.Of(workflow3.ParameterType_String),
SubParameters: []*workflow3.APIParameter{
{
Type: workflow3.ParameterType_String,
},
},
},
},
{
name: "array of objects",
param: &common.APIParameter{
Name: "users",
Type: common.ParameterType_Array,
SubParameters: []*common.APIParameter{
{
Name: "[Array Item]",
Type: common.ParameterType_Object,
SubParameters: []*common.APIParameter{
{
Name: "name",
Type: common.ParameterType_String,
},
{
Name: "id",
Type: common.ParameterType_Number,
},
},
},
},
},
expected: &workflow3.APIParameter{
Name: "users",
Type: workflow3.ParameterType_Array,
SubType: ptr.Of(workflow3.ParameterType_Object),
SubParameters: []*workflow3.APIParameter{
{
Name: "name",
Type: workflow3.ParameterType_String,
},
{
Name: "id",
Type: workflow3.ParameterType_Number,
},
},
},
},
{
name: "array of array of strings",
param: &common.APIParameter{
Name: "matrix",
Type: common.ParameterType_Array,
SubParameters: []*common.APIParameter{
{
Name: "[Array Item]",
Type: common.ParameterType_Array,
SubParameters: []*common.APIParameter{
{
Name: "[Array Item]",
Type: common.ParameterType_String,
},
},
},
},
},
expected: &workflow3.APIParameter{
Name: "matrix",
Type: workflow3.ParameterType_Array,
SubType: ptr.Of(workflow3.ParameterType_Array),
SubParameters: []*workflow3.APIParameter{
{
Name: "", // Name is cleared
Type: workflow3.ParameterType_Array,
SubType: ptr.Of(workflow3.ParameterType_String),
SubParameters: []*workflow3.APIParameter{
{
Type: workflow3.ParameterType_String,
},
},
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
actual := toWorkflowAPIParameter(tc.param)
assert.Equal(t, tc.expected, actual)
})
}
}