feat(plugin): support saas plugin (#2349)

Co-authored-by: yuwenbinjie <yuwenbinjie@bytedance.com>
Co-authored-by: ski <csu.zengxiaohui@gmail.com>
Co-authored-by: Ryo <fanlv@bytedance.com>
This commit is contained in:
junwen-lee
2025-10-21 10:43:48 +08:00
committed by GitHub
parent d5e913d76d
commit 6e02c861c7
254 changed files with 48874 additions and 2399 deletions

View File

@ -18,9 +18,14 @@ package plugin
import (
"context"
"os"
"strconv"
"strings"
"time"
typesConsts "github.com/coze-dev/coze-studio/backend/types/consts"
"github.com/coze-dev/coze-studio/backend/api/model/app/bot_common"
productCommon "github.com/coze-dev/coze-studio/backend/api/model/marketplace/product_common"
productAPI "github.com/coze-dev/coze-studio/backend/api/model/marketplace/product_public_api"
pluginAPI "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop"
@ -33,6 +38,7 @@ import (
"github.com/coze-dev/coze-studio/backend/domain/plugin/repository"
"github.com/coze-dev/coze-studio/backend/domain/plugin/service"
search "github.com/coze-dev/coze-studio/backend/domain/search/service"
userEntity "github.com/coze-dev/coze-studio/backend/domain/user/entity"
user "github.com/coze-dev/coze-studio/backend/domain/user/service"
"github.com/coze-dev/coze-studio/backend/infra/storage"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
@ -64,15 +70,8 @@ func (p *PluginApplicationService) CheckAndLockPluginEdit(ctx context.Context, r
}
func (p *PluginApplicationService) GetBotDefaultParams(ctx context.Context, req *pluginAPI.GetBotDefaultParamsRequest) (resp *pluginAPI.GetBotDefaultParamsResponse, err error) {
_, exist, err := p.pluginRepo.GetOnlinePlugin(ctx, req.PluginID, repository.WithPluginID())
if err != nil {
return nil, errorx.Wrapf(err, "GetOnlinePlugin failed, pluginID=%d", req.PluginID)
}
if !exist {
return nil, errorx.New(errno.ErrPluginRecordNotFound)
}
draftAgentTool, err := p.DomainSVC.GetDraftAgentToolByName(ctx, req.BotID, req.APIName)
draftAgentTool, err := p.DomainSVC.GetDraftAgentToolByName(ctx, req.BotID, req.PluginID, req.APIName)
if err != nil {
return nil, errorx.Wrapf(err, "GetDraftAgentToolByName failed, agentID=%d, toolName=%s", req.BotID, req.APIName)
}
@ -220,6 +219,12 @@ func (p *PluginApplicationService) buildPluginProductExtraInfo(ctx context.Conte
}
return ptr.Of(productCommon.PluginType_CLoudPlugin)
}(),
JumpSaasURL: func() *string {
if plugin.SaasPluginExtra != nil && plugin.SaasPluginExtra.JumpSaasURL != nil {
return plugin.SaasPluginExtra.JumpSaasURL
}
return nil
}(),
}
toolInfos := make([]*productAPI.PluginToolInfo, 0, len(tools))
@ -261,6 +266,8 @@ func (p *PluginApplicationService) buildPluginProductExtraInfo(ctx context.Conte
} else {
authMode = ptr.Of(productAPI.PluginAuthMode_Configured)
}
} else if authInfo.Type == consts.AuthTypeOfSaasInstalled {
authMode = ptr.Of(productAPI.PluginAuthMode_NeedInstalled)
}
}
@ -314,3 +321,330 @@ func (p *PluginApplicationService) validateDraftPluginAccess(ctx context.Context
return plugin, nil
}
// convertPluginToProductInfo converts a plugin entity to ProductInfo
func convertPluginToProductInfo(plugin *entity.PluginInfo) *productAPI.ProductInfo {
isOfficial := func() bool {
if plugin.SaasPluginExtra == nil {
return true
}
return plugin.SaasPluginExtra.IsOfficial
}()
return &productAPI.ProductInfo{
MetaInfo: &productAPI.ProductMetaInfo{
ID: plugin.GetRefProductID(),
Name: plugin.GetName(),
EntityID: plugin.ID,
Description: plugin.GetDesc(),
IconURL: plugin.GetIconURI(),
ListedAt: plugin.CreatedAt,
EntityType: func() productCommon.ProductEntityType {
if ptr.From(plugin.Source) == bot_common.PluginFrom_FromSaas {
return productCommon.ProductEntityType_SaasPlugin
}
return productCommon.ProductEntityType_Plugin
}(),
IsOfficial: isOfficial,
Status: productCommon.ProductStatus_Listed,
UserInfo: &productCommon.UserInfo{
Name: func() string {
if isOfficial {
return "Coze Official"
}
return "Coze Community"
}(),
},
},
}
}
func (p *PluginApplicationService) convertPluginsToProductInfos(ctx context.Context, plugins []*entity.PluginInfo, tools map[int64][]*entity.ToolInfo) []*productAPI.ProductInfo {
products := make([]*productAPI.ProductInfo, 0, len(plugins))
for _, plugin := range plugins {
var pExtra *productAPI.PluginExtraInfo
if tool, exist := tools[plugin.ID]; exist {
pluginExtra, err := p.buildPluginProductExtraInfo(ctx, plugin, tool)
if err != nil {
logs.CtxErrorf(ctx, "buildPluginProductExtraInfo failed: %v", err)
} else {
pExtra = pluginExtra
}
}
pi := convertPluginToProductInfo(plugin)
pi.PluginExtra = pExtra
products = append(products, pi)
}
return products
}
func (p *PluginApplicationService) convertPluginsToMetaInfos(ctx context.Context, plugins []*entity.PluginInfo, tools map[int64][]*entity.ToolInfo) []*productAPI.ProductMetaInfo {
products := make([]*productAPI.ProductMetaInfo, 0, len(plugins))
for _, plugin := range plugins {
pi := &productAPI.ProductMetaInfo{
ID: plugin.ID,
Name: plugin.GetName(),
Description: plugin.GetDesc(),
IconURL: plugin.GetIconURI(),
ListedAt: plugin.CreatedAt,
EntityID: plugin.ID,
}
products = append(products, pi)
}
return products
}
func (p *PluginApplicationService) getSaasPluginList(ctx context.Context, domainReq *dto.ListSaasPluginProductsRequest) (*dto.ListPluginProductsResponse, error) {
return p.DomainSVC.ListSaasPluginProducts(ctx, domainReq)
}
func (p *PluginApplicationService) getSaasPluginToolsList(ctx context.Context, pluginIDs []int64) (map[int64][]*entity.ToolInfo, error) {
tools, _, err := p.DomainSVC.BatchGetSaasPluginToolsInfo(ctx, pluginIDs)
return tools, err
}
func (p *PluginApplicationService) GetCozeSaasPluginList(ctx context.Context, req *productAPI.GetProductListRequest) (resp *productAPI.GetProductListResponse, err error) {
domainResp, err := p.getSaasPluginList(ctx, &dto.ListSaasPluginProductsRequest{
PageNum: ptr.Of(req.PageNum),
PageSize: ptr.Of(req.PageSize),
Keyword: req.Keyword,
EntityTypes: req.EntityTypes,
})
if err != nil {
logs.CtxErrorf(ctx, "ListSaasPluginProducts failed: %v", err)
return nil, err
}
// tools
pluginIDs := make([]int64, 0, len(domainResp.Plugins))
for _, product := range domainResp.Plugins {
pluginIDs = append(pluginIDs, product.ID)
}
tools, err := p.getSaasPluginToolsList(ctx, pluginIDs)
if err != nil {
logs.CtxErrorf(ctx, "BatchGetSaasPluginToolsInfo failed: %v", err)
return nil, err
}
products := p.convertPluginsToProductInfos(ctx, domainResp.Plugins, tools)
return &productAPI.GetProductListResponse{
Code: 0,
Message: "success",
Data: &productAPI.GetProductListData{
Products: products,
Total: int32(domainResp.Total),
HasMore: domainResp.HasMore,
},
}, nil
}
func (p *PluginApplicationService) PublicSearchProduct(ctx context.Context, req *productAPI.SearchProductRequest) (resp *productAPI.SearchProductResponse, err error) {
domainResp, err := p.getSaasPluginList(ctx, &dto.ListSaasPluginProductsRequest{
PageNum: ptr.Of(req.PageNum),
PageSize: ptr.Of(req.PageSize),
Keyword: ptr.Of(req.Keyword),
EntityTypes: func() []productCommon.ProductEntityType {
if req.EntityTypes == nil {
return nil
}
return p.convertEntityTypesStrToSlice(*req.EntityTypes)
}(),
CategoryIDs: req.CategoryIDs,
IsOfficial: req.IsOfficial,
PluginType: req.PluginType,
ProductPaidType: req.ProductPaidType,
SortType: req.SortType,
})
if err != nil {
logs.CtxErrorf(ctx, "ListSaasPluginProducts failed: %v", err)
return nil, err
}
// tools
pluginIDs := make([]int64, 0, len(domainResp.Plugins))
for _, product := range domainResp.Plugins {
pluginIDs = append(pluginIDs, product.ID)
}
tools, err := p.getSaasPluginToolsList(ctx, pluginIDs)
if err != nil {
logs.CtxErrorf(ctx, "BatchGetSaasPluginToolsInfo failed: %v", err)
return nil, err
}
products := p.convertPluginsToProductInfos(ctx, domainResp.Plugins, tools)
return &productAPI.SearchProductResponse{
Code: 0,
Message: "success",
Data: &productAPI.SearchProductResponseData{
Products: products,
Total: ptr.Of(int32(domainResp.Total)),
HasMore: ptr.Of(domainResp.HasMore),
},
}, nil
}
func (p *PluginApplicationService) convertEntityTypesStrToSlice(entityTypesStr string) []productCommon.ProductEntityType {
var entityTypes []productCommon.ProductEntityType
if entityTypesStr != "" {
typeStrs := strings.Split(entityTypesStr, ",")
for _, typeStr := range typeStrs {
typeStr = strings.TrimSpace(typeStr)
if typeStr != "" {
if entityType, err := productCommon.ProductEntityTypeFromString(typeStr); err == nil {
entityTypes = append(entityTypes, entityType)
}
}
}
}
return entityTypes
}
func (p *PluginApplicationService) PublicSearchSuggest(ctx context.Context, req *productAPI.SearchSuggestRequest) (resp *productAPI.SearchSuggestResponse, err error) {
domainResp, err := p.getSaasPluginList(ctx, &dto.ListSaasPluginProductsRequest{
PageNum: req.PageNum,
PageSize: req.PageSize,
Keyword: req.Keyword,
EntityTypes: func() []productCommon.ProductEntityType {
if req.EntityTypes == nil {
return nil
}
return p.convertEntityTypesStrToSlice(*req.EntityTypes)
}(),
})
if err != nil {
logs.CtxErrorf(ctx, "ListSaasPluginProducts for suggestions failed: %v", err)
return nil, err
}
// tools
pluginIDs := make([]int64, 0, len(domainResp.Plugins))
for _, product := range domainResp.Plugins {
pluginIDs = append(pluginIDs, product.ID)
}
tools, err := p.getSaasPluginToolsList(ctx, pluginIDs)
if err != nil {
logs.CtxErrorf(ctx, "BatchGetSaasPluginToolsInfo failed: %v", err)
return nil, err
}
suggestionProducts := p.convertPluginsToProductInfos(ctx, domainResp.Plugins, tools)
return &productAPI.SearchSuggestResponse{
Code: 0,
Message: "success",
Data: &productAPI.SearchSuggestResponseData{
SuggestionV2: suggestionProducts,
Suggestions: p.convertPluginsToMetaInfos(ctx, domainResp.Plugins, tools),
HasMore: ptr.Of(domainResp.HasMore),
},
}, nil
}
func (p *PluginApplicationService) GetSaasProductCategoryList(ctx context.Context, req *productAPI.GetProductCategoryListRequest) (resp *productAPI.GetProductCategoryListResponse, err error) {
domainReq := &dto.ListPluginCategoriesRequest{}
if req.GetEntityType() == productCommon.ProductEntityType_SaasPlugin {
domainReq.EntityType = ptr.Of("plugin")
}
domainResp, err := p.DomainSVC.ListSaasPluginCategories(ctx, domainReq)
if err != nil {
logs.CtxErrorf(ctx, "ListSaasPluginCategories failed: %v", err)
return nil, err
}
// 转换响应数据
categories := make([]*productAPI.ProductCategory, 0)
if domainResp.Data != nil && domainResp.Data.Items != nil {
for _, item := range domainResp.Data.Items {
// 将字符串 ID 转换为 int64
categoryID, _ := strconv.ParseInt(item.ID, 10, 64)
categories = append(categories, &productAPI.ProductCategory{
ID: categoryID,
Name: item.Name,
})
}
}
return &productAPI.GetProductCategoryListResponse{
Code: 0,
Message: "success",
Data: &productAPI.GetProductCategoryListData{
EntityType: req.GetEntityType(),
Categories: categories,
},
}, nil
}
func (p *PluginApplicationService) GetProductCallInfo(ctx context.Context, req *productAPI.GetProductCallInfoRequest) (resp *productAPI.GetProductCallInfoResponse, err error) {
userInfo, err := p.userSVC.GetSaasUserInfo(ctx)
if err != nil {
logs.CtxErrorf(ctx, "GetSaasUserInfo failed: %v", err)
return nil, err
}
benefit, err := p.userSVC.GetUserBenefit(ctx)
if err != nil {
logs.CtxErrorf(ctx, "GetUserBenefit failed: %v", err)
return nil, err
}
// Build response data
data := &productAPI.GetProductCallInfoData{
UserInfo: &productAPI.UserInfo{
UserName: ptr.Of(userInfo.UserName),
NickName: ptr.Of(userInfo.NickName),
AvatarURL: ptr.Of(userInfo.AvatarURL),
},
}
if benefit != nil {
data.UserLevel = func() productAPI.UserLevel {
switch benefit.UserLevel {
case userEntity.UserLevelPro:
return productAPI.UserLevel_ProPersonal
case userEntity.UserLevelEnterprise:
return productAPI.UserLevel_Enterprise
default:
return productAPI.UserLevel_Free
}
}()
data.CallCountLimit = &productAPI.ProductCallCountLimit{
IsUnlimited: benefit.IsUnlimited,
UsedCount: benefit.UsedCount,
TotalCount: benefit.TotalCount,
ResetDatetime: benefit.ResetDatetime,
}
data.CallRateLimit = &productAPI.ProductCallRateLimit{
QPS: benefit.CallQPS,
}
}
return &productAPI.GetProductCallInfoResponse{
Code: 0,
Message: "success",
Data: data,
}, nil
}
func (p *PluginApplicationService) GetMarketPluginConfig(ctx context.Context, req *productAPI.GetMarketPluginConfigRequest) (resp *productAPI.GetMarketPluginConfigResponse, err error) {
enableSaasPluginEnv := os.Getenv(typesConsts.CozeSaasPluginEnabled)
saasAPIiKey := os.Getenv(typesConsts.CozeSaasAPIKey)
enableSaasPlugin := enableSaasPluginEnv == "true" && len(saasAPIiKey) > 0
resp = &productAPI.GetMarketPluginConfigResponse{
Code: 0,
Message: "success",
Data: &productAPI.Configuration{
EnableSaasPlugin: &enableSaasPlugin,
},
}
return resp, nil
}