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