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
 | |
| }
 |