939 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			939 lines
		
	
	
		
			25 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 service
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/bytedance/sonic"
 | 
						|
	"github.com/getkin/kin-openapi/openapi3"
 | 
						|
	"gopkg.in/yaml.v3"
 | 
						|
 | 
						|
	model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
 | 
						|
	searchModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/search"
 | 
						|
	"github.com/coze-dev/coze-studio/backend/api/model/plugin_develop_common"
 | 
						|
	common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop_common"
 | 
						|
	resCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common"
 | 
						|
	"github.com/coze-dev/coze-studio/backend/crossdomain/contract/crosssearch"
 | 
						|
	"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
 | 
						|
	"github.com/coze-dev/coze-studio/backend/domain/plugin/internal/openapi"
 | 
						|
	"github.com/coze-dev/coze-studio/backend/domain/plugin/repository"
 | 
						|
	"github.com/coze-dev/coze-studio/backend/pkg/errorx"
 | 
						|
	"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/logs"
 | 
						|
	"github.com/coze-dev/coze-studio/backend/types/errno"
 | 
						|
)
 | 
						|
 | 
						|
func (p *pluginServiceImpl) CreateDraftPlugin(ctx context.Context, req *CreateDraftPluginRequest) (pluginID int64, err error) {
 | 
						|
	mf := entity.NewDefaultPluginManifest()
 | 
						|
	mf.CommonParams = map[model.HTTPParamLocation][]*plugin_develop_common.CommonParamSchema{}
 | 
						|
	mf.NameForHuman = req.Name
 | 
						|
	mf.NameForModel = req.Name
 | 
						|
	mf.DescriptionForHuman = req.Desc
 | 
						|
	mf.DescriptionForModel = req.Desc
 | 
						|
	mf.API.Type, _ = model.ToPluginType(req.PluginType)
 | 
						|
	mf.LogoURL = req.IconURI
 | 
						|
 | 
						|
	authV2, err := req.AuthInfo.toAuthV2()
 | 
						|
	if err != nil {
 | 
						|
		return 0, err
 | 
						|
	}
 | 
						|
	mf.Auth = authV2
 | 
						|
 | 
						|
	for loc, params := range req.CommonParams {
 | 
						|
		location, ok := model.ToHTTPParamLocation(loc)
 | 
						|
		if !ok {
 | 
						|
			return 0, fmt.Errorf("invalid location '%s'", loc.String())
 | 
						|
		}
 | 
						|
		for _, param := range params {
 | 
						|
			mf.CommonParams[location] = append(mf.CommonParams[location],
 | 
						|
				&plugin_develop_common.CommonParamSchema{
 | 
						|
					Name:  param.Name,
 | 
						|
					Value: param.Value,
 | 
						|
				})
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	doc := entity.NewDefaultOpenapiDoc()
 | 
						|
	doc.Servers = append(doc.Servers, &openapi3.Server{
 | 
						|
		URL: req.ServerURL,
 | 
						|
	})
 | 
						|
	doc.Info.Title = req.Name
 | 
						|
	doc.Info.Description = req.Desc
 | 
						|
 | 
						|
	err = doc.Validate(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return 0, err
 | 
						|
	}
 | 
						|
	err = mf.Validate(false)
 | 
						|
	if err != nil {
 | 
						|
		return 0, err
 | 
						|
	}
 | 
						|
 | 
						|
	pl := entity.NewPluginInfo(&model.PluginInfo{
 | 
						|
		IconURI:     ptr.Of(req.IconURI),
 | 
						|
		SpaceID:     req.SpaceID,
 | 
						|
		ServerURL:   ptr.Of(req.ServerURL),
 | 
						|
		DeveloperID: req.DeveloperID,
 | 
						|
		APPID:       req.ProjectID,
 | 
						|
		PluginType:  req.PluginType,
 | 
						|
		Manifest:    mf,
 | 
						|
		OpenapiDoc:  doc,
 | 
						|
	})
 | 
						|
 | 
						|
	pluginID, err = p.pluginRepo.CreateDraftPlugin(ctx, pl)
 | 
						|
	if err != nil {
 | 
						|
		return 0, errorx.Wrapf(err, "CreateDraftPlugin failed")
 | 
						|
	}
 | 
						|
 | 
						|
	return pluginID, nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) GetDraftPlugin(ctx context.Context, pluginID int64) (plugin *entity.PluginInfo, err error) {
 | 
						|
	pl, exist, err := p.pluginRepo.GetDraftPlugin(ctx, pluginID)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errorx.Wrapf(err, "GetDraftPlugin failed, pluginID=%d", pluginID)
 | 
						|
	}
 | 
						|
	if !exist {
 | 
						|
		return nil, errorx.New(errno.ErrPluginRecordNotFound)
 | 
						|
	}
 | 
						|
 | 
						|
	return pl, nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) MGetDraftPlugins(ctx context.Context, pluginIDs []int64) (plugins []*entity.PluginInfo, err error) {
 | 
						|
	plugins, err = p.pluginRepo.MGetDraftPlugins(ctx, pluginIDs)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return plugins, nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) ListDraftPlugins(ctx context.Context, req *ListDraftPluginsRequest) (resp *ListDraftPluginsResponse, err error) {
 | 
						|
	if req.PageInfo.Name == nil || *req.PageInfo.Name == "" {
 | 
						|
		res, err := p.pluginRepo.ListDraftPlugins(ctx, &repository.ListDraftPluginsRequest{
 | 
						|
			SpaceID:  req.SpaceID,
 | 
						|
			APPID:    req.APPID,
 | 
						|
			PageInfo: req.PageInfo,
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			return nil, errorx.Wrapf(err, "ListDraftPlugins failed, spaceID=%d, appID=%d", req.SpaceID, req.APPID)
 | 
						|
		}
 | 
						|
 | 
						|
		return &ListDraftPluginsResponse{
 | 
						|
			Plugins: res.Plugins,
 | 
						|
			Total:   res.Total,
 | 
						|
		}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	res, err := crosssearch.DefaultSVC().SearchResources(ctx, &searchModel.SearchResourcesRequest{
 | 
						|
		SpaceID:  req.SpaceID,
 | 
						|
		APPID:    req.APPID,
 | 
						|
		Name:     *req.PageInfo.Name,
 | 
						|
		OrderAsc: false,
 | 
						|
		ResTypeFilter: []resCommon.ResType{
 | 
						|
			resCommon.ResType_Plugin,
 | 
						|
		},
 | 
						|
		OrderFiledName: func() string {
 | 
						|
			if req.PageInfo.SortBy == nil || *req.PageInfo.SortBy != entity.SortByCreatedAt {
 | 
						|
				return searchModel.FieldOfUpdateTime
 | 
						|
			}
 | 
						|
			return searchModel.FieldOfCreateTime
 | 
						|
		}(),
 | 
						|
		Page:  ptr.Of(int32(req.PageInfo.Page)),
 | 
						|
		Limit: int32(req.PageInfo.Size),
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return nil, errorx.Wrapf(err, "SearchResources failed, spaceID=%d, appID=%d", req.SpaceID, req.APPID)
 | 
						|
	}
 | 
						|
 | 
						|
	plugins := make([]*entity.PluginInfo, 0, len(res.Data))
 | 
						|
	for _, pl := range res.Data {
 | 
						|
		draftPlugin, exist, err := p.pluginRepo.GetDraftPlugin(ctx, pl.ResID)
 | 
						|
		if err != nil {
 | 
						|
			return nil, errorx.Wrapf(err, "GetDraftPlugin failed, pluginID=%d", pl.ResID)
 | 
						|
		}
 | 
						|
		if !exist {
 | 
						|
			logs.CtxWarnf(ctx, "draft plugin not exist, pluginID=%d", pl.ResID)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		plugins = append(plugins, draftPlugin)
 | 
						|
	}
 | 
						|
 | 
						|
	total := int64(0)
 | 
						|
	if res.TotalHits != nil {
 | 
						|
		total = *res.TotalHits
 | 
						|
	}
 | 
						|
 | 
						|
	return &ListDraftPluginsResponse{
 | 
						|
		Plugins: plugins,
 | 
						|
		Total:   total,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) CreateDraftPluginWithCode(ctx context.Context, req *CreateDraftPluginWithCodeRequest) (resp *CreateDraftPluginWithCodeResponse, err error) {
 | 
						|
	err = req.OpenapiDoc.Validate(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	err = req.Manifest.Validate(false)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	res, err := p.pluginRepo.CreateDraftPluginWithCode(ctx, &repository.CreateDraftPluginWithCodeRequest{
 | 
						|
		SpaceID:     req.SpaceID,
 | 
						|
		DeveloperID: req.DeveloperID,
 | 
						|
		ProjectID:   req.ProjectID,
 | 
						|
		Manifest:    req.Manifest,
 | 
						|
		OpenapiDoc:  req.OpenapiDoc,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return nil, errorx.Wrapf(err, "CreateDraftPluginWithCode failed")
 | 
						|
	}
 | 
						|
 | 
						|
	resp = &CreateDraftPluginWithCodeResponse{
 | 
						|
		Plugin: res.Plugin,
 | 
						|
		Tools:  res.Tools,
 | 
						|
	}
 | 
						|
 | 
						|
	return resp, nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) UpdateDraftPluginWithCode(ctx context.Context, req *UpdateDraftPluginWithCodeRequest) (err error) {
 | 
						|
	doc := req.OpenapiDoc
 | 
						|
	mf := req.Manifest
 | 
						|
 | 
						|
	err = doc.Validate(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	err = mf.Validate(false)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	apiSchemas := make(map[entity.UniqueToolAPI]*model.Openapi3Operation, len(doc.Paths))
 | 
						|
	apis := make([]entity.UniqueToolAPI, 0, len(doc.Paths))
 | 
						|
 | 
						|
	for subURL, pathItem := range doc.Paths {
 | 
						|
		for method, op := range pathItem.Operations() {
 | 
						|
			api := entity.UniqueToolAPI{
 | 
						|
				SubURL: subURL,
 | 
						|
				Method: method,
 | 
						|
			}
 | 
						|
			apiSchemas[api] = model.NewOpenapi3Operation(op)
 | 
						|
			apis = append(apis, api)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	oldDraftTools, err := p.toolRepo.GetPluginAllDraftTools(ctx, req.PluginID)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "GetPluginAllDraftTools failed, pluginID=%d", req.PluginID)
 | 
						|
	}
 | 
						|
 | 
						|
	draftPlugin, exist, err := p.pluginRepo.GetDraftPlugin(ctx, req.PluginID)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "GetDraftPlugin failed, pluginID=%d", req.PluginID)
 | 
						|
	}
 | 
						|
	if !exist {
 | 
						|
		return errorx.New(errno.ErrPluginRecordNotFound)
 | 
						|
	}
 | 
						|
 | 
						|
	if draftPlugin.GetServerURL() != doc.Servers[0].URL {
 | 
						|
		for _, draftTool := range oldDraftTools {
 | 
						|
			draftTool.DebugStatus = ptr.Of(common.APIDebugStatus_DebugWaiting)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	oldDraftToolsMap := slices.ToMap(oldDraftTools, func(e *entity.ToolInfo) (entity.UniqueToolAPI, *entity.ToolInfo) {
 | 
						|
		return entity.UniqueToolAPI{
 | 
						|
			SubURL: e.GetSubURL(),
 | 
						|
			Method: e.GetMethod(),
 | 
						|
		}, e
 | 
						|
	})
 | 
						|
 | 
						|
	// 1. Delete tool - > Turn off Enable
 | 
						|
	for api, oldTool := range oldDraftToolsMap {
 | 
						|
		_, ok := apiSchemas[api]
 | 
						|
		if !ok {
 | 
						|
			oldTool.DebugStatus = ptr.Of(common.APIDebugStatus_DebugWaiting)
 | 
						|
			oldTool.ActivatedStatus = ptr.Of(model.DeactivateTool)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	newDraftTools := make([]*entity.ToolInfo, 0, len(apis))
 | 
						|
	for api, newOp := range apiSchemas {
 | 
						|
		oldTool, ok := oldDraftToolsMap[api]
 | 
						|
		if ok { // 2. Update tool - > Overlay
 | 
						|
			oldTool.ActivatedStatus = ptr.Of(model.ActivateTool)
 | 
						|
			oldTool.Operation = newOp
 | 
						|
			if needResetDebugStatusTool(ctx, newOp, oldTool.Operation) {
 | 
						|
				oldTool.DebugStatus = ptr.Of(common.APIDebugStatus_DebugWaiting)
 | 
						|
			}
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// 3. New tools
 | 
						|
		newDraftTools = append(newDraftTools, &entity.ToolInfo{
 | 
						|
			PluginID:        req.PluginID,
 | 
						|
			ActivatedStatus: ptr.Of(model.ActivateTool),
 | 
						|
			DebugStatus:     ptr.Of(common.APIDebugStatus_DebugWaiting),
 | 
						|
			SubURL:          ptr.Of(api.SubURL),
 | 
						|
			Method:          ptr.Of(api.Method),
 | 
						|
			Operation:       newOp,
 | 
						|
		})
 | 
						|
	}
 | 
						|
 | 
						|
	err = p.pluginRepo.UpdateDraftPluginWithCode(ctx, &repository.UpdatePluginDraftWithCode{
 | 
						|
		PluginID:      req.PluginID,
 | 
						|
		OpenapiDoc:    doc,
 | 
						|
		Manifest:      mf,
 | 
						|
		UpdatedTools:  oldDraftTools,
 | 
						|
		NewDraftTools: newDraftTools,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "UpdateDraftPluginWithCode failed, pluginID=%d", req.PluginID)
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func needResetDebugStatusTool(_ context.Context, nt, ot *model.Openapi3Operation) bool {
 | 
						|
	if len(nt.Parameters) != len(ot.Parameters) {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
 | 
						|
	otParams := make(map[string]*openapi3.Parameter, len(ot.Parameters))
 | 
						|
	cnt := make(map[string]int, len(nt.Parameters))
 | 
						|
 | 
						|
	for _, p := range nt.Parameters {
 | 
						|
		cnt[p.Value.Name]++
 | 
						|
	}
 | 
						|
	for _, p := range ot.Parameters {
 | 
						|
		cnt[p.Value.Name]--
 | 
						|
		otParams[p.Value.Name] = p.Value
 | 
						|
	}
 | 
						|
	for _, v := range cnt {
 | 
						|
		if v != 0 {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for _, p := range nt.Parameters {
 | 
						|
		np, op := p.Value, otParams[p.Value.Name]
 | 
						|
		if np.In != op.In {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
		if np.Required != op.Required {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
 | 
						|
		if !isJsonSchemaEqual(op.Schema.Value, np.Schema.Value) {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if nt.RequestBody == nil && ot.RequestBody == nil {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if (nt.RequestBody == nil && ot.RequestBody != nil) ||
 | 
						|
		(nt.RequestBody != nil && ot.RequestBody == nil) {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
 | 
						|
	nReqBody, oReqBody := nt.RequestBody.Value, ot.RequestBody.Value
 | 
						|
 | 
						|
	if len(nReqBody.Content) != len(oReqBody.Content) {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
	cnt = make(map[string]int, len(nReqBody.Content))
 | 
						|
	for ct := range nReqBody.Content {
 | 
						|
		cnt[ct]++
 | 
						|
	}
 | 
						|
	for ct := range oReqBody.Content {
 | 
						|
		cnt[ct]--
 | 
						|
	}
 | 
						|
	for _, v := range cnt {
 | 
						|
		if v != 0 {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for ct, nct := range nReqBody.Content {
 | 
						|
		oct := oReqBody.Content[ct]
 | 
						|
		if !isJsonSchemaEqual(nct.Schema.Value, oct.Schema.Value) {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
func isJsonSchemaEqual(nsc, osc *openapi3.Schema) bool {
 | 
						|
	if nsc.Type != osc.Type {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if nsc.Format != osc.Format {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if nsc.Default != osc.Default {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if nsc.Extensions[model.APISchemaExtendAssistType] != osc.Extensions[model.APISchemaExtendAssistType] {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if nsc.Extensions[model.APISchemaExtendGlobalDisable] != osc.Extensions[model.APISchemaExtendGlobalDisable] {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	switch nsc.Type {
 | 
						|
	case openapi3.TypeObject:
 | 
						|
		if len(nsc.Required) != len(osc.Required) {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		if len(nsc.Required) > 0 {
 | 
						|
			cnt := make(map[string]int, len(nsc.Required))
 | 
						|
			for _, x := range nsc.Required {
 | 
						|
				cnt[x]++
 | 
						|
			}
 | 
						|
			for _, x := range osc.Required {
 | 
						|
				cnt[x]--
 | 
						|
			}
 | 
						|
			for _, v := range cnt {
 | 
						|
				if v != 0 {
 | 
						|
					return true
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if len(nsc.Properties) != len(osc.Properties) {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		if len(nsc.Properties) > 0 {
 | 
						|
			for paramName, np := range nsc.Properties {
 | 
						|
				op, ok := osc.Properties[paramName]
 | 
						|
				if !ok {
 | 
						|
					return false
 | 
						|
				}
 | 
						|
				if !isJsonSchemaEqual(np.Value, op.Value) {
 | 
						|
					return false
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	case openapi3.TypeArray:
 | 
						|
		if !isJsonSchemaEqual(nsc.Items.Value, osc.Items.Value) {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) UpdateDraftPlugin(ctx context.Context, req *UpdateDraftPluginRequest) (err error) {
 | 
						|
	oldPlugin, exist, err := p.pluginRepo.GetDraftPlugin(ctx, req.PluginID)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "GetDraftPlugin failed, pluginID=%d", req.PluginID)
 | 
						|
	}
 | 
						|
	if !exist {
 | 
						|
		return errorx.New(errno.ErrPluginRecordNotFound)
 | 
						|
	}
 | 
						|
 | 
						|
	doc, err := updatePluginOpenapiDoc(ctx, oldPlugin.OpenapiDoc, req)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "updatePluginOpenapiDoc failed")
 | 
						|
	}
 | 
						|
	mf, err := updatePluginManifest(ctx, oldPlugin.Manifest, req)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "updatePluginManifest failed")
 | 
						|
	}
 | 
						|
 | 
						|
	newPlugin := entity.NewPluginInfo(&model.PluginInfo{
 | 
						|
		ID:         req.PluginID,
 | 
						|
		IconURI:    ptr.Of(mf.LogoURL),
 | 
						|
		ServerURL:  req.URL,
 | 
						|
		Manifest:   mf,
 | 
						|
		OpenapiDoc: doc,
 | 
						|
	})
 | 
						|
 | 
						|
	if newPlugin.GetServerURL() == "" ||
 | 
						|
		oldPlugin.GetServerURL() == newPlugin.GetServerURL() {
 | 
						|
		err = p.pluginRepo.UpdateDraftPluginWithoutURLChanged(ctx, newPlugin)
 | 
						|
		if err != nil {
 | 
						|
			return errorx.Wrapf(err, "UpdateDraftPluginWithoutURLChanged failed, pluginID=%d", req.PluginID)
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	err = p.pluginRepo.UpdateDraftPlugin(ctx, newPlugin)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "UpdateDraftPlugin failed, pluginID=%d", req.PluginID)
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func updatePluginOpenapiDoc(_ context.Context, doc *model.Openapi3T, req *UpdateDraftPluginRequest) (*model.Openapi3T, error) {
 | 
						|
	if req.Name != nil {
 | 
						|
		doc.Info.Title = *req.Name
 | 
						|
	}
 | 
						|
 | 
						|
	if req.Desc != nil {
 | 
						|
		doc.Info.Description = *req.Desc
 | 
						|
	}
 | 
						|
 | 
						|
	if req.URL != nil {
 | 
						|
		hasServer := false
 | 
						|
		for _, svr := range doc.Servers {
 | 
						|
			if svr.URL == *req.URL {
 | 
						|
				hasServer = true
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if !hasServer {
 | 
						|
			doc.Servers = openapi3.Servers{{URL: *req.URL}}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return doc, nil
 | 
						|
}
 | 
						|
 | 
						|
func updatePluginManifest(_ context.Context, mf *entity.PluginManifest, req *UpdateDraftPluginRequest) (*entity.PluginManifest, error) {
 | 
						|
	if req.Name != nil {
 | 
						|
		mf.NameForHuman = *req.Name
 | 
						|
		mf.NameForModel = *req.Name
 | 
						|
	}
 | 
						|
 | 
						|
	if req.Desc != nil {
 | 
						|
		mf.DescriptionForHuman = *req.Desc
 | 
						|
		mf.DescriptionForModel = *req.Desc
 | 
						|
	}
 | 
						|
 | 
						|
	if req.Icon != nil {
 | 
						|
		mf.LogoURL = req.Icon.URI
 | 
						|
	}
 | 
						|
 | 
						|
	if len(req.CommonParams) > 0 {
 | 
						|
		if mf.CommonParams == nil {
 | 
						|
			mf.CommonParams = make(map[model.HTTPParamLocation][]*plugin_develop_common.CommonParamSchema, len(req.CommonParams))
 | 
						|
		}
 | 
						|
		for loc, params := range req.CommonParams {
 | 
						|
			location, ok := model.ToHTTPParamLocation(loc)
 | 
						|
			if !ok {
 | 
						|
				return nil, fmt.Errorf("invalid location '%s'", loc.String())
 | 
						|
			}
 | 
						|
			commonParams := make([]*plugin_develop_common.CommonParamSchema, 0, len(params))
 | 
						|
			for _, param := range params {
 | 
						|
				commonParams = append(commonParams, &plugin_develop_common.CommonParamSchema{
 | 
						|
					Name:  param.Name,
 | 
						|
					Value: param.Value,
 | 
						|
				})
 | 
						|
			}
 | 
						|
			mf.CommonParams[location] = commonParams
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if req.AuthInfo != nil {
 | 
						|
		authV2, err := req.AuthInfo.toAuthV2()
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		mf.Auth = authV2
 | 
						|
	}
 | 
						|
 | 
						|
	return mf, nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) DeleteDraftPlugin(ctx context.Context, pluginID int64) (err error) {
 | 
						|
	return p.pluginRepo.DeleteDraftPlugin(ctx, pluginID)
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) MGetDraftTools(ctx context.Context, toolIDs []int64) (tools []*entity.ToolInfo, err error) {
 | 
						|
	tools, err = p.toolRepo.MGetDraftTools(ctx, toolIDs)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errorx.Wrapf(err, "MGetDraftTools failed, toolIDs=%v", toolIDs)
 | 
						|
	}
 | 
						|
 | 
						|
	return tools, nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) UpdateDraftTool(ctx context.Context, req *UpdateDraftToolRequest) (err error) {
 | 
						|
	draftPlugin, exist, err := p.pluginRepo.GetDraftPlugin(ctx, req.PluginID)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "GetDraftPlugin failed, pluginID=%d", req.PluginID)
 | 
						|
	}
 | 
						|
	if !exist {
 | 
						|
		return errorx.New(errno.ErrPluginRecordNotFound)
 | 
						|
	}
 | 
						|
 | 
						|
	draftTool, exist, err := p.toolRepo.GetDraftTool(ctx, req.ToolID)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "GetDraftTool failed, toolID=%d", req.ToolID)
 | 
						|
	}
 | 
						|
	if !exist {
 | 
						|
		return errorx.New(errno.ErrPluginRecordNotFound)
 | 
						|
	}
 | 
						|
 | 
						|
	if req.SaveExample != nil {
 | 
						|
		return p.updateDraftToolDebugExample(ctx, draftPlugin, draftTool, *req.SaveExample, req.DebugExample)
 | 
						|
	}
 | 
						|
 | 
						|
	return p.updateDraftTool(ctx, req, draftTool)
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) updateDraftTool(ctx context.Context, req *UpdateDraftToolRequest, draftTool *entity.ToolInfo) (err error) {
 | 
						|
	if req.Method != nil && req.SubURL != nil {
 | 
						|
		api := entity.UniqueToolAPI{
 | 
						|
			SubURL: ptr.FromOrDefault(req.SubURL, ""),
 | 
						|
			Method: ptr.FromOrDefault(req.Method, ""),
 | 
						|
		}
 | 
						|
		existTool, exist, err := p.toolRepo.GetDraftToolWithAPI(ctx, draftTool.PluginID, api)
 | 
						|
		if err != nil {
 | 
						|
			return errorx.Wrapf(err, "GetDraftToolWithAPI failed, pluginID=%d, api=%v", draftTool.PluginID, api)
 | 
						|
		}
 | 
						|
		if exist && draftTool.ID != existTool.ID {
 | 
						|
			return errorx.New(errno.ErrPluginDuplicatedTool, errorx.KVf(errno.PluginMsgKey, "[%s]:%s", api.Method, api.SubURL))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	var activatedStatus *model.ActivatedStatus
 | 
						|
	if req.Disabled != nil {
 | 
						|
		if *req.Disabled {
 | 
						|
			activatedStatus = ptr.Of(model.DeactivateTool)
 | 
						|
		} else {
 | 
						|
			activatedStatus = ptr.Of(model.ActivateTool)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	debugStatus := draftTool.DebugStatus
 | 
						|
	if req.Method != nil ||
 | 
						|
		req.SubURL != nil ||
 | 
						|
		req.Parameters != nil ||
 | 
						|
		req.RequestBody != nil ||
 | 
						|
		req.Responses != nil {
 | 
						|
		debugStatus = ptr.Of(common.APIDebugStatus_DebugWaiting)
 | 
						|
	}
 | 
						|
 | 
						|
	op := draftTool.Operation
 | 
						|
	if req.Name != nil {
 | 
						|
		op.OperationID = *req.Name
 | 
						|
	}
 | 
						|
	if req.Desc != nil {
 | 
						|
		op.Summary = *req.Desc
 | 
						|
	}
 | 
						|
	if req.APIExtend != nil {
 | 
						|
		if op.Extensions == nil {
 | 
						|
			op.Extensions = map[string]any{}
 | 
						|
		}
 | 
						|
		authMode, ok := model.ToAPIAuthMode(req.APIExtend.AuthMode)
 | 
						|
		if ok {
 | 
						|
			op.Extensions[model.APISchemaExtendAuthMode] = authMode
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// update request parameters
 | 
						|
	if req.Parameters != nil {
 | 
						|
		op.Parameters = req.Parameters
 | 
						|
	}
 | 
						|
 | 
						|
	// update request body
 | 
						|
	if req.RequestBody == nil {
 | 
						|
		op.RequestBody = draftTool.Operation.RequestBody
 | 
						|
	} else {
 | 
						|
		mType, ok := req.RequestBody.Value.Content[model.MediaTypeJson]
 | 
						|
		if !ok {
 | 
						|
			return fmt.Errorf("the '%s' media type is not defined in request body", model.MediaTypeJson)
 | 
						|
		}
 | 
						|
		if op.RequestBody == nil || op.RequestBody.Value == nil || op.RequestBody.Value.Content == nil {
 | 
						|
			op.RequestBody = &openapi3.RequestBodyRef{
 | 
						|
				Value: &openapi3.RequestBody{
 | 
						|
					Content: map[string]*openapi3.MediaType{},
 | 
						|
				},
 | 
						|
			}
 | 
						|
		}
 | 
						|
		op.RequestBody.Value.Content[model.MediaTypeJson] = mType
 | 
						|
	}
 | 
						|
 | 
						|
	// update responses
 | 
						|
	if req.Responses == nil {
 | 
						|
		op.Responses = draftTool.Operation.Responses
 | 
						|
	} else {
 | 
						|
		newRespRef, ok := req.Responses[strconv.Itoa(http.StatusOK)]
 | 
						|
		if !ok {
 | 
						|
			return fmt.Errorf("the '%d' status code is not defined in responses", http.StatusOK)
 | 
						|
		}
 | 
						|
		newMIMEType, ok := newRespRef.Value.Content[model.MediaTypeJson]
 | 
						|
		if !ok {
 | 
						|
			return fmt.Errorf("the '%s' media type is not defined in responses", model.MediaTypeJson)
 | 
						|
		}
 | 
						|
 | 
						|
		if op.Responses == nil {
 | 
						|
			op.Responses = map[string]*openapi3.ResponseRef{}
 | 
						|
		}
 | 
						|
 | 
						|
		oldRespRef, ok := op.Responses[strconv.Itoa(http.StatusOK)]
 | 
						|
		if !ok {
 | 
						|
			oldRespRef = &openapi3.ResponseRef{
 | 
						|
				Value: &openapi3.Response{
 | 
						|
					Content: map[string]*openapi3.MediaType{},
 | 
						|
				},
 | 
						|
			}
 | 
						|
			op.Responses[strconv.Itoa(http.StatusOK)] = oldRespRef
 | 
						|
		}
 | 
						|
 | 
						|
		if oldRespRef.Value.Content == nil {
 | 
						|
			oldRespRef.Value.Content = map[string]*openapi3.MediaType{}
 | 
						|
		}
 | 
						|
 | 
						|
		oldRespRef.Value.Content[model.MediaTypeJson] = newMIMEType
 | 
						|
	}
 | 
						|
 | 
						|
	updatedTool := &entity.ToolInfo{
 | 
						|
		ID:              req.ToolID,
 | 
						|
		PluginID:        req.PluginID,
 | 
						|
		ActivatedStatus: activatedStatus,
 | 
						|
		DebugStatus:     debugStatus,
 | 
						|
		Method:          req.Method,
 | 
						|
		SubURL:          req.SubURL,
 | 
						|
		Operation:       op,
 | 
						|
	}
 | 
						|
 | 
						|
	err = p.toolRepo.UpdateDraftTool(ctx, updatedTool)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "UpdateDraftTool failed, toolID=%d", req.ToolID)
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) updateDraftToolDebugExample(ctx context.Context, draftPlugin *entity.PluginInfo,
 | 
						|
	draftTool *entity.ToolInfo, save bool, example *common.DebugExample) (err error) {
 | 
						|
 | 
						|
	components := draftPlugin.OpenapiDoc.Components
 | 
						|
 | 
						|
	if !save && components != nil && components.Examples != nil {
 | 
						|
		delete(components.Examples, draftTool.Operation.OperationID)
 | 
						|
	}
 | 
						|
 | 
						|
	if save {
 | 
						|
		if components == nil {
 | 
						|
			components = &openapi3.Components{}
 | 
						|
		}
 | 
						|
		if components.Examples == nil {
 | 
						|
			components.Examples = make(map[string]*openapi3.ExampleRef)
 | 
						|
		}
 | 
						|
 | 
						|
		draftPlugin.OpenapiDoc.Components = components
 | 
						|
 | 
						|
		reqExample, respExample := map[string]any{}, map[string]any{}
 | 
						|
		if example.ReqExample != "" {
 | 
						|
			err = sonic.UnmarshalString(example.ReqExample, &reqExample)
 | 
						|
			if err != nil {
 | 
						|
				return errorx.WrapByCode(err, errno.ErrPluginInvalidOpenapi3Doc, errorx.KV(errno.PluginMsgKey, "invalid request example"))
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if example.RespExample != "" {
 | 
						|
			err = sonic.UnmarshalString(example.RespExample, &respExample)
 | 
						|
			if err != nil {
 | 
						|
				return errorx.WrapByCode(err, errno.ErrPluginInvalidOpenapi3Doc, errorx.KV(errno.PluginMsgKey, "invalid response example"))
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		components.Examples[draftTool.Operation.OperationID] = &openapi3.ExampleRef{
 | 
						|
			Value: &openapi3.Example{
 | 
						|
				Value: map[string]any{
 | 
						|
					"ReqExample":  reqExample,
 | 
						|
					"RespExample": respExample,
 | 
						|
				},
 | 
						|
			},
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	err = p.pluginRepo.UpdateDebugExample(ctx, draftPlugin.ID, draftPlugin.OpenapiDoc)
 | 
						|
	if err != nil {
 | 
						|
		return errorx.Wrapf(err, "UpdateDebugExample failed, pluginID=%d", draftPlugin.ID)
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) ConvertToOpenapi3Doc(ctx context.Context, req *ConvertToOpenapi3DocRequest) (resp *ConvertToOpenapi3DocResponse) {
 | 
						|
	var err error
 | 
						|
	defer func() {
 | 
						|
		if err != nil {
 | 
						|
			logs.Errorf("ConvertToOpenapi3Doc failed, err=%s", err)
 | 
						|
 | 
						|
			resp.ErrMsg = "internal server error"
 | 
						|
 | 
						|
			var e errorx.StatusError
 | 
						|
			if errors.As(err, &e) {
 | 
						|
				resp.ErrMsg = e.Msg()
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	resp = &ConvertToOpenapi3DocResponse{}
 | 
						|
 | 
						|
	cvt, format, err := getConvertFunc(ctx, req.RawInput)
 | 
						|
	if err != nil {
 | 
						|
		resp.Format = format
 | 
						|
		return resp
 | 
						|
	}
 | 
						|
 | 
						|
	doc, mf, err := cvt(ctx, req.RawInput)
 | 
						|
	if err != nil {
 | 
						|
		resp.Format = format
 | 
						|
		return resp
 | 
						|
	}
 | 
						|
 | 
						|
	err = validateConvertResult(ctx, req, doc, mf)
 | 
						|
	if err != nil {
 | 
						|
		resp.Format = format
 | 
						|
		return resp
 | 
						|
	}
 | 
						|
 | 
						|
	return &ConvertToOpenapi3DocResponse{
 | 
						|
		OpenapiDoc: doc,
 | 
						|
		Manifest:   mf,
 | 
						|
		Format:     format,
 | 
						|
		ErrMsg:     "",
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type convertFunc func(ctx context.Context, rawInput string) (*model.Openapi3T, *entity.PluginManifest, error)
 | 
						|
 | 
						|
func getConvertFunc(ctx context.Context, rawInput string) (convertFunc, common.PluginDataFormat, error) {
 | 
						|
	if strings.HasPrefix(rawInput, "curl") {
 | 
						|
		return openapi.CurlToOpenapi3Doc, common.PluginDataFormat_Curl, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if strings.Contains(rawInput, "_postman_id") { // postman collection
 | 
						|
		return openapi.PostmanToOpenapi3Doc, common.PluginDataFormat_Postman, nil
 | 
						|
	}
 | 
						|
 | 
						|
	var vd struct {
 | 
						|
		OpenAPI string `json:"openapi" yaml:"openapi"`
 | 
						|
		Swagger string `json:"swagger" yaml:"swagger"`
 | 
						|
	}
 | 
						|
 | 
						|
	err := sonic.UnmarshalString(rawInput, &vd)
 | 
						|
	if err != nil {
 | 
						|
		err = yaml.Unmarshal([]byte(rawInput), &vd)
 | 
						|
		if err != nil {
 | 
						|
			return nil, 0, fmt.Errorf("invalid schema")
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if vd.OpenAPI == "3" || strings.HasPrefix(vd.OpenAPI, "3.") {
 | 
						|
		return openapi.ToOpenapi3Doc, common.PluginDataFormat_OpenAPI, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if vd.Swagger == "2" || strings.HasPrefix(vd.Swagger, "2.") {
 | 
						|
		return openapi.SwaggerToOpenapi3Doc, common.PluginDataFormat_Swagger, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return nil, 0, fmt.Errorf("invalid schema")
 | 
						|
}
 | 
						|
 | 
						|
func validateConvertResult(ctx context.Context, req *ConvertToOpenapi3DocRequest, doc *model.Openapi3T, mf *entity.PluginManifest) error {
 | 
						|
	if req.PluginServerURL != nil {
 | 
						|
		if doc.Servers[0].URL != *req.PluginServerURL {
 | 
						|
			return errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey, "inconsistent API URL prefix"))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	err := doc.Validate(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	err = mf.Validate(false)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *pluginServiceImpl) CreateDraftToolsWithCode(ctx context.Context, req *CreateDraftToolsWithCodeRequest) (resp *CreateDraftToolsWithCodeResponse, err error) {
 | 
						|
	err = req.OpenapiDoc.Validate(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	toolAPIs := make([]entity.UniqueToolAPI, 0, len(req.OpenapiDoc.Paths))
 | 
						|
	for path, item := range req.OpenapiDoc.Paths {
 | 
						|
		for method := range item.Operations() {
 | 
						|
			toolAPIs = append(toolAPIs, entity.UniqueToolAPI{
 | 
						|
				SubURL: path,
 | 
						|
				Method: method,
 | 
						|
			})
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	existTools, err := p.toolRepo.MGetDraftToolWithAPI(ctx, req.PluginID, toolAPIs,
 | 
						|
		repository.WithToolID(),
 | 
						|
		repository.WithToolMethod(),
 | 
						|
		repository.WithToolSubURL())
 | 
						|
	if err != nil {
 | 
						|
		return nil, errorx.Wrapf(err, "MGetDraftToolWithAPI failed, pluginID=%d, apis=%v", req.PluginID, toolAPIs)
 | 
						|
	}
 | 
						|
 | 
						|
	duplicatedTools := make([]entity.UniqueToolAPI, 0, len(existTools))
 | 
						|
	for _, api := range toolAPIs {
 | 
						|
		if _, exist := existTools[api]; exist {
 | 
						|
			duplicatedTools = append(duplicatedTools, api)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if !req.ConflictAndUpdate && len(duplicatedTools) > 0 {
 | 
						|
		return &CreateDraftToolsWithCodeResponse{
 | 
						|
			DuplicatedTools: duplicatedTools,
 | 
						|
		}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	tools := make([]*entity.ToolInfo, 0, len(toolAPIs))
 | 
						|
	for path, item := range req.OpenapiDoc.Paths {
 | 
						|
		for method, op := range item.Operations() {
 | 
						|
			tools = append(tools, &entity.ToolInfo{
 | 
						|
				PluginID:        req.PluginID,
 | 
						|
				Method:          ptr.Of(method),
 | 
						|
				SubURL:          ptr.Of(path),
 | 
						|
				ActivatedStatus: ptr.Of(model.ActivateTool),
 | 
						|
				DebugStatus:     ptr.Of(common.APIDebugStatus_DebugWaiting),
 | 
						|
				Operation:       model.NewOpenapi3Operation(op),
 | 
						|
			})
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	err = p.toolRepo.UpsertDraftTools(ctx, req.PluginID, tools)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errorx.Wrapf(err, "UpsertDraftTools failed, pluginID=%d", req.PluginID)
 | 
						|
	}
 | 
						|
 | 
						|
	resp = &CreateDraftToolsWithCodeResponse{}
 | 
						|
 | 
						|
	return resp, nil
 | 
						|
}
 |