coze-studio/backend/domain/plugin/conf/load_plugin.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
}