feat(plugin): rebase main

This commit is contained in:
lijunwen.gigoo
2025-09-22 21:54:20 +08:00
parent ab0a7f0ed5
commit efa0bc0f7a
16 changed files with 4467 additions and 57 deletions

View File

@ -40,6 +40,7 @@ import (
"github.com/coze-dev/coze-studio/backend/application/search"
"github.com/coze-dev/coze-studio/backend/application/singleagent"
"github.com/coze-dev/coze-studio/backend/application/template"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
)
// PublicGetProductList .
@ -273,3 +274,47 @@ func PublicDuplicateProduct(ctx context.Context, c *app.RequestContext) {
c.JSON(consts.StatusOK, resp)
}
// PublicSearchProduct .
// @router /api/marketplace/product/search [GET]
func PublicSearchProduct(ctx context.Context, c *app.RequestContext) {
var err error
var req product_public_api.SearchProductRequest
err = c.BindAndValidate(&req)
if err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
// Call plugin application service
resp, err := plugin.PluginApplicationSVC.PublicSearchProduct(ctx, &req)
if err != nil {
logs.CtxErrorf(ctx, "PublicSearchProduct failed: %v", err)
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}
// PublicSearchSuggest .
// @router /api/marketplace/product/search/suggest [GET]
func PublicSearchSuggest(ctx context.Context, c *app.RequestContext) {
var err error
var req product_public_api.SearchSuggestRequest
err = c.BindAndValidate(&req)
if err != nil {
invalidParamRequestResponse(c, err.Error())
return
}
// Call plugin application service
resp, err := plugin.PluginApplicationSVC.PublicSearchSuggest(ctx, &req)
if err != nil {
logs.CtxErrorf(ctx, "PublicSearchSuggest failed: %v", err)
internalServerErrorResponse(ctx, c, err)
return
}
c.JSON(consts.StatusOK, resp)
}

View File

@ -1,4 +1,20 @@
// Code generated by thriftgo (0.4.1). DO NOT EDIT.
/*
* 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.
*/
// Code generated by thriftgo (0.4.2). DO NOT EDIT.
package marketplace_common

View File

@ -26,6 +26,110 @@ import (
"github.com/coze-dev/coze-studio/backend/api/model/marketplace/marketplace_common"
)
type BotModType int64
const (
BotModType_SingleAgent BotModType = 1
BotModType_MultiAgent BotModType = 2
)
func (p BotModType) String() string {
switch p {
case BotModType_SingleAgent:
return "SingleAgent"
case BotModType_MultiAgent:
return "MultiAgent"
}
return "<UNSET>"
}
func BotModTypeFromString(s string) (BotModType, error) {
switch s {
case "SingleAgent":
return BotModType_SingleAgent, nil
case "MultiAgent":
return BotModType_MultiAgent, nil
}
return BotModType(0), fmt.Errorf("not a valid BotModType string")
}
func BotModTypePtr(v BotModType) *BotModType { return &v }
func (p *BotModType) Scan(value interface{}) (err error) {
var result sql.NullInt64
err = result.Scan(value)
*p = BotModType(result.Int64)
return
}
func (p *BotModType) Value() (driver.Value, error) {
if p == nil {
return nil, nil
}
return int64(*p), nil
}
type Component int64
const (
Component_UsePlugin Component = 1
Component_UseWorkFlow Component = 2
Component_UseKnowledge Component = 3
Component_UseVoice Component = 4
Component_UseCard Component = 5
Component_UseImageWorkflow Component = 6
)
func (p Component) String() string {
switch p {
case Component_UsePlugin:
return "UsePlugin"
case Component_UseWorkFlow:
return "UseWorkFlow"
case Component_UseKnowledge:
return "UseKnowledge"
case Component_UseVoice:
return "UseVoice"
case Component_UseCard:
return "UseCard"
case Component_UseImageWorkflow:
return "UseImageWorkflow"
}
return "<UNSET>"
}
func ComponentFromString(s string) (Component, error) {
switch s {
case "UsePlugin":
return Component_UsePlugin, nil
case "UseWorkFlow":
return Component_UseWorkFlow, nil
case "UseKnowledge":
return Component_UseKnowledge, nil
case "UseVoice":
return Component_UseVoice, nil
case "UseCard":
return Component_UseCard, nil
case "UseImageWorkflow":
return Component_UseImageWorkflow, nil
}
return Component(0), fmt.Errorf("not a valid Component string")
}
func ComponentPtr(v Component) *Component { return &v }
func (p *Component) Scan(value interface{}) (err error) {
var result sql.NullInt64
err = result.Scan(value)
*p = Component(result.Int64)
return
}
func (p *Component) Value() (driver.Value, error) {
if p == nil {
return nil, nil
}
return int64(*p), nil
}
type ProductEntityType int64
const (

File diff suppressed because it is too large Load Diff

View File

@ -165,6 +165,9 @@ func Register(r *server.Hertz) {
_favorite := _product.Group("/favorite", _favoriteMw()...)
_favorite.GET("/list.v2", append(_publicgetuserfavoritelistv2Mw(), coze.PublicGetUserFavoriteListV2)...)
_product.GET("/list", append(_publicgetproductlistMw(), coze.PublicGetProductList)...)
_product.GET("/search", append(_publicsearchproductMw(), coze.PublicSearchProduct)...)
_search0 := _product.Group("/search", _search0Mw()...)
_search0.GET("/suggest", append(_publicsearchsuggestMw(), coze.PublicSearchSuggest)...)
}
}
{

View File

@ -1570,3 +1570,18 @@ func _getonlineappdataMw() []app.HandlerFunc {
// your code...
return nil
}
func _search0Mw() []app.HandlerFunc {
// your code...
return nil
}
func _publicsearchproductMw() []app.HandlerFunc {
// your code...
return nil
}
func _publicsearchsuggestMw() []app.HandlerFunc {
// your code...
return nil
}

View File

@ -381,10 +381,65 @@ func (p *PluginApplicationService) GetQueriedOAuthPluginList(ctx context.Context
return resp, nil
}
func (t *PluginApplicationService) GetCozeSaasPluginList(ctx context.Context, req *productAPI.GetProductListRequest) (resp *productAPI.GetProductListResponse, err error) {
domainReq := &service.ListPluginProductsRequest{}
// convertPluginToProductInfo converts a plugin entity to ProductInfo
func convertPluginToProductInfo(plugin *entity.PluginInfo) *productAPI.ProductInfo {
return &productAPI.ProductInfo{
MetaInfo: &productAPI.ProductMetaInfo{
ID: plugin.ID,
Name: plugin.GetName(),
EntityID: plugin.ID,
Description: plugin.GetDesc(),
IconURL: plugin.GetIconURI(),
ListedAt: plugin.CreatedAt,
},
PluginExtra: &productAPI.PluginExtraInfo{
IsOfficial: plugin.IsOfficial(),
},
}
}
// convertPluginsToProductInfos converts a slice of plugins to ProductInfo slice
func convertPluginsToProductInfos(plugins []*entity.PluginInfo) []*productAPI.ProductInfo {
products := make([]*productAPI.ProductInfo, 0, len(plugins))
for _, plugin := range plugins {
products = append(products, convertPluginToProductInfo(plugin))
}
return products
}
// getSaasPluginList is a common method to get SaaS plugin list from domain service
func (t *PluginApplicationService) getSaasPluginList(ctx context.Context) ([]*entity.PluginInfo, int64, error) {
domainReq := &service.ListPluginProductsRequest{}
domainResp, err := t.DomainSVC.ListSaasPluginProducts(ctx, domainReq)
if err != nil {
return nil, 0, err
}
return domainResp.Plugins, domainResp.Total, nil
}
// convertPluginsToSuggestions converts plugins to suggestion products with deduplication and limit
func convertPluginsToSuggestions(plugins []*entity.PluginInfo, limit int) []*productAPI.ProductInfo {
suggestionProducts := make([]*productAPI.ProductInfo, 0, len(plugins))
suggestionSet := make(map[string]bool) // Use map to avoid duplicates
for _, plugin := range plugins {
// Add plugin as suggestion if name is unique
if plugin.GetName() != "" && !suggestionSet[plugin.GetName()] {
suggestionProducts = append(suggestionProducts, convertPluginToProductInfo(plugin))
suggestionSet[plugin.GetName()] = true
}
// Limit suggestions to avoid too many results
if len(suggestionProducts) >= limit {
break
}
}
return suggestionProducts
}
func (t *PluginApplicationService) GetCozeSaasPluginList(ctx context.Context, req *productAPI.GetProductListRequest) (resp *productAPI.GetProductListResponse, err error) {
plugins, total, err := t.getSaasPluginList(ctx)
if err != nil {
logs.CtxErrorf(ctx, "ListSaasPluginProducts failed: %v", err)
return &productAPI.GetProductListResponse{
@ -393,32 +448,61 @@ func (t *PluginApplicationService) GetCozeSaasPluginList(ctx context.Context, re
}, nil
}
products := make([]*productAPI.ProductInfo, 0, len(domainResp.Plugins))
for _, plugin := range domainResp.Plugins {
productInfo := &productAPI.ProductInfo{
MetaInfo: &productAPI.ProductMetaInfo{
ID: plugin.ID,
Name: plugin.GetName(),
EntityID: plugin.ID,
Description: plugin.GetDesc(),
IconURL: plugin.GetIconURI(),
ListedAt: plugin.CreatedAt,
},
PluginExtra: &productAPI.PluginExtraInfo{
IsOfficial: plugin.IsOfficial(),
},
}
products = append(products, productInfo)
}
products := convertPluginsToProductInfos(plugins)
return &productAPI.GetProductListResponse{
Code: 0,
Message: "success",
Data: &productAPI.GetProductListData{
Products: products,
Total: int32(domainResp.Total),
Total: int32(total),
HasMore: false,
},
}, nil
}
func (t *PluginApplicationService) PublicSearchProduct(ctx context.Context, req *productAPI.SearchProductRequest) (resp *productAPI.SearchProductResponse, err error) {
plugins, total, err := t.getSaasPluginList(ctx)
if err != nil {
logs.CtxErrorf(ctx, "ListSaasPluginProducts failed: %v", err)
return &productAPI.SearchProductResponse{
Code: -1,
Message: "Failed to search SaaS plugins",
}, nil
}
products := convertPluginsToProductInfos(plugins)
return &productAPI.SearchProductResponse{
Code: 0,
Message: "success",
Data: &productAPI.SearchProductResponseData{
Products: products,
Total: ptr.Of(int32(total)),
HasMore: ptr.Of(false),
},
}, nil
}
func (t *PluginApplicationService) PublicSearchSuggest(ctx context.Context, req *productAPI.SearchSuggestRequest) (resp *productAPI.SearchSuggestResponse, err error) {
plugins, _, err := t.getSaasPluginList(ctx)
if err != nil {
logs.CtxErrorf(ctx, "ListSaasPluginProducts for suggestions failed: %v", err)
return &productAPI.SearchSuggestResponse{
Code: -1,
Message: "Failed to get search suggestions",
}, nil
}
// Convert plugins to suggestions with limit of 10
suggestionProducts := convertPluginsToSuggestions(plugins, 10)
return &productAPI.SearchSuggestResponse{
Code: 0,
Message: "success",
Data: &productAPI.SearchSuggestResponseData{
SuggestionV2: suggestionProducts,
HasMore: ptr.Of(false),
},
}, nil
}

View File

@ -0,0 +1,89 @@
/*
* 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 entity
// SearchSaasPluginRequest represents the request parameters for searching SaaS plugins
type SearchSaasPluginRequest struct {
Keyword *string `json:"keyword,omitempty"`
PageNum *int `json:"page_num,omitempty"`
PageSize *int `json:"page_size,omitempty"`
SortType *string `json:"sort_type,omitempty"`
CategoryID *string `json:"category_id,omitempty"`
IsOfficial *bool `json:"is_official,omitempty"`
}
// SearchSaasPluginResponse represents the response from coze.cn search API
type SearchSaasPluginResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *SearchSaasPluginData `json:"data"`
}
// SearchSaasPluginData represents the data section of search response
type SearchSaasPluginData struct {
Items []*SaasPluginItem `json:"items"`
HasMore bool `json:"has_more"`
}
// SaasPluginItem represents a single plugin item in search results
type SaasPluginItem struct {
MetaInfo *SaasPluginMetaInfo `json:"metainfo"`
PluginInfo *SaasPluginInfo `json:"plugin_info"`
}
// SaasPluginMetaInfo represents the metadata of a SaaS plugin
type SaasPluginMetaInfo struct {
ProductID string `json:"product_id"`
EntityID string `json:"entity_id"`
EntityVersion string `json:"entity_version"`
EntityType string `json:"entity_type"`
Name string `json:"name"`
Description string `json:"description"`
UserInfo *SaasPluginUserInfo `json:"user_info"`
Category *SaasPluginCategory `json:"category"`
IconURL string `json:"icon_url"`
ListedAt int64 `json:"listed_at"`
PaidType string `json:"paid_type"`
IsOfficial bool `json:"is_official"`
}
// SaasPluginUserInfo represents the user information of a SaaS plugin
type SaasPluginUserInfo struct {
UserID int64 `json:"user_id"`
UserName string `json:"user_name"`
NickName string `json:"nick_name"`
AvatarURL string `json:"avatar_url"`
}
// SaasPluginCategory represents the category information of a SaaS plugin
type SaasPluginCategory struct {
ID string `json:"id"`
Name string `json:"name"`
}
// SaasPluginInfo represents the plugin statistics and information
type SaasPluginInfo struct {
Description string `json:"description"`
TotalToolsCount int `json:"total_tools_count"`
FavoriteCount int `json:"favorite_count"`
Heat int `json:"heat"`
SuccessRate float64 `json:"success_rate"`
AvgExecDurationMs float64 `json:"avg_exec_duration_ms"`
BotsUseCount int64 `json:"bots_use_count"`
AssociatedBotsUseCount int64 `json:"associated_bots_use_count"`
CallCount int64 `json:"call_count"`
}

View File

@ -71,7 +71,7 @@ func (p *pluginServiceImpl) MGetAgentTools(ctx context.Context, req *model.MGetA
for _, v := range req.VersionAgentTools {
toolIDs = append(toolIDs, v.ToolID)
}
// todo :: saas plugin or local plugin
existTools, err := p.toolRepo.MGetOnlineTools(ctx, toolIDs, repository.WithToolID())
if err != nil {
return nil, errorx.Wrapf(err, "MGetOnlineTools failed, toolIDs=%v", toolIDs)
@ -109,11 +109,16 @@ func (p *pluginServiceImpl) MGetAgentTools(ctx context.Context, req *model.MGetA
}
}
//local plugin
tools, err = p.toolRepo.MGetVersionAgentTool(ctx, req.AgentID, vTools)
if err != nil {
return nil, errorx.Wrapf(err, "MGetVersionAgentTool failed, agentID=%d, vTools=%v", req.AgentID, vTools)
}
// TODO::saas plugin
// TODO::merge saas plugin or local plugin
return tools, nil
}

View File

@ -57,25 +57,15 @@ func (p *pluginServiceImpl) ListSaasPluginProducts(ctx context.Context, req *Lis
func (p *pluginServiceImpl) fetchSaasPluginsFromCoze(ctx context.Context) ([]*entity.PluginInfo, error) {
client := saasapi.NewCozeAPIClient()
resp, err := client.Get(ctx, "/v1/stores/plugins")
searchReq := &entity.SearchSaasPluginRequest{}
searchResp, err := p.searchSaasPlugin(ctx, searchReq)
if err != nil {
return nil, errorx.Wrapf(err, "failed to call coze.cn API")
return nil, errorx.Wrapf(err, "failed to search SaaS plugins")
}
var pluginsData struct {
Plugins []CozePlugin `json:"plugins"`
Total int64 `json:"total"`
}
if err := json.Unmarshal(resp.Data, &pluginsData); err != nil {
return nil, errorx.Wrapf(err, "failed to parse coze.cn API response")
}
plugins := make([]*entity.PluginInfo, 0, len(pluginsData.Plugins))
for _, cozePlugin := range pluginsData.Plugins {
plugin := convertCozePluginToEntity(cozePlugin)
plugins := make([]*entity.PluginInfo, 0, len(searchResp.Data.Items))
for _, item := range searchResp.Data.Items {
plugin := convertSaasPluginItemToEntity(item)
plugins = append(plugins, plugin)
}
@ -84,16 +74,60 @@ func (p *pluginServiceImpl) fetchSaasPluginsFromCoze(ctx context.Context) ([]*en
return plugins, nil
}
func convertSaasPluginItemToEntity(item *entity.SaasPluginItem) *entity.PluginInfo {
if item == nil || item.MetaInfo == nil {
return nil
}
metaInfo := item.MetaInfo
var pluginID int64
if id, err := strconv.ParseInt(metaInfo.ProductID, 10, 64); err == nil {
pluginID = id
} else {
// 如果ID不是数字使用简单的hash算法生成ID
pluginID = int64(simpleHash(metaInfo.ProductID))
}
// 创建插件清单
manifest := &model.PluginManifest{
SchemaVersion: "v1",
NameForModel: metaInfo.Name,
NameForHuman: metaInfo.Name,
DescriptionForModel: metaInfo.Description,
DescriptionForHuman: metaInfo.Description,
LogoURL: metaInfo.IconURL,
Auth: &model.AuthV2{
Type: model.AuthzTypeOfNone,
},
API: model.APIDesc{
Type: "openapi",
},
}
pluginInfo := &model.PluginInfo{
ID: pluginID,
PluginType: pluginCommon.PluginType_PLUGIN,
SpaceID: 0,
DeveloperID: 0,
APPID: nil,
IconURI: &metaInfo.IconURL,
ServerURL: ptr.Of(""),
CreatedAt: metaInfo.ListedAt,
UpdatedAt: metaInfo.ListedAt,
Manifest: manifest,
}
return entity.NewPluginInfo(pluginInfo)
}
func convertCozePluginToEntity(cozePlugin CozePlugin) *entity.PluginInfo {
var pluginID int64
if id, err := strconv.ParseInt(cozePlugin.ID, 10, 64); err == nil {
pluginID = id
} else {
// 如果ID不是数字使用简单的hash算法生成ID
pluginID = int64(simpleHash(cozePlugin.ID))
}
// 创建插件清单
manifest := &model.PluginManifest{
SchemaVersion: "v1",
NameForModel: cozePlugin.Name,
@ -142,7 +176,6 @@ func (p *pluginServiceImpl) GetSaasPluginInfo(ctx context.Context, pluginID int6
return nil, errorx.Wrapf(err, "failed to call coze.cn API")
}
// 解析响应数据
var cozePlugin CozePlugin
if err := json.Unmarshal(resp.Data, &cozePlugin); err != nil {
return nil, errorx.Wrapf(err, "failed to parse coze.cn API response")
@ -154,3 +187,45 @@ func (p *pluginServiceImpl) GetSaasPluginInfo(ctx context.Context, pluginID int6
return plugin, nil
}
func (p *pluginServiceImpl) searchSaasPlugin(ctx context.Context, req *entity.SearchSaasPluginRequest) (resp *entity.SearchSaasPluginResponse, err error) {
client := saasapi.NewCozeAPIClient()
// 构建查询参数
queryParams := make(map[string]interface{})
if req.Keyword != nil {
queryParams["keyword"] = req.Keyword
}
if req.PageNum != nil {
queryParams["page_num"] = req.PageNum
}
if req.PageSize != nil {
queryParams["page_size"] = req.PageSize
}
if req.SortType != nil {
queryParams["sort_type"] = req.SortType
}
if req.CategoryID != nil {
queryParams["category_id"] = req.CategoryID
}
if req.IsOfficial != nil {
queryParams["is_official"] = req.IsOfficial
}
apiResp, err := client.GetWithQuery(ctx, "/v1/stores/plugins", queryParams)
if err != nil {
return nil, errorx.Wrapf(err, "failed to call coze.cn search API")
}
var searchResp entity.SearchSaasPluginResponse
if err := json.Unmarshal(apiResp.Data, &searchResp.Data); err != nil {
return nil, errorx.Wrapf(err, "failed to parse coze.cn search API response")
}
searchResp.Code = apiResp.Code
searchResp.Msg = apiResp.Msg
logs.CtxInfof(ctx, "searched SaaS plugins from coze.cn, found %d items", len(searchResp.Data.Items))
return &searchResp, nil
}

View File

@ -79,7 +79,7 @@ type PluginService interface {
GetOAuthStatus(ctx context.Context, userID, pluginID int64) (resp *dto.GetOAuthStatusResponse, err error)
GetAgentPluginsOAuthStatus(ctx context.Context, userID, agentID int64) (status []*dto.AgentPluginOAuthStatus, err error)
//Saas Plugin
//Saas Plugin Product
ListSaasPluginProducts(ctx context.Context, req *ListPluginProductsRequest) (resp *ListPluginProductsResponse, err error)
GetSaasPluginInfo(ctx context.Context, pluginID int64) (plugin *entity.PluginInfo, err error)

View File

@ -0,0 +1,151 @@
/*
* 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 entity
// BenefitType represents the type of benefit
type BenefitType int32
const (
BenefitTypeUnknown BenefitType = 0
// Add specific benefit types as needed
)
// UserLevel represents the user level
type UserLevel int32
const (
UserLevelUnknown UserLevel = 0
UserLevelBasic UserLevel = 1
UserLevelPro UserLevel = 2
// Add more levels as needed
)
// EntityBenefitStatus represents the status of a benefit entity
type EntityBenefitStatus int32
const (
EntityBenefitStatusUnknown EntityBenefitStatus = 0
EntityBenefitStatusActive EntityBenefitStatus = 1
EntityBenefitStatusExpired EntityBenefitStatus = 2
// Add more statuses as needed
)
// ResourceUsageStrategy represents the resource usage strategy
type ResourceUsageStrategy int32
const (
ResourceUsageStrategyUnknown ResourceUsageStrategy = 0
ResourceUsageStrategyByQuota ResourceUsageStrategy = 1
// Add more strategies as needed
)
// GetEnterpriseBenefitRequest represents the request for getting enterprise benefit
type GetEnterpriseBenefitRequest struct {
BenefitType *BenefitType `json:"benefit_type,omitempty" form:"benefit_type"`
ResourceID *string `json:"resource_id,omitempty" form:"resource_id"`
}
// GetEnterpriseBenefitResponse represents the response for getting enterprise benefit
type GetEnterpriseBenefitResponse struct {
Code int32 `json:"code"`
Message string `json:"message"`
Data *BenefitData `json:"data,omitempty"`
}
// BenefitData represents the benefit data
type BenefitData struct {
BasicInfo *BasicInfo `json:"basic_info,omitempty"`
BenefitInfo *BenefitInfo `json:"benefit_info,omitempty"`
}
// BasicInfo represents the basic information
type BasicInfo struct {
UserLevel UserLevel `json:"user_level"`
}
// BenefitInfo represents the benefit information
type BenefitInfo struct {
ResourceID *string `json:"resource_id,omitempty"`
BenefitType *BenefitType `json:"benefit_type,omitempty"`
Basic *BenefitTypeInfoItem `json:"basic,omitempty"` // Basic value
Extra []*BenefitTypeInfoItem `json:"extra,omitempty"` // Extra values, may not exist
}
// BenefitTypeInfoItem represents a benefit type info item
type BenefitTypeInfoItem struct {
ItemID *string `json:"item_id,omitempty"`
ItemInfo *CommonCounter `json:"item_info,omitempty"`
Status *EntityBenefitStatus `json:"status,omitempty"`
BenefitID *string `json:"benefit_id,omitempty"`
}
// CommonCounter represents a common counter
type CommonCounter struct {
Used *float64 `json:"used,omitempty"` // Used amount when Strategy == ByQuota, returns 0 if no usage data
Total *float64 `json:"total,omitempty"` // Total limit when Strategy == ByQuota
Strategy *ResourceUsageStrategy `json:"strategy,omitempty"` // Resource usage strategy
StartAt *int64 `json:"start_at,omitempty"` // Start time in seconds
EndAt *int64 `json:"end_at,omitempty"` // End time in seconds
}
// String methods for enums (for better debugging and logging)
func (bt BenefitType) String() string {
switch bt {
case BenefitTypeUnknown:
return "Unknown"
default:
return "Unknown"
}
}
func (ul UserLevel) String() string {
switch ul {
case UserLevelUnknown:
return "Unknown"
case UserLevelBasic:
return "Basic"
case UserLevelPro:
return "Pro"
default:
return "Unknown"
}
}
func (ebs EntityBenefitStatus) String() string {
switch ebs {
case EntityBenefitStatusUnknown:
return "Unknown"
case EntityBenefitStatusActive:
return "Active"
case EntityBenefitStatusExpired:
return "Expired"
default:
return "Unknown"
}
}
func (rus ResourceUsageStrategy) String() string {
switch rus {
case ResourceUsageStrategyUnknown:
return "Unknown"
case ResourceUsageStrategyByQuota:
return "ByQuota"
default:
return "Unknown"
}
}

View File

@ -83,28 +83,52 @@ func (s *CozeUserService) GetUserInfo(ctx context.Context, userID int64) (*entit
}, nil
}
// GetUserBenefit calls the /v1/users/benefit endpoint
func (s *CozeUserService) GetUserBenefit(ctx context.Context, userID int64) (*entity.UserBenefit, error) {
resp, err := s.client.Get(ctx, "/v1/users/benefit")
// GetEnterpriseBenefit calls the /v1/commerce/benefit/benefits/get endpoint with query parameters
func (s *CozeUserService) GetEnterpriseBenefit(ctx context.Context, req *entity.GetEnterpriseBenefitRequest) (*entity.GetEnterpriseBenefitResponse, error) {
// Build query parameters
queryParams := make(map[string]interface{})
if req.BenefitType != nil {
queryParams["benefit_type"] = int32(*req.BenefitType)
}
if req.ResourceID != nil {
queryParams["resource_id"] = *req.ResourceID
}
// Call API
resp, err := s.client.GetWithQuery(ctx, "/v1/commerce/benefit/benefits/get", queryParams)
if err != nil {
logs.CtxErrorf(ctx, "failed to call GetUserBenefit API: %v", err)
logs.CtxErrorf(ctx, "failed to call GetEnterpriseBenefit API: %v", err)
return nil, errorx.New(errno.ErrUserResourceNotFound, errorx.KV("reason", "API call failed"))
}
// Parse the data field
var benefitData struct {
UserID int64 `json:"user_id"`
// Add more fields here when API response format is confirmed
}
if err := json.Unmarshal(resp.Data, &benefitData); err != nil {
// Parse response data
var benefitResp entity.GetEnterpriseBenefitResponse
if err := json.Unmarshal(resp.Data, &benefitResp.Data); err != nil {
logs.CtxErrorf(ctx, "failed to parse benefit data: %v", err)
return nil, errorx.New(errno.ErrUserResourceNotFound, errorx.KV("reason", "data parse failed"))
}
// Map to entity.UserBenefit
// Set response basic information
benefitResp.Code = int32(resp.Code)
benefitResp.Message = resp.Msg
logs.CtxInfof(ctx, "successfully retrieved enterprise benefit data")
return &benefitResp, nil
}
// GetUserBenefit calls the /v1/users/benefit endpoint (legacy method, kept for backward compatibility)
func (s *CozeUserService) GetUserBenefit(ctx context.Context, userID int64) (*entity.UserBenefit, error) {
// Use new enterprise benefit interface without query parameters
req := &entity.GetEnterpriseBenefitRequest{}
_, err := s.GetEnterpriseBenefit(ctx, req)
if err != nil {
return nil, err
}
// Convert to old UserBenefit format for backward compatibility
return &entity.UserBenefit{
UserID: benefitData.UserID,
UserID: userID,
}, nil
}
@ -128,3 +152,8 @@ func (u *userImpl) GetSaasUserInfo(ctx context.Context, userID int64) (*entity.U
func (u *userImpl) GetUserBenefit(ctx context.Context, userID int64) (*entity.UserBenefit, error) {
return getCozeUserService().GetUserBenefit(ctx, userID)
}
// GetEnterpriseBenefit gets enterprise benefit with query parameters
func (u *userImpl) GetEnterpriseBenefit(ctx context.Context, req *entity.GetEnterpriseBenefitRequest) (*entity.GetEnterpriseBenefitResponse, error) {
return getCozeUserService().GetEnterpriseBenefit(ctx, req)
}

View File

@ -23,7 +23,9 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"time"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
@ -61,6 +63,11 @@ func (c *CozeAPIClient) Get(ctx context.Context, path string) (*CozeAPIResponse,
return c.request(ctx, "GET", path, nil)
}
// GetWithQuery performs a GET request to the coze.cn API with query parameters
func (c *CozeAPIClient) GetWithQuery(ctx context.Context, path string, queryParams map[string]interface{}) (*CozeAPIResponse, error) {
return c.requestWithQuery(ctx, "GET", path, nil, queryParams)
}
// Post performs a POST request to the coze.cn API
func (c *CozeAPIClient) Post(ctx context.Context, path string, body interface{}) (*CozeAPIResponse, error) {
var bodyBytes []byte
@ -146,6 +153,114 @@ func (c *CozeAPIClient) request(ctx context.Context, method, path string, body [
return &apiResp, nil
}
// requestWithQuery is the core method for making HTTP requests to coze.cn API with query parameters
func (c *CozeAPIClient) requestWithQuery(ctx context.Context, method, path string, body []byte, queryParams map[string]interface{}) (*CozeAPIResponse, error) {
baseURL := fmt.Sprintf("%s%s", c.BaseURL, path)
// Build query parameters
if queryParams != nil && len(queryParams) > 0 {
u, err := url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
q := u.Query()
for key, value := range queryParams {
if value != nil {
switch v := value.(type) {
case string:
if v != "" {
q.Set(key, v)
}
case int:
q.Set(key, strconv.Itoa(v))
case bool:
q.Set(key, strconv.FormatBool(v))
case *string:
if v != nil && *v != "" {
q.Set(key, *v)
}
case *int:
if v != nil {
q.Set(key, strconv.Itoa(*v))
}
case *bool:
if v != nil {
q.Set(key, strconv.FormatBool(*v))
}
}
}
}
u.RawQuery = q.Encode()
baseURL = u.String()
}
var req *http.Request
var err error
if body != nil {
req, err = http.NewRequestWithContext(ctx, method, baseURL, bytes.NewReader(body))
} else {
req, err = http.NewRequestWithContext(ctx, method, baseURL, nil)
}
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Add API key if available
if c.APIKey != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))
}
// Make request with retries
var resp *http.Response
for i := 0; i <= c.MaxRetries; i++ {
resp, err = c.HTTPClient.Do(req)
if err == nil {
break
}
if i < c.MaxRetries {
logs.CtxWarnf(ctx, "coze API request failed, retrying (%d/%d): %v", i+1, c.MaxRetries, err)
time.Sleep(time.Duration(i+1) * time.Second)
}
}
if err != nil {
return nil, fmt.Errorf("request failed after %d retries: %w", c.MaxRetries, err)
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Check HTTP status code
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
}
// Parse response
var apiResp CozeAPIResponse
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse API response: %w", err)
}
// Check API response code
if apiResp.Code != 0 {
return nil, fmt.Errorf("API returned error: code=%d, msg=%s", apiResp.Code, apiResp.Msg)
}
return &apiResp, nil
}
// getEnvOrDefault returns environment variable value or default if not set
func getSaasOpenAPIUrl() string {
if value := os.Getenv("COZE_SAAS_API_BASE_URL"); value != "" {

View File

@ -2,6 +2,20 @@ include "marketplace_common.thrift"
namespace go marketplace.product_common
enum BotModType {
SingleAgent = 1,
MultiAgent = 2,
}
enum Component {
UsePlugin = 1,
UseWorkFlow = 2,
UseKnowledge = 3,
UseVoice = 4,
UseCard = 5,
UseImageWorkflow = 6,
}
enum ProductEntityType {
Bot = 1 ,
Plugin = 2 ,

View File

@ -11,8 +11,73 @@ service PublicProductService {
FavoriteProductResponse PublicFavoriteProduct(1: FavoriteProductRequest req)(api.post = "/api/marketplace/product/favorite", api.category = "PublicAPI")
GetUserFavoriteListV2Response PublicGetUserFavoriteListV2(1: GetUserFavoriteListV2Request req)(api.get = "/api/marketplace/product/favorite/list.v2", api.category = "PublicAPI")
DuplicateProductResponse PublicDuplicateProduct (1: DuplicateProductRequest req) (api.post = "/api/marketplace/product/duplicate", api.category = "PublicAPI")
SearchProductResponse PublicSearchProduct(1: SearchProductRequest req)(api.get = "/api/marketplace/product/search", api.category = "PublicAPI")
SearchSuggestResponse PublicSearchSuggest(1: SearchSuggestRequest req)(api.get = "/api/marketplace/product/search/suggest", api.category = "PublicAPI")
}
struct SearchProductRequest{
1 : required string Keyword (api.query = "keyword") ,
2 : required i32 PageNum (api.query = "page_num") ,
3 : required i32 PageSize (api.query = "page_size") ,
4 : optional product_common.ProductEntityType EntityType (agw.key = "entity_type") ,
5 : optional product_common.SortType SortType (api.query = "sort_type") ,
11 : optional product_common.ProductPublishMode PublishMode (agw.key = "publish_mode") , // Open/closed source
12 : optional list<i64> ModelIDs (agw.js_conv="str", agw.cli_conv="str", api.query = "model_ids") , // Models used
13 : optional product_common.BotModType BotModType (agw.key = "bot_mod_type") , // Multimodal type
14 : optional list<product_common.Component> Components (agw.key = "components") , // Sub-attributes
15 : optional list<i64> PublishPlatformIDs (agw.js_conv="str", agw.cli_conv="str", api.query = "publish_platform_ids"), // Publish platform IDs
16 : optional list<i64> CategoryIDs (agw.js_conv="str", agw.cli_conv="str", api.query = "category_ids"), // Product category IDs
17 : optional bool IsOfficial (api.query = "is_official"), // Is official
18 : optional bool IsRecommend (api.query = "is_recommend"), // Is recommended
19 : optional list<product_common.ProductEntityType> EntityTypes ( api.query = "entity_types"), // Product type list, use this parameter first, then EntityType
20 : optional product_common.PluginType PluginType (agw.key = "plugin_type"), // Plugin type
21 : optional product_common.ProductPaidType ProductPaidType (agw.key = "product_paid_type"), // Product paid type
255: optional base.Base Base ,
}
struct SearchProductResponse{
1 : required i32 Code (agw.key = "code") ,
2 : required string Message (agw.key = "message"),
3 : optional SearchProductResponseData Data (agw.key = "data") ,
255: optional base.BaseResp BaseResp ,
}
struct SearchProductResponseData{
1: optional list<ProductInfo> Products (agw.key = "products"),
2: optional i32 Total (agw.key = "total") ,
3: optional bool HasMore (agw.key = "has_more"),
4: optional map<product_common.ProductEntityType, i32> EntityTotal (agw.key = "entity_total"), // Entity count
}
struct SearchSuggestResponse{
1 : required i32 Code (agw.key = "code") ,
2 : required string Message (agw.key = "message"),
3 : optional SearchSuggestResponseData Data (agw.key = "data") ,
255: optional base.BaseResp BaseResp ,
}
struct SearchSuggestResponseData{
1: optional list<ProductMetaInfo> Suggestions (agw.key = "suggestions"), // Deprecated
2: optional bool HasMore (agw.key = "has_more"),
3: optional list<ProductInfo> SuggestionV2(agw.key = "suggestion_v2"),
}
struct SearchSuggestRequest {
1: optional string Keyword (api.query = "keyword"),
2: optional product_common.ProductEntityType EntityType (api.query = "entity_type"), // Optional, defaults to bot recommendation if not provided
3: optional i32 PageNum (api.query = "page_num"),
4: optional i32 PageSize (api.query = "page_size"),
5: optional list<product_common.ProductEntityType> EntityTypes (api.query = "entity_types"), // Product type list, use this parameter first, then EntityType
255: optional base.Base Base ,
}
struct FavoriteProductResponse {
1 : required i32 Code (agw.key = "code", api.body = "code") ,
2 : required string Message (agw.key = "message", api.body = "message") ,
@ -566,4 +631,4 @@ struct DuplicateProductData {
// New ID after copy
1: i64 NewEntityID (agw.js_conv="str", api.js_conv="str", agw.cli_conv="str", api.body = "new_entity_id")
2: optional i64 NewPluginID (agw.js_conv="str", api.js_conv="str", agw.cli_conv="str", api.body = "new_plugin_id") // Plugin ID for workflow
}
}