288 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			288 lines
		
	
	
		
			7.3 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 conf
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/getkin/kin-openapi/openapi3"
 | |
| 	"github.com/mohae/deepcopy"
 | |
| 	"golang.org/x/mod/semver"
 | |
| 	"gopkg.in/yaml.v3"
 | |
| 
 | |
| 	model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
 | |
| 	common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop_common"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/logs"
 | |
| )
 | |
| 
 | |
| type pluginProductMeta struct {
 | |
| 	PluginID       int64                  `yaml:"plugin_id" validate:"required"`
 | |
| 	ProductID      int64                  `yaml:"product_id" validate:"required"`
 | |
| 	Deprecated     bool                   `yaml:"deprecated"`
 | |
| 	Version        string                 `yaml:"version" validate:"required"`
 | |
| 	PluginType     common.PluginType      `yaml:"plugin_type" validate:"required"`
 | |
| 	OpenapiDocFile string                 `yaml:"openapi_doc_file" validate:"required"`
 | |
| 	Manifest       *entity.PluginManifest `yaml:"manifest" validate:"required"`
 | |
| 	Tools          []*toolProductMeta     `yaml:"tools" validate:"required"`
 | |
| }
 | |
| 
 | |
| type toolProductMeta struct {
 | |
| 	ToolID     int64  `yaml:"tool_id" validate:"required"`
 | |
| 	Deprecated bool   `yaml:"deprecated"`
 | |
| 	Method     string `yaml:"method" validate:"required"`
 | |
| 	SubURL     string `yaml:"sub_url" validate:"required"`
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	pluginProducts map[int64]*PluginInfo
 | |
| 	toolProducts   map[int64]*ToolInfo
 | |
| )
 | |
| 
 | |
| func GetToolProduct(toolID int64) (*ToolInfo, bool) {
 | |
| 	ti, ok := toolProducts[toolID]
 | |
| 	if !ok {
 | |
| 		return nil, false
 | |
| 	}
 | |
| 
 | |
| 	ti_ := deepcopy.Copy(ti).(*ToolInfo)
 | |
| 
 | |
| 	return ti_, true
 | |
| }
 | |
| 
 | |
| func MGetToolProducts(toolIDs []int64) []*ToolInfo {
 | |
| 	tools := make([]*ToolInfo, 0, len(toolIDs))
 | |
| 	for _, toolID := range toolIDs {
 | |
| 		ti, ok := GetToolProduct(toolID)
 | |
| 		if !ok {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		tools = append(tools, ti)
 | |
| 	}
 | |
| 
 | |
| 	return tools
 | |
| }
 | |
| 
 | |
| func GetPluginProduct(pluginID int64) (*PluginInfo, bool) {
 | |
| 	pl, ok := pluginProducts[pluginID]
 | |
| 	return pl, ok
 | |
| }
 | |
| 
 | |
| func MGetPluginProducts(pluginIDs []int64) []*PluginInfo {
 | |
| 	plugins := make([]*PluginInfo, 0, len(pluginIDs))
 | |
| 	for _, pluginID := range pluginIDs {
 | |
| 		pl, ok := pluginProducts[pluginID]
 | |
| 		if !ok {
 | |
| 			continue
 | |
| 		}
 | |
| 		plugins = append(plugins, pl)
 | |
| 	}
 | |
| 	return plugins
 | |
| }
 | |
| 
 | |
| func GetAllPluginProducts() []*PluginInfo {
 | |
| 	plugins := make([]*PluginInfo, 0, len(pluginProducts))
 | |
| 	for _, pl := range pluginProducts {
 | |
| 		plugins = append(plugins, pl)
 | |
| 	}
 | |
| 	return plugins
 | |
| }
 | |
| 
 | |
| type PluginInfo struct {
 | |
| 	Info    *model.PluginInfo
 | |
| 	ToolIDs []int64
 | |
| }
 | |
| 
 | |
| func (pi PluginInfo) GetPluginAllTools() (tools []*ToolInfo) {
 | |
| 	tools = make([]*ToolInfo, 0, len(pi.ToolIDs))
 | |
| 	for _, toolID := range pi.ToolIDs {
 | |
| 		ti, ok := toolProducts[toolID]
 | |
| 		if !ok {
 | |
| 			continue
 | |
| 		}
 | |
| 		tools = append(tools, ti)
 | |
| 	}
 | |
| 	return tools
 | |
| }
 | |
| 
 | |
| type ToolInfo struct {
 | |
| 	Info *entity.ToolInfo
 | |
| }
 | |
| 
 | |
| func loadPluginProductMeta(ctx context.Context, basePath string) (err error) {
 | |
| 	root := path.Join(basePath, "pluginproduct")
 | |
| 	metaFile := path.Join(root, "plugin_meta.yaml")
 | |
| 
 | |
| 	file, err := os.ReadFile(metaFile)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("read file '%s' failed, err=%v", metaFile, err)
 | |
| 	}
 | |
| 
 | |
| 	var pluginsMeta []*pluginProductMeta
 | |
| 	err = yaml.Unmarshal(file, &pluginsMeta)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("unmarshal file '%s' failed, err=%v", metaFile, err)
 | |
| 	}
 | |
| 
 | |
| 	pluginProducts = make(map[int64]*PluginInfo, len(pluginsMeta))
 | |
| 	toolProducts = map[int64]*ToolInfo{}
 | |
| 
 | |
| 	for _, m := range pluginsMeta {
 | |
| 		if !checkPluginMetaInfo(ctx, m) {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		err = m.Manifest.Validate(true)
 | |
| 		if err != nil {
 | |
| 			logs.CtxErrorf(ctx, "plugin manifest validates failed, err=%v", err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		docPath := path.Join(root, m.OpenapiDocFile)
 | |
| 		loader := openapi3.NewLoader()
 | |
| 		_doc, err := loader.LoadFromFile(docPath)
 | |
| 		if err != nil {
 | |
| 			logs.CtxErrorf(ctx, "load file '%s', err=%v", docPath, err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		doc := ptr.Of(model.Openapi3T(*_doc))
 | |
| 
 | |
| 		err = doc.Validate(ctx)
 | |
| 		if err != nil {
 | |
| 			logs.CtxErrorf(ctx, "the openapi3 doc '%s' validates failed, err=%v", m.OpenapiDocFile, err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		pi := &PluginInfo{
 | |
| 			Info: &model.PluginInfo{
 | |
| 				ID:           m.PluginID,
 | |
| 				RefProductID: &m.ProductID,
 | |
| 				PluginType:   m.PluginType,
 | |
| 				Version:      ptr.Of(m.Version),
 | |
| 				IconURI:      ptr.Of(m.Manifest.LogoURL),
 | |
| 				ServerURL:    ptr.Of(doc.Servers[0].URL),
 | |
| 				Manifest:     m.Manifest,
 | |
| 				OpenapiDoc:   doc,
 | |
| 			},
 | |
| 			ToolIDs: make([]int64, 0, len(m.Tools)),
 | |
| 		}
 | |
| 
 | |
| 		if pluginProducts[m.PluginID] != nil {
 | |
| 			logs.CtxErrorf(ctx, "duplicate plugin id '%d'", m.PluginID)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		pluginProducts[m.PluginID] = pi
 | |
| 
 | |
| 		apis := make(map[entity.UniqueToolAPI]*model.Openapi3Operation, len(doc.Paths))
 | |
| 		for subURL, pathItem := range doc.Paths {
 | |
| 			for method, op := range pathItem.Operations() {
 | |
| 				api := entity.UniqueToolAPI{
 | |
| 					SubURL: subURL,
 | |
| 					Method: strings.ToUpper(method),
 | |
| 				}
 | |
| 				apis[api] = model.NewOpenapi3Operation(op)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		for _, t := range m.Tools {
 | |
| 			if t.Deprecated {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			_, ok := toolProducts[t.ToolID]
 | |
| 			if ok {
 | |
| 				logs.CtxErrorf(ctx, "duplicate tool id '%d'", t.ToolID)
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			api := entity.UniqueToolAPI{
 | |
| 				SubURL: t.SubURL,
 | |
| 				Method: strings.ToUpper(t.Method),
 | |
| 			}
 | |
| 			op, ok := apis[api]
 | |
| 			if !ok {
 | |
| 				logs.CtxErrorf(ctx, "api '[%s]:%s' not found in doc '%s'", api.Method, api.SubURL, docPath)
 | |
| 				continue
 | |
| 			}
 | |
| 			if err = op.Validate(ctx); err != nil {
 | |
| 				logs.CtxErrorf(ctx, "the openapi3 operation of tool '[%s]:%s' in '%s' validates failed, err=%v",
 | |
| 					t.Method, t.SubURL, m.OpenapiDocFile, err)
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			pi.ToolIDs = append(pi.ToolIDs, t.ToolID)
 | |
| 
 | |
| 			toolProducts[t.ToolID] = &ToolInfo{
 | |
| 				Info: &entity.ToolInfo{
 | |
| 					ID:              t.ToolID,
 | |
| 					PluginID:        m.PluginID,
 | |
| 					Version:         ptr.Of(m.Version),
 | |
| 					Method:          ptr.Of(t.Method),
 | |
| 					SubURL:          ptr.Of(t.SubURL),
 | |
| 					Operation:       op,
 | |
| 					ActivatedStatus: ptr.Of(model.ActivateTool),
 | |
| 					DebugStatus:     ptr.Of(common.APIDebugStatus_DebugPassed),
 | |
| 				},
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if len(pi.ToolIDs) == 0 {
 | |
| 			delete(pluginProducts, m.PluginID)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func checkPluginMetaInfo(ctx context.Context, m *pluginProductMeta) (continued bool) {
 | |
| 	if m.Deprecated {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	if !semver.IsValid(m.Version) {
 | |
| 		logs.CtxErrorf(ctx, "invalid version '%s'", m.Version)
 | |
| 		return false
 | |
| 	}
 | |
| 	if m.PluginID <= 0 {
 | |
| 		logs.CtxErrorf(ctx, "invalid plugin id '%d'", m.PluginID)
 | |
| 		return false
 | |
| 	}
 | |
| 	if m.ProductID <= 0 {
 | |
| 		logs.CtxErrorf(ctx, "invalid product id '%d'", m.ProductID)
 | |
| 		return false
 | |
| 	}
 | |
| 	_, ok := toolProducts[m.PluginID]
 | |
| 	if ok {
 | |
| 		logs.CtxErrorf(ctx, "duplicate plugin id '%d'", m.PluginID)
 | |
| 		return false
 | |
| 	}
 | |
| 	if m.PluginType != common.PluginType_PLUGIN {
 | |
| 		logs.CtxErrorf(ctx, "invalid plugin type '%s'", m.PluginType)
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	return true
 | |
| }
 |