feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
903
backend/domain/plugin/service/plugin_draft.go
Normal file
903
backend/domain/plugin/service/plugin_draft.go
Normal file
@@ -0,0 +1,903 @@
|
||||
/*
|
||||
* 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.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 {
|
||||
mParams := mf.CommonParams[location]
|
||||
mParams = append(mParams, &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. 删除 tool -> 关闭启用
|
||||
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. 更新 tool -> 覆盖
|
||||
oldTool.ActivatedStatus = ptr.Of(model.ActivateTool)
|
||||
oldTool.Operation = newOp
|
||||
if needResetDebugStatusTool(ctx, newOp, oldTool.Operation) {
|
||||
oldTool.DebugStatus = ptr.Of(common.APIDebugStatus_DebugWaiting)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. 新增 tool
|
||||
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(ot.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
|
||||
}
|
||||
}
|
||||
|
||||
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 *UpdateToolDraftRequest) (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.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.Parameters != nil {
|
||||
op.Parameters = req.Parameters
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
components := draftPlugin.OpenapiDoc.Components
|
||||
if req.SaveExample != nil && !*req.SaveExample &&
|
||||
components != nil && components.Examples != nil {
|
||||
delete(components.Examples, draftTool.Operation.OperationID)
|
||||
} else if req.DebugExample != nil {
|
||||
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 req.DebugExample.ReqExample != "" {
|
||||
err = sonic.UnmarshalString(req.DebugExample.ReqExample, &reqExample)
|
||||
if err != nil {
|
||||
return errorx.WrapByCode(err, errno.ErrPluginInvalidOpenapi3Doc, errorx.KV(errno.PluginMsgKey, "invalid request example"))
|
||||
}
|
||||
}
|
||||
if req.DebugExample.RespExample != "" {
|
||||
err = sonic.UnmarshalString(req.DebugExample.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.toolRepo.UpdateDraftToolAndDebugExample(ctx, draftPlugin.ID, draftPlugin.OpenapiDoc, updatedTool)
|
||||
if err != nil {
|
||||
return errorx.Wrapf(err, "UpdateDraftToolAndDebugExample failed, pluginID=%d, toolID=%d", draftPlugin.ID, req.ToolID)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user