chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:24:55 +08:00
commit 7baeb17845
15305 changed files with 2077301 additions and 0 deletions
@@ -0,0 +1,473 @@
/*
* 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 plugin
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/bytedance/sonic"
"github.com/getkin/kin-openapi/openapi3"
gonanoid "github.com/matoous/go-nanoid"
pluginAPI "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop"
common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common"
resCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/consts"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/convert"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/convert/api"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/model"
"github.com/coze-dev/coze-studio/backend/domain/plugin/dto"
"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
"github.com/coze-dev/coze-studio/backend/domain/plugin/repository"
searchEntity "github.com/coze-dev/coze-studio/backend/domain/search/entity"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
func (p *PluginApplicationService) GetPluginAPIs(ctx context.Context, req *pluginAPI.GetPluginAPIsRequest) (resp *pluginAPI.GetPluginAPIsResponse, err error) {
pl, err := p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateGetPluginAPIsRequest failed")
}
var (
draftTools []*entity.ToolInfo
total int64
)
if len(req.APIIds) > 0 {
toolIDs := make([]int64, 0, len(req.APIIds))
for _, id := range req.APIIds {
toolID, pErr := strconv.ParseInt(id, 10, 64)
if pErr != nil {
return nil, fmt.Errorf("invalid tool id '%s'", id)
}
toolIDs = append(toolIDs, toolID)
}
draftTools, err = p.toolRepo.MGetDraftTools(ctx, toolIDs)
if err != nil {
return nil, errorx.Wrapf(err, "MGetDraftTools failed, toolIDs=%v", toolIDs)
}
total = int64(len(draftTools))
} else {
pageInfo := dto.PageInfo{
Page: int(req.Page),
Size: int(req.Size),
SortBy: ptr.Of(dto.SortByCreatedAt),
OrderByACS: ptr.Of(false),
}
draftTools, total, err = p.toolRepo.ListPluginDraftTools(ctx, req.PluginID, pageInfo)
if err != nil {
return nil, errorx.Wrapf(err, "ListPluginDraftTools failed, pluginID=%d", req.PluginID)
}
}
if len(draftTools) == 0 {
return &pluginAPI.GetPluginAPIsResponse{
APIInfo: make([]*common.PluginAPIInfo, 0),
Total: 0,
}, nil
}
draftToolIDs := slices.Transform(draftTools, func(tl *entity.ToolInfo) int64 {
return tl.ID
})
onlineStatus, err := p.getToolOnlineStatus(ctx, draftToolIDs)
if err != nil {
return nil, err
}
apis := make([]*common.PluginAPIInfo, 0, len(draftTools))
for _, tool := range draftTools {
method, ok := convert.ToThriftAPIMethod(tool.GetMethod())
if !ok {
return nil, fmt.Errorf("invalid method '%s'", tool.GetMethod())
}
reqParams, err := tool.ToReqAPIParameter()
if err != nil {
return nil, err
}
respParams, err := tool.ToRespAPIParameter()
if err != nil {
return nil, err
}
var apiExtend *common.APIExtend
if tmp, ok := tool.Operation.Extensions[consts.APISchemaExtendAuthMode].(string); ok {
if mode, ok := convert.ToThriftAPIAuthMode(consts.ToolAuthMode(tmp)); ok {
apiExtend = &common.APIExtend{
AuthMode: mode,
}
}
}
api := &common.PluginAPIInfo{
APIID: strconv.FormatInt(tool.ID, 10),
CreateTime: strconv.FormatInt(tool.CreatedAt/1000, 10),
DebugStatus: tool.GetDebugStatus(),
Desc: tool.GetDesc(),
Disabled: func() bool {
return tool.IsDeactivated()
}(),
Method: method,
Name: tool.GetName(),
OnlineStatus: onlineStatus[tool.ID],
Path: tool.GetSubURL(),
PluginID: strconv.FormatInt(tool.PluginID, 10),
RequestParams: reqParams,
ResponseParams: respParams,
StatisticData: common.NewPluginStatisticData(),
APIExtend: apiExtend,
}
example := pl.GetToolExample(ctx, tool.GetName())
if example != nil {
api.DebugExample = &common.DebugExample{
ReqExample: example.RequestExample,
RespExample: example.ResponseExample,
}
api.DebugExampleStatus = common.DebugExampleStatus_Enable
}
apis = append(apis, api)
}
resp = &pluginAPI.GetPluginAPIsResponse{
APIInfo: apis,
Total: int32(total),
}
return resp, nil
}
func (p *PluginApplicationService) CreateAPI(ctx context.Context, req *pluginAPI.CreateAPIRequest) (resp *pluginAPI.CreateAPIResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateCreateAPIRequest failed")
}
defaultSubURL := gonanoid.MustID(6)
tool := &entity.ToolInfo{
PluginID: req.PluginID,
ActivatedStatus: ptr.Of(consts.ActivateTool),
DebugStatus: ptr.Of(common.APIDebugStatus_DebugWaiting),
SubURL: ptr.Of("/" + defaultSubURL),
Method: ptr.Of(http.MethodGet),
Operation: model.NewOpenapi3Operation(&openapi3.Operation{
Summary: req.Desc,
OperationID: req.Name,
Parameters: []*openapi3.ParameterRef{},
RequestBody: model.DefaultOpenapi3RequestBody(),
Responses: model.DefaultOpenapi3Responses(),
Extensions: map[string]any{},
}),
}
toolID, err := p.toolRepo.CreateDraftTool(ctx, tool)
if err != nil {
return nil, errorx.Wrapf(err, "CreateDraftTool failed, pluginID=%d", req.PluginID)
}
resp = &pluginAPI.CreateAPIResponse{
APIID: strconv.FormatInt(toolID, 10),
}
return resp, nil
}
func (p *PluginApplicationService) UpdateAPI(ctx context.Context, req *pluginAPI.UpdateAPIRequest) (resp *pluginAPI.UpdateAPIResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateUpdateAPIRequest failed")
}
op, err := api.APIParamsToOpenapiOperation(req.RequestParams, req.ResponseParams)
if err != nil {
return nil, err
}
var method *string
if m, ok := convert.ToHTTPMethod(req.GetMethod()); ok {
method = &m
}
updateReq := &dto.UpdateDraftToolRequest{
PluginID: req.PluginID,
ToolID: req.APIID,
Name: req.Name,
Desc: req.Desc,
SubURL: req.Path,
Method: method,
Parameters: op.Parameters,
RequestBody: op.RequestBody,
Responses: op.Responses,
Disabled: req.Disabled,
SaveExample: req.SaveExample,
DebugExample: req.DebugExample,
APIExtend: req.APIExtend,
}
err = p.DomainSVC.UpdateDraftTool(ctx, updateReq)
if err != nil {
return nil, errorx.Wrapf(err, "UpdateDraftTool failed, pluginID=%d, toolID=%d", updateReq.PluginID, updateReq.ToolID)
}
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Updated,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResID: req.PluginID,
UpdateTimeMS: ptr.Of(time.Now().UnixMilli()),
},
})
if err != nil {
logs.CtxErrorf(ctx, "publish resource '%d' failed, err=%v", req.PluginID, err)
}
resp = &pluginAPI.UpdateAPIResponse{}
return resp, nil
}
func (p *PluginApplicationService) DeleteAPI(ctx context.Context, req *pluginAPI.DeleteAPIRequest) (resp *pluginAPI.DeleteAPIResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateDeleteAPIRequest failed")
}
err = p.toolRepo.DeleteDraftTool(ctx, req.APIID)
if err != nil {
return nil, errorx.Wrapf(err, "DeleteDraftTool failed, toolID=%d", req.APIID)
}
resp = &pluginAPI.DeleteAPIResponse{}
return resp, nil
}
func (p *PluginApplicationService) BatchCreateAPI(ctx context.Context, req *pluginAPI.BatchCreateAPIRequest) (resp *pluginAPI.BatchCreateAPIResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateBatchCreateAPIRequest failed")
}
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData([]byte(req.Openapi))
if err != nil {
return nil, errorx.New(errno.ErrPluginInvalidOpenapi3Doc, errorx.KV(errno.PluginMsgKey, err.Error()))
}
res, err := p.DomainSVC.CreateDraftToolsWithCode(ctx, &dto.CreateDraftToolsWithCodeRequest{
PluginID: req.PluginID,
OpenapiDoc: ptr.Of(model.Openapi3T(*doc)),
ConflictAndUpdate: req.ReplaceSamePaths,
})
if err != nil {
return nil, errorx.Wrapf(err, "CreateDraftToolsWithCode failed, pluginID=%d", req.PluginID)
}
duplicated := slices.Transform(res.DuplicatedTools, func(e dto.UniqueToolAPI) *common.PluginAPIInfo {
method, _ := convert.ToThriftAPIMethod(e.Method)
return &common.PluginAPIInfo{
Path: e.SubURL,
Method: method,
}
})
resp = &pluginAPI.BatchCreateAPIResponse{
PathsDuplicated: duplicated,
}
if len(duplicated) > 0 {
resp.Code = errno.ErrPluginDuplicatedTool
}
return resp, nil
}
func (p *PluginApplicationService) GetUpdatedAPIs(ctx context.Context, req *pluginAPI.GetUpdatedAPIsRequest) (resp *pluginAPI.GetUpdatedAPIsResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateGetUpdatedAPIsRequest failed")
}
draftTools, err := p.toolRepo.GetPluginAllDraftTools(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "GetPluginAllDraftTools failed, pluginID=%d", req.PluginID)
}
onlineTools, err := p.toolRepo.GetPluginAllOnlineTools(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "GetPluginAllOnlineTools failed, pluginID=%d", req.PluginID)
}
var updatedToolName, createdToolName, delToolName []string
draftMap := slices.ToMap(draftTools, func(e *entity.ToolInfo) (string, *entity.ToolInfo) {
return e.GetName(), e
})
onlineMap := slices.ToMap(onlineTools, func(e *entity.ToolInfo) (string, *entity.ToolInfo) {
return e.GetName(), e
})
for name := range draftMap {
if _, ok := onlineMap[name]; !ok {
createdToolName = append(createdToolName, name)
}
}
for name, ot := range onlineMap {
dt, ok := draftMap[name]
if !ok {
delToolName = append(delToolName, name)
continue
}
if ot.GetMethod() != dt.GetMethod() ||
ot.GetSubURL() != dt.GetSubURL() ||
ot.GetDesc() != dt.GetDesc() {
updatedToolName = append(updatedToolName, name)
continue
}
os, err := sonic.MarshalString(ot.Operation)
if err != nil {
logs.CtxErrorf(ctx, "marshal online tool operation failed, toolID=%d, err=%v", ot.ID, err)
updatedToolName = append(updatedToolName, name)
continue
}
ds, err := sonic.MarshalString(dt.Operation)
if err != nil {
logs.CtxErrorf(ctx, "marshal draft tool operation failed, toolID=%d, err=%v", ot.ID, err)
updatedToolName = append(updatedToolName, name)
continue
}
if os != ds {
updatedToolName = append(updatedToolName, name)
}
}
resp = &pluginAPI.GetUpdatedAPIsResponse{
UpdatedAPINames: updatedToolName,
CreatedAPINames: createdToolName,
DeletedAPINames: delToolName,
}
return resp, nil
}
func (p *PluginApplicationService) DebugAPI(ctx context.Context, req *pluginAPI.DebugAPIRequest) (resp *pluginAPI.DebugAPIResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateDebugAPIRequest failed")
}
const defaultErrReason = "internal server error"
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))
}
resp = &pluginAPI.DebugAPIResponse{
Success: false,
RawReq: "{}",
RawResp: "{}",
Resp: "{}",
}
opts := []model.ExecuteToolOpt{}
switch req.Operation {
case common.DebugOperation_Debug:
opts = append(opts, model.WithInvalidRespProcessStrategy(consts.InvalidResponseProcessStrategyOfReturnErr))
case common.DebugOperation_Parse:
opts = append(opts, model.WithAutoGenRespSchema(),
model.WithInvalidRespProcessStrategy(consts.InvalidResponseProcessStrategyOfReturnRaw),
)
}
res, err := p.DomainSVC.ExecuteTool(ctx, &model.ExecuteToolRequest{
UserID: conv.Int64ToStr(*userID),
PluginID: req.PluginID,
ToolID: req.APIID,
ExecScene: consts.ExecSceneOfToolDebug,
ExecDraftTool: true,
ArgumentsInJson: req.Parameters,
}, opts...)
if err != nil {
var e errorx.StatusError
if errors.As(err, &e) {
resp.Reason = e.Msg()
return resp, nil
}
logs.CtxErrorf(ctx, "ExecuteTool failed, err=%v", err)
resp.Reason = defaultErrReason
return resp, nil
}
resp = &pluginAPI.DebugAPIResponse{
Success: true,
Resp: res.TrimmedResp,
RawReq: res.Request,
RawResp: res.RawResp,
ResponseParams: []*common.APIParameter{},
}
if req.Operation == common.DebugOperation_Parse {
res.Tool.Operation.Responses = res.RespSchema
}
respParams, err := res.Tool.ToRespAPIParameter()
if err != nil {
logs.CtxErrorf(ctx, "ToRespAPIParameter failed, err=%v", err)
resp.Success = false
resp.Reason = defaultErrReason
} else {
resp.ResponseParams = respParams
}
return resp, nil
}
func (p *PluginApplicationService) getToolOnlineStatus(ctx context.Context, toolIDs []int64) (map[int64]common.OnlineStatus, error) {
onlineTools, err := p.toolRepo.MGetOnlineTools(ctx, toolIDs, repository.WithToolID())
if err != nil {
return nil, errorx.Wrapf(err, "MGetOnlineTools failed, toolIDs=%v", toolIDs)
}
onlineStatus := make(map[int64]common.OnlineStatus, len(onlineTools))
for _, tool := range onlineTools {
onlineStatus[tool.ID] = common.OnlineStatus_ONLINE
}
return onlineStatus, nil
}
+231
View File
@@ -0,0 +1,231 @@
/*
* 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 plugin
import (
"context"
"encoding/json"
"net/url"
"os"
botOpenAPI "github.com/coze-dev/coze-studio/backend/api/model/app/bot_open_api"
developerAPI "github.com/coze-dev/coze-studio/backend/api/model/app/developer_api"
pluginAPI "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop"
common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
pluginConf "github.com/coze-dev/coze-studio/backend/domain/plugin/conf"
"github.com/coze-dev/coze-studio/backend/domain/plugin/dto"
"github.com/coze-dev/coze-studio/backend/domain/plugin/encrypt"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
func (p *PluginApplicationService) GetOAuthSchema(ctx context.Context, req *pluginAPI.GetOAuthSchemaRequest) (resp *pluginAPI.GetOAuthSchemaResponse, err error) {
return &pluginAPI.GetOAuthSchemaResponse{
OauthSchema: pluginConf.GetOAuthSchema(),
}, nil
}
func (p *PluginApplicationService) GetOAuthStatus(ctx context.Context, req *pluginAPI.GetOAuthStatusRequest) (resp *pluginAPI.GetOAuthStatusResponse, err error) {
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))
}
res, err := p.DomainSVC.GetOAuthStatus(ctx, *userID, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "GetOAuthStatus failed, pluginID=%d", req.PluginID)
}
resp = &pluginAPI.GetOAuthStatusResponse{
IsOauth: res.IsOauth,
Status: res.Status,
Content: res.OAuthURL,
}
return resp, nil
}
func (p *PluginApplicationService) OauthAuthorizationCode(ctx context.Context, req *botOpenAPI.OauthAuthorizationCodeReq) (resp *botOpenAPI.OauthAuthorizationCodeResp, err error) {
stateStr, err := url.QueryUnescape(req.State)
if err != nil {
return nil, errorx.WrapByCode(err, errno.ErrPluginOAuthFailed, errorx.KV(errno.PluginMsgKey, "invalid state"))
}
secret := os.Getenv(encrypt.StateSecretEnv)
if secret == "" {
secret = encrypt.DefaultStateSecret
}
stateBytes, err := encrypt.DecryptByAES(stateStr, secret)
if err != nil {
return nil, errorx.WrapByCode(err, errno.ErrPluginOAuthFailed, errorx.KV(errno.PluginMsgKey, "invalid state"))
}
state := &dto.OAuthState{}
err = json.Unmarshal(stateBytes, state)
if err != nil {
return nil, errorx.WrapByCode(err, errno.ErrPluginOAuthFailed, errorx.KV(errno.PluginMsgKey, "invalid state"))
}
err = p.DomainSVC.OAuthCode(ctx, req.Code, state)
if err != nil {
return nil, errorx.WrapByCode(err, errno.ErrPluginOAuthFailed, errorx.KV(errno.PluginMsgKey, "authorize failed"))
}
resp = &botOpenAPI.OauthAuthorizationCodeResp{}
return resp, nil
}
func (p *PluginApplicationService) GetQueriedOAuthPluginList(ctx context.Context, req *pluginAPI.GetQueriedOAuthPluginListRequest) (resp *pluginAPI.GetQueriedOAuthPluginListResponse, err error) {
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))
}
status, err := p.DomainSVC.GetAgentPluginsOAuthStatus(ctx, *userID, req.BotID)
if err != nil {
return nil, errorx.Wrapf(err, "GetAgentPluginsOAuthStatus failed, userID=%d, agentID=%d", *userID, req.BotID)
}
if len(status) == 0 {
return &pluginAPI.GetQueriedOAuthPluginListResponse{
OauthPluginList: []*pluginAPI.OAuthPluginInfo{},
}, nil
}
oauthPluginList := make([]*pluginAPI.OAuthPluginInfo, 0, len(status))
for _, s := range status {
oauthPluginList = append(oauthPluginList, &pluginAPI.OAuthPluginInfo{
PluginID: s.PluginID,
Status: s.Status,
Name: s.PluginName,
PluginIcon: s.PluginIconURL,
})
}
resp = &pluginAPI.GetQueriedOAuthPluginListResponse{
OauthPluginList: oauthPluginList,
}
return resp, nil
}
func (p *PluginApplicationService) RevokeAuthToken(ctx context.Context, req *pluginAPI.RevokeAuthTokenRequest) (resp *pluginAPI.RevokeAuthTokenResponse, err error) {
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))
}
err = p.DomainSVC.RevokeAccessToken(ctx, &dto.AuthorizationCodeMeta{
UserID: conv.Int64ToStr(*userID),
PluginID: req.PluginID,
IsDraft: req.GetBotID() == 0,
})
if err != nil {
return nil, errorx.Wrapf(err, "RevokeAccessToken failed, pluginID=%d", req.PluginID)
}
resp = &pluginAPI.RevokeAuthTokenResponse{}
return resp, nil
}
func (p *PluginApplicationService) GetUserAuthority(ctx context.Context, req *pluginAPI.GetUserAuthorityRequest) (resp *pluginAPI.GetUserAuthorityResponse, err error) {
resp = &pluginAPI.GetUserAuthorityResponse{
Data: &common.GetUserAuthorityData{
CanEdit: true,
CanRead: true,
CanDelete: true,
CanDebug: true,
CanPublish: true,
CanReadChangelog: true,
},
}
return resp, nil
}
// PluginOauthAuthorizationCode handles the OAuth provider callback in the two-step confirmation flow.
// It validates the state, stores a temporary confirm_code, and returns it for redirect.
func (p *PluginApplicationService) PluginOauthAuthorizationCode(ctx context.Context, req *developerAPI.PluginOauthAuthorizationCodeReq) (confirmCode string, err error) {
if req.Code == "" {
return "", errorx.New(errno.ErrPluginOAuthFailed, errorx.KV(errno.PluginMsgKey, "code is required"))
}
if req.State == "" {
return "", errorx.New(errno.ErrPluginOAuthFailed, errorx.KV(errno.PluginMsgKey, "state is required"))
}
if req.PluginID == "" {
return "", errorx.New(errno.ErrPluginOAuthFailed, errorx.KV(errno.PluginMsgKey, "plugin_id is required"))
}
confirmCode, err = p.DomainSVC.PluginOauthCallback(ctx, req.Code, req.State, req.PluginID)
if err != nil {
return "", errorx.Wrapf(err, "PluginOauthCallback failed")
}
return confirmCode, nil
}
// PluginOauthInfo returns plugin information for the OAuth confirmation page.
func (p *PluginApplicationService) PluginOauthInfo(ctx context.Context, req *developerAPI.PluginOauthInfoReq) (resp *developerAPI.PluginOauthInfoResp, err error) {
info, err := p.DomainSVC.GetPluginOauthInfo(ctx, req.ConfirmCode)
if err != nil {
return nil, errorx.Wrapf(err, "GetPluginOauthInfo failed")
}
// Look up username from the state's userID via user service
username := ""
if info.StateUserID > 0 {
userProfile, profileErr := p.userSVC.GetUserProfiles(ctx, info.StateUserID)
if profileErr == nil && userProfile != nil {
username = userProfile.Name
if username == "" {
username = userProfile.UniqueName
}
}
}
resp = &developerAPI.PluginOauthInfoResp{
Data: &developerAPI.PluginOauthInfo{
PluginID: info.PluginID,
PluginName: info.PluginName,
PluginIcon: info.PluginIcon,
Username: username,
},
}
return resp, nil
}
// PluginOauthConfirm handles the user's confirmation of OAuth authorization.
// It uses the current session user ID to prevent phishing attacks.
func (p *PluginApplicationService) PluginOauthConfirm(ctx context.Context, req *developerAPI.PluginOauthConfirmReq) (resp *developerAPI.PluginOauthConfirmResp, err error) {
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))
}
err = p.DomainSVC.ConfirmPluginOauth(ctx, req.ConfirmCode, *userID)
if err != nil {
return nil, errorx.Wrapf(err, "ConfirmPluginOauth failed")
}
resp = &developerAPI.PluginOauthConfirmResp{}
return resp, nil
}
+340
View File
@@ -0,0 +1,340 @@
/*
* 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 plugin
import (
"context"
"fmt"
"strings"
"time"
"github.com/bytedance/sonic"
"github.com/getkin/kin-openapi/openapi3"
"gopkg.in/yaml.v3"
pluginAPI "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop"
common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common"
resCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/consts"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/convert"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/model"
"github.com/coze-dev/coze-studio/backend/domain/plugin/dto"
"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
"github.com/coze-dev/coze-studio/backend/domain/plugin/repository"
searchEntity "github.com/coze-dev/coze-studio/backend/domain/search/entity"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
func (p *PluginApplicationService) GetPluginInfo(ctx context.Context, req *pluginAPI.GetPluginInfoRequest) (resp *pluginAPI.GetPluginInfoResponse, err error) {
draftPlugin, err := p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateGetPluginInfoRequest failed")
}
metaInfo, err := p.getPluginMetaInfo(ctx, draftPlugin)
if err != nil {
return nil, err
}
codeInfo, err := p.getPluginCodeInfo(ctx, draftPlugin)
if err != nil {
return nil, err
}
_, exist, err := p.pluginRepo.GetOnlinePlugin(ctx, req.PluginID, repository.WithPluginID())
if err != nil {
return nil, errorx.Wrapf(err, "GetOnlinePlugin failed, pluginID=%d", req.PluginID)
}
resp = &pluginAPI.GetPluginInfoResponse{
MetaInfo: metaInfo,
CodeInfo: codeInfo,
Creator: common.NewCreator(),
StatisticData: common.NewPluginStatisticData(),
PluginType: draftPlugin.PluginType,
CreationMethod: common.CreationMethod_COZE,
Published: exist,
}
return resp, nil
}
func (p *PluginApplicationService) getPluginCodeInfo(ctx context.Context, draftPlugin *entity.PluginInfo) (*common.CodeInfo, error) {
tools, err := p.toolRepo.GetPluginAllDraftTools(ctx, draftPlugin.ID)
if err != nil {
return nil, errorx.Wrapf(err, "GetPluginAllDraftTools failed, pluginID=%d", draftPlugin.ID)
}
paths := openapi3.Paths{}
for _, tool := range tools {
if tool.IsDeactivated() {
continue
}
item := &openapi3.PathItem{}
item.SetOperation(tool.GetMethod(), tool.Operation.Operation)
paths[tool.GetSubURL()] = item
}
draftPlugin.OpenapiDoc.Paths = paths
manifestStr, err := sonic.MarshalString(draftPlugin.Manifest)
if err != nil {
return nil, fmt.Errorf("marshal manifest failed, err=%v", err)
}
docBytes, err := yaml.Marshal(draftPlugin.OpenapiDoc)
if err != nil {
return nil, fmt.Errorf("marshal openapi doc failed, err=%v", err)
}
codeInfo := &common.CodeInfo{
OpenapiDesc: string(docBytes),
PluginDesc: manifestStr,
}
return codeInfo, nil
}
func (p *PluginApplicationService) getPluginMetaInfo(ctx context.Context, draftPlugin *entity.PluginInfo) (*common.PluginMetaInfo, error) {
commonParams := make(map[common.ParameterLocation][]*common.CommonParamSchema, len(draftPlugin.Manifest.CommonParams))
for loc, params := range draftPlugin.Manifest.CommonParams {
location, ok := convert.ToThriftHTTPParamLocation(loc)
if !ok {
return nil, fmt.Errorf("invalid location '%s'", loc)
}
commonParams[location] = make([]*common.CommonParamSchema, 0, len(params))
for _, param := range params {
commonParams[location] = append(commonParams[location], &common.CommonParamSchema{
Name: param.Name,
Value: param.Value,
})
}
}
iconURL, err := p.oss.GetObjectUrl(ctx, draftPlugin.GetIconURI())
if err != nil {
logs.CtxWarnf(ctx, "get icon url with '%s' failed, err=%v", draftPlugin.GetIconURI(), err)
}
metaInfo := &common.PluginMetaInfo{
Name: draftPlugin.GetName(),
Desc: draftPlugin.GetDesc(),
URL: draftPlugin.GetServerURL(),
Icon: &common.PluginIcon{
URI: draftPlugin.GetIconURI(),
URL: iconURL,
},
CommonParams: commonParams,
}
err = p.fillAuthInfoInMetaInfo(ctx, draftPlugin, metaInfo)
if err != nil {
return nil, errorx.Wrapf(err, "fillAuthInfoInMetaInfo failed, pluginID=%d", draftPlugin.ID)
}
return metaInfo, nil
}
func (p *PluginApplicationService) fillAuthInfoInMetaInfo(ctx context.Context, draftPlugin *entity.PluginInfo, metaInfo *common.PluginMetaInfo) (err error) {
authInfo := draftPlugin.GetAuthInfo()
authType, ok := convert.ToThriftAuthType(authInfo.Type)
if !ok {
return fmt.Errorf("invalid auth type '%s'", authInfo.Type)
}
var subAuthType *int32
if authInfo.SubType != "" {
_subAuthType, ok := convert.ToThriftAuthSubType(authInfo.SubType)
if !ok {
return fmt.Errorf("invalid sub authz type '%s'", authInfo.SubType)
}
subAuthType = &_subAuthType
}
metaInfo.AuthType = append(metaInfo.AuthType, authType)
metaInfo.SubAuthType = subAuthType
if authType == common.AuthorizationType_None {
return nil
}
if authType == common.AuthorizationType_Service {
var loc common.AuthorizationServiceLocation
_loc := consts.HTTPParamLocation(strings.ToLower(string(authInfo.AuthOfAPIToken.Location)))
if _loc == consts.ParamInHeader {
loc = common.AuthorizationServiceLocation_Header
} else if _loc == consts.ParamInQuery {
loc = common.AuthorizationServiceLocation_Query
} else {
return fmt.Errorf("invalid location '%s'", authInfo.AuthOfAPIToken.Location)
}
metaInfo.Location = ptr.Of(loc)
metaInfo.Key = ptr.Of(authInfo.AuthOfAPIToken.Key)
metaInfo.ServiceToken = ptr.Of(authInfo.AuthOfAPIToken.ServiceToken)
}
if authType == common.AuthorizationType_OAuth {
metaInfo.OauthInfo = &authInfo.Payload
}
return nil
}
func (p *PluginApplicationService) UpdatePlugin(ctx context.Context, req *pluginAPI.UpdatePluginRequest) (resp *pluginAPI.UpdatePluginResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateUpdatePluginRequest failed")
}
userID := ctxutil.GetUIDFromCtx(ctx)
loader := openapi3.NewLoader()
_doc, err := loader.LoadFromData([]byte(req.Openapi))
if err != nil {
return nil, errorx.New(errno.ErrPluginInvalidOpenapi3Doc, errorx.KV(errno.PluginMsgKey, err.Error()))
}
doc := ptr.Of(model.Openapi3T(*_doc))
manifest := &model.PluginManifest{}
err = sonic.UnmarshalString(req.AiPlugin, manifest)
if err != nil {
return nil, errorx.New(errno.ErrPluginInvalidManifest, errorx.KV(errno.PluginMsgKey, err.Error()))
}
err = p.DomainSVC.UpdateDraftPluginWithCode(ctx, &dto.UpdateDraftPluginWithCodeRequest{
UserID: *userID,
PluginID: req.PluginID,
OpenapiDoc: doc,
Manifest: manifest,
})
if err != nil {
return nil, errorx.Wrapf(err, "UpdateDraftPluginWithCode failed, pluginID=%d", req.PluginID)
}
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Updated,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResID: req.PluginID,
Name: &manifest.NameForHuman,
UpdateTimeMS: ptr.Of(time.Now().UnixMilli()),
},
})
if err != nil {
logs.CtxErrorf(ctx, "publish resource '%d' failed, err=%v", req.PluginID, err)
}
resp = &pluginAPI.UpdatePluginResponse{
Data: &common.UpdatePluginData{
Res: true,
},
}
return resp, nil
}
func (p *PluginApplicationService) UpdatePluginMeta(ctx context.Context, req *pluginAPI.UpdatePluginMetaRequest) (resp *pluginAPI.UpdatePluginMetaResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateUpdatePluginMetaRequest failed")
}
authInfo, err := getUpdateAuthInfo(ctx, req)
if err != nil {
return nil, err
}
updateReq := &dto.UpdateDraftPluginRequest{
PluginID: req.PluginID,
Name: req.Name,
Desc: req.Desc,
URL: req.URL,
Icon: req.Icon,
CommonParams: req.CommonParams,
AuthInfo: authInfo,
}
err = p.DomainSVC.UpdateDraftPlugin(ctx, updateReq)
if err != nil {
return nil, errorx.Wrapf(err, "UpdateDraftPlugin failed, pluginID=%d", req.PluginID)
}
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Updated,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResID: req.PluginID,
Name: req.Name,
UpdateTimeMS: ptr.Of(time.Now().UnixMilli()),
},
})
if err != nil {
logs.CtxErrorf(ctx, "publish resource '%d' failed, err=%v", req.PluginID, err)
}
resp = &pluginAPI.UpdatePluginMetaResponse{}
return resp, nil
}
func getUpdateAuthInfo(ctx context.Context, req *pluginAPI.UpdatePluginMetaRequest) (authInfo *dto.PluginAuthInfo, err error) {
if req.AuthType == nil {
return nil, nil
}
_authType, ok := convert.ToAuthType(req.GetAuthType())
if !ok {
return nil, fmt.Errorf("invalid auth type '%d'", req.GetAuthType())
}
authType := &_authType
var authSubType *consts.AuthzSubType
if req.SubAuthType != nil {
_authSubType, ok := convert.ToAuthSubType(req.GetSubAuthType())
if !ok {
return nil, fmt.Errorf("invalid sub authz type '%d'", req.GetSubAuthType())
}
authSubType = &_authSubType
}
var location *consts.HTTPParamLocation
if req.Location != nil {
if *req.Location == common.AuthorizationServiceLocation_Header {
location = ptr.Of(consts.ParamInHeader)
} else if *req.Location == common.AuthorizationServiceLocation_Query {
location = ptr.Of(consts.ParamInQuery)
} else {
return nil, fmt.Errorf("invalid location '%d'", req.GetLocation())
}
}
authInfo = &dto.PluginAuthInfo{
AuthzType: authType,
Location: location,
Key: req.Key,
ServiceToken: req.ServiceToken,
OAuthInfo: req.OauthInfo,
AuthzSubType: authSubType,
AuthzPayload: req.AuthPayload,
}
return authInfo, nil
}
+140
View File
@@ -0,0 +1,140 @@
/*
* 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 plugin
import (
"context"
"strconv"
"strings"
"gorm.io/gorm"
"github.com/coze-dev/coze-studio/backend/domain/plugin/conf"
"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"
user "github.com/coze-dev/coze-studio/backend/domain/user/service"
"github.com/coze-dev/coze-studio/backend/infra/cache"
"github.com/coze-dev/coze-studio/backend/infra/idgen"
"github.com/coze-dev/coze-studio/backend/infra/storage"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
type ServiceComponents struct {
IDGen idgen.IDGenerator
DB *gorm.DB
OSS storage.Storage
CacheCli cache.Cmdable
EventBus search.ResourceEventBus
UserSVC user.User
}
func InitService(ctx context.Context, components *ServiceComponents) (*PluginApplicationService, error) {
err := conf.InitConfig(ctx)
if err != nil {
return nil, err
}
toolRepo := repository.NewToolRepo(&repository.ToolRepoComponents{
IDGen: components.IDGen,
DB: components.DB,
})
pluginRepo := repository.NewPluginRepo(&repository.PluginRepoComponents{
IDGen: components.IDGen,
DB: components.DB,
})
oauthRepo := repository.NewOAuthRepo(&repository.OAuthRepoComponents{
IDGen: components.IDGen,
DB: components.DB,
})
pluginSVC := service.NewService(&service.Components{
IDGen: components.IDGen,
DB: components.DB,
OSS: components.OSS,
CacheCli: components.CacheCli,
PluginRepo: pluginRepo,
ToolRepo: toolRepo,
OAuthRepo: oauthRepo,
})
err = checkIDExist(ctx, pluginSVC)
if err != nil {
return nil, err
}
PluginApplicationSVC.DomainSVC = pluginSVC
PluginApplicationSVC.eventbus = components.EventBus
PluginApplicationSVC.oss = components.OSS
PluginApplicationSVC.userSVC = components.UserSVC
PluginApplicationSVC.pluginRepo = pluginRepo
PluginApplicationSVC.toolRepo = toolRepo
return PluginApplicationSVC, nil
}
func checkIDExist(ctx context.Context, pluginService service.PluginService) error {
pluginProducts := conf.GetAllPluginProducts()
pluginIDs := make([]int64, 0, len(pluginProducts))
var toolIDs []int64
for _, p := range pluginProducts {
pluginIDs = append(pluginIDs, p.Info.ID)
toolIDs = append(toolIDs, p.ToolIDs...)
}
pluginInfos, err := pluginService.MGetDraftPlugins(ctx, pluginIDs)
if err != nil {
return err
}
if len(pluginInfos) > 0 {
conflictsIDs := make([]int64, 0, len(pluginInfos))
for _, p := range pluginInfos {
conflictsIDs = append(conflictsIDs, p.ID)
}
return errorx.New(errno.ErrPluginIDExist,
errorx.KV("plugin_id", strings.Join(slices.Transform(conflictsIDs, func(id int64) string {
return strconv.FormatInt(id, 10)
}), ",")),
)
}
tools, err := pluginService.MGetDraftTools(ctx, toolIDs)
if err != nil {
return err
}
if len(tools) > 0 {
conflictsIDs := make([]int64, 0, len(tools))
for _, t := range tools {
conflictsIDs = append(conflictsIDs, t.ID)
}
return errorx.New(errno.ErrToolIDExist,
errorx.KV("tool_id", strings.Join(slices.Transform(conflictsIDs, func(id int64) string {
return strconv.FormatInt(id, 10)
}), ",")),
)
}
return nil
}
+262
View File
@@ -0,0 +1,262 @@
/*
* 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 plugin
import (
"context"
"time"
pluginAPI "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop"
common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common"
resCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/model"
"github.com/coze-dev/coze-studio/backend/domain/plugin/dto"
"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
searchEntity "github.com/coze-dev/coze-studio/backend/domain/search/entity"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
func (p *PluginApplicationService) PublishPlugin(ctx context.Context, req *pluginAPI.PublishPluginRequest) (resp *pluginAPI.PublishPluginResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validatePublishPluginRequest failed")
}
err = p.DomainSVC.PublishPlugin(ctx, &model.PublishPluginRequest{
PluginID: req.PluginID,
Version: req.VersionName,
VersionDesc: req.VersionDesc,
})
if err != nil {
return nil, errorx.Wrapf(err, "PublishPlugin failed, pluginID=%d", req.PluginID)
}
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Updated,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResID: req.PluginID,
PublishStatus: ptr.Of(resCommon.PublishStatus_Published),
PublishTimeMS: ptr.Of(time.Now().UnixMilli()),
},
})
if err != nil {
logs.CtxErrorf(ctx, "publish resource '%d' failed, err=%v", req.PluginID, err)
}
resp = &pluginAPI.PublishPluginResponse{}
return resp, nil
}
func (p *PluginApplicationService) DelPlugin(ctx context.Context, req *pluginAPI.DelPluginRequest) (resp *pluginAPI.DelPluginResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateDelPluginRequest failed")
}
err = p.DomainSVC.DeleteDraftPlugin(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "DeleteDraftPlugin failed, pluginID=%d", req.PluginID)
}
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Deleted,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResID: req.PluginID,
UpdateTimeMS: ptr.Of(time.Now().UnixMilli()),
},
})
if err != nil {
return nil, errorx.Wrapf(err, "publish resource '%d' failed", req.PluginID)
}
resp = &pluginAPI.DelPluginResponse{}
return resp, nil
}
func (p *PluginApplicationService) GetPluginNextVersion(ctx context.Context, req *pluginAPI.GetPluginNextVersionRequest) (resp *pluginAPI.GetPluginNextVersionResponse, err error) {
_, err = p.validateDraftPluginAccess(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "validateGetPluginNextVersionRequest failed")
}
nextVersion, err := p.DomainSVC.GetPluginNextVersion(ctx, req.PluginID)
if err != nil {
return nil, errorx.Wrapf(err, "GetPluginNextVersion failed, pluginID=%d", req.PluginID)
}
resp = &pluginAPI.GetPluginNextVersionResponse{
NextVersionName: nextVersion,
}
return resp, nil
}
func (p *PluginApplicationService) GetDevPluginList(ctx context.Context, req *pluginAPI.GetDevPluginListRequest) (resp *pluginAPI.GetDevPluginListResponse, err error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))
}
pageInfo := dto.PageInfo{
Name: req.Name,
Page: int(req.GetPage()),
Size: int(req.GetSize()),
OrderByACS: ptr.Of(false),
}
if req.GetOrderBy() == common.OrderBy_UpdateTime {
pageInfo.SortBy = ptr.Of(dto.SortByUpdatedAt)
} else {
pageInfo.SortBy = ptr.Of(dto.SortByCreatedAt)
}
res, err := p.DomainSVC.ListDraftPlugins(ctx, &dto.ListDraftPluginsRequest{
SpaceID: req.SpaceID,
APPID: req.ProjectID,
PageInfo: pageInfo,
})
if err != nil {
return nil, errorx.Wrapf(err, "ListDraftPlugins failed, spaceID=%d, appID=%d", req.SpaceID, req.ProjectID)
}
pluginList := make([]*common.PluginInfoForPlayground, 0, len(res.Plugins))
for _, pl := range res.Plugins {
if pl.DeveloperID > 0 && pl.DeveloperID != *uid {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "plugin developer is not current user"))
}
tools, err := p.toolRepo.GetPluginAllDraftTools(ctx, pl.ID)
if err != nil {
return nil, errorx.Wrapf(err, "GetPluginAllDraftTools failed, pluginID=%d", pl.ID)
}
pluginInfo, err := p.toPluginInfoForPlayground(ctx, pl, tools)
if err != nil {
return nil, err
}
pluginInfo.VersionTs = "0" // when you get the plugin information in the project, version ts is set to 0 by default
pluginList = append(pluginList, pluginInfo)
}
resp = &pluginAPI.GetDevPluginListResponse{
PluginList: pluginList,
Total: res.Total,
}
return resp, nil
}
func (p *PluginApplicationService) DeleteAPPAllPlugins(ctx context.Context, appID int64) (err error) {
pluginIDs, err := p.DomainSVC.DeleteAPPAllPlugins(ctx, appID)
if err != nil {
return errorx.Wrapf(err, "DeleteAPPAllPlugins failed, appID=%d", appID)
}
for _, id := range pluginIDs {
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Deleted,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResID: id,
},
})
if err != nil {
return errorx.Wrapf(err, "publish resource '%d' failed", id)
}
}
return nil
}
func (p *PluginApplicationService) CopyPlugin(ctx context.Context, req *dto.CopyPluginRequest) (resp *dto.CopyPluginResponse, err error) {
res, err := p.DomainSVC.CopyPlugin(ctx, &dto.CopyPluginRequest{
UserID: req.UserID,
PluginID: req.PluginID,
CopyScene: req.CopyScene,
TargetAPPID: req.TargetAPPID,
})
if err != nil {
return nil, errorx.Wrapf(err, "CopyPlugin failed, pluginID=%d", req.PluginID)
}
plugin := res.Plugin
now := time.Now().UnixMilli()
resDoc := &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResSubType: ptr.Of(int32(plugin.PluginType)),
ResID: plugin.ID,
Name: ptr.Of(plugin.GetName()),
SpaceID: &plugin.SpaceID,
APPID: plugin.APPID,
OwnerID: &req.UserID,
PublishStatus: ptr.Of(resCommon.PublishStatus_UnPublished),
CreateTimeMS: ptr.Of(now),
}
if plugin.Published() {
resDoc.PublishStatus = ptr.Of(resCommon.PublishStatus_Published)
resDoc.PublishTimeMS = ptr.Of(now)
}
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Created,
Resource: resDoc,
})
if err != nil {
return nil, errorx.Wrapf(err, "publish resource '%d' failed", plugin.ID)
}
resp = &dto.CopyPluginResponse{
Plugin: res.Plugin,
Tools: res.Tools,
}
return resp, nil
}
func (p *PluginApplicationService) MoveAPPPluginToLibrary(ctx context.Context, pluginID int64) (plugin *entity.PluginInfo, err error) {
plugin, err = p.DomainSVC.MoveAPPPluginToLibrary(ctx, pluginID)
if err != nil {
return nil, errorx.Wrapf(err, "MoveAPPPluginToLibrary failed, pluginID=%d", pluginID)
}
now := time.Now().UnixMilli()
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Updated,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResID: pluginID,
APPID: ptr.Of(int64(0)),
PublishStatus: ptr.Of(resCommon.PublishStatus_Published),
PublishTimeMS: ptr.Of(now),
UpdateTimeMS: ptr.Of(now),
},
})
if err != nil {
return nil, errorx.Wrapf(err, "publish resource '%d' failed", pluginID)
}
return plugin, nil
}
+246
View File
@@ -0,0 +1,246 @@
/*
* 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 plugin
import (
"context"
"fmt"
"slices"
"strconv"
"github.com/coze-dev/coze-studio/backend/api/model/app/bot_common"
"github.com/coze-dev/coze-studio/backend/api/model/marketplace/product_common"
pluginAPI "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop"
common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/convert"
"github.com/coze-dev/coze-studio/backend/domain/plugin/dto"
"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
)
func (p *PluginApplicationService) GetPlaygroundPluginList(ctx context.Context, req *pluginAPI.GetPlaygroundPluginListRequest) (resp *pluginAPI.GetPlaygroundPluginListResponse, err error) {
var (
plugins []*entity.PluginInfo
total int64
)
var isSaasPlugin bool
if slices.Contains(req.PluginTypes, int32(product_common.ProductEntityType_SaasPlugin)) {
isSaasPlugin = true
}
if len(req.PluginIds) > 0 {
if isSaasPlugin {
plugins, total, err = p.getSaasPluginListByIDs(ctx, req.PluginIds)
} else {
plugins, total, err = p.getPlaygroundPluginListByIDs(ctx, req.PluginIds)
}
} else {
plugins, total, err = p.getPlaygroundPluginList(ctx, req)
}
if err != nil {
return nil, errorx.Wrapf(err, "getPlaygroundPluginList failed, req=%v", req)
}
pluginList := make([]*common.PluginInfoForPlayground, 0, len(plugins))
if isSaasPlugin {
pluginIDs := make([]int64, 0, len(plugins))
for _, product := range 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
}
pluginList, err = p.convertSaasPluginListToPlayground(ctx, plugins, tools)
if err != nil {
return nil, err
}
} else {
for _, pl := range plugins {
tools, err := p.toolRepo.GetPluginAllOnlineTools(ctx, pl.ID)
if err != nil {
return nil, errorx.Wrapf(err, "GetPluginAllOnlineTools failed, pluginID=%d", pl.ID)
}
pluginInfo, err := p.toPluginInfoForPlayground(ctx, pl, tools)
if err != nil {
return nil, err
}
pluginList = append(pluginList, pluginInfo)
}
}
resp = &pluginAPI.GetPlaygroundPluginListResponse{
Data: &common.GetPlaygroundPluginListData{
Total: int32(total),
PluginList: pluginList,
},
}
return resp, nil
}
func (p *PluginApplicationService) convertSaasPluginListToPlayground(ctx context.Context, plugins []*entity.PluginInfo, tools map[int64][]*entity.ToolInfo) ([]*common.PluginInfoForPlayground, error) {
products := make([]*common.PluginInfoForPlayground, 0, len(plugins))
for _, pl := range plugins {
tools := tools[pl.ID]
pluginInfo, err := p.toPluginInfoForPlayground(ctx, pl, tools)
if err != nil {
return nil, err
}
products = append(products, pluginInfo)
}
return products, nil
}
func (p *PluginApplicationService) getSaasPluginListByIDs(ctx context.Context, pluginIDs []string) ([]*entity.PluginInfo, int64, error) {
ids := make([]int64, 0, len(pluginIDs))
for _, pluginIDStr := range pluginIDs {
pluginID, err := strconv.ParseInt(pluginIDStr, 10, 64)
if err != nil {
return nil, 0, fmt.Errorf("invalid pluginID '%s': %w", pluginIDStr, err)
}
ids = append(ids, pluginID)
}
plugins, err := p.DomainSVC.GetSaasPluginInfo(ctx, ids)
if err != nil {
return nil, 0, errorx.Wrapf(err, "GetSaasPluginInfo failed, pluginIDs=%v", pluginIDs)
}
return plugins, int64(len(plugins)), nil
}
func (p *PluginApplicationService) getPlaygroundPluginListByIDs(ctx context.Context, pluginIDs []string) (plugins []*entity.PluginInfo, total int64, err error) {
ids := make([]int64, 0, len(pluginIDs))
for _, id := range pluginIDs {
pluginID, pErr := strconv.ParseInt(id, 10, 64)
if pErr != nil {
return nil, 0, fmt.Errorf("invalid pluginID '%s'", id)
}
ids = append(ids, pluginID)
}
plugins, err = p.pluginRepo.MGetOnlinePlugins(ctx, ids)
if err != nil {
return nil, 0, errorx.Wrapf(err, "MGetOnlinePlugins failed, pluginIDs=%v", pluginIDs)
}
total = int64(len(plugins))
return plugins, total, nil
}
func (p *PluginApplicationService) getPlaygroundPluginList(ctx context.Context, req *pluginAPI.GetPlaygroundPluginListRequest) (plugins []*entity.PluginInfo, total int64, err error) {
pageInfo := dto.PageInfo{
Name: req.Name,
Page: int(req.GetPage()),
Size: int(req.GetSize()),
SortBy: func() *dto.SortField {
if req.GetOrderBy() == 0 {
return ptr.Of(dto.SortByUpdatedAt)
}
return ptr.Of(dto.SortByCreatedAt)
}(),
OrderByACS: ptr.Of(false),
}
plugins, total, err = p.DomainSVC.ListCustomOnlinePlugins(ctx, req.GetSpaceID(), pageInfo)
if err != nil {
return nil, 0, errorx.Wrapf(err, "ListCustomOnlinePlugins failed, spaceID=%d", req.GetSpaceID())
}
return plugins, total, nil
}
func (p *PluginApplicationService) toPluginInfoForPlayground(ctx context.Context, pl *entity.PluginInfo, tools []*entity.ToolInfo) (*common.PluginInfoForPlayground, error) {
pluginAPIs := make([]*common.PluginApi, 0, len(tools))
for _, tl := range tools {
params, err := tl.ToPluginParameters()
if err != nil {
return nil, err
}
pluginAPIs = append(pluginAPIs, &common.PluginApi{
APIID: strconv.FormatInt(tl.ID, 10),
Name: tl.GetName(),
Desc: tl.GetDesc(),
PluginID: strconv.FormatInt(pl.ID, 10),
PluginName: pl.GetName(),
RunMode: common.RunMode_Sync,
Parameters: params,
})
}
var creator *common.Creator
if pl.Source != ptr.Of(bot_common.PluginFrom_FromSaas) {
userInfo, err := p.userSVC.GetUserInfo(ctx, pl.DeveloperID)
if err != nil {
logs.CtxErrorf(ctx, "get user info failed, err=%v", err)
creator = common.NewCreator()
} else {
creator = &common.Creator{
ID: strconv.FormatInt(pl.DeveloperID, 10),
Name: userInfo.Name,
AvatarURL: userInfo.IconURL,
UserUniqueName: userInfo.UniqueName,
}
}
}
iconURL, err := p.oss.GetObjectUrl(ctx, pl.GetIconURI())
if err != nil {
logs.Errorf("get plugin icon url failed, err=%v", err)
}
authType, ok := convert.ToThriftAuthType(pl.GetAuthInfo().Type)
if !ok {
return nil, fmt.Errorf("invalid auth type '%s'", pl.GetAuthInfo().Type)
}
pluginInfo := &common.PluginInfoForPlayground{
Auth: int32(authType),
CreateTime: strconv.FormatInt(pl.CreatedAt/1000, 10),
CreationMethod: common.CreationMethod_COZE,
Creator: creator,
DescForHuman: pl.GetDesc(),
ID: strconv.FormatInt(pl.ID, 10),
IsOfficial: pl.IsOfficial(),
MaterialID: strconv.FormatInt(pl.ID, 10),
Name: pl.GetName(),
PluginIcon: iconURL,
PluginType: pl.PluginType,
SpaceID: strconv.FormatInt(pl.SpaceID, 10),
StatisticData: common.NewPluginStatisticData(),
Status: common.PluginStatus_SUBMITTED,
UpdateTime: strconv.FormatInt(pl.UpdatedAt/1000, 10),
ProjectID: strconv.FormatInt(pl.GetAPPID(), 10),
VersionName: pl.GetVersion(),
VersionTs: pl.GetVersion(), // Compatible with front-end logic, in theory VersionName should be used
PluginApis: pluginAPIs,
}
return pluginInfo, nil
}
+654
View File
@@ -0,0 +1,654 @@
/*
* 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 plugin
import (
"context"
"strconv"
"strings"
"time"
"github.com/coze-dev/coze-studio/backend/bizpkg/config"
"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"
common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/consts"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/convert/api"
"github.com/coze-dev/coze-studio/backend/domain/plugin/dto"
"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
"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"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
var PluginApplicationSVC = &PluginApplicationService{}
type PluginApplicationService struct {
DomainSVC service.PluginService
eventbus search.ResourceEventBus
oss storage.Storage
userSVC user.User
toolRepo repository.ToolRepository
pluginRepo repository.PluginRepository
}
func (p *PluginApplicationService) CheckAndLockPluginEdit(ctx context.Context, req *pluginAPI.CheckAndLockPluginEditRequest) (resp *pluginAPI.CheckAndLockPluginEditResponse, err error) {
resp = &pluginAPI.CheckAndLockPluginEditResponse{
Data: &common.CheckAndLockPluginEditData{
Seized: true,
},
}
return resp, nil
}
func (p *PluginApplicationService) GetBotDefaultParams(ctx context.Context, req *pluginAPI.GetBotDefaultParamsRequest) (resp *pluginAPI.GetBotDefaultParamsResponse, err error) {
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)
}
reqAPIParams, err := draftAgentTool.ToReqAPIParameter()
if err != nil {
return nil, err
}
respAPIParams, err := draftAgentTool.ToRespAPIParameter()
if err != nil {
return nil, err
}
resp = &pluginAPI.GetBotDefaultParamsResponse{
RequestParams: reqAPIParams,
ResponseParams: respAPIParams,
}
return resp, nil
}
func (p *PluginApplicationService) UpdateBotDefaultParams(ctx context.Context, req *pluginAPI.UpdateBotDefaultParamsRequest) (resp *pluginAPI.UpdateBotDefaultParamsResponse, err error) {
op, err := api.APIParamsToOpenapiOperation(req.RequestParams, req.ResponseParams)
if err != nil {
return nil, err
}
err = p.DomainSVC.UpdateBotDefaultParams(ctx, &dto.UpdateBotDefaultParamsRequest{
PluginID: req.PluginID,
ToolName: req.APIName,
AgentID: req.BotID,
Parameters: op.Parameters,
RequestBody: op.RequestBody,
Responses: op.Responses,
})
if err != nil {
return nil, errorx.Wrapf(err, "UpdateBotDefaultParams failed, agentID=%d, toolName=%s", req.BotID, req.APIName)
}
resp = &pluginAPI.UpdateBotDefaultParamsResponse{}
return resp, nil
}
func (p *PluginApplicationService) UnlockPluginEdit(ctx context.Context, req *pluginAPI.UnlockPluginEditRequest) (resp *pluginAPI.UnlockPluginEditResponse, err error) {
resp = &pluginAPI.UnlockPluginEditResponse{
Released: true,
}
return resp, nil
}
func (p *PluginApplicationService) PublicGetProductList(ctx context.Context, req *productAPI.GetProductListRequest) (resp *productAPI.GetProductListResponse, err error) {
res, err := p.DomainSVC.ListPluginProducts(ctx, &dto.ListPluginProductsRequest{})
if err != nil {
return nil, errorx.Wrapf(err, "ListPluginProducts failed")
}
products := make([]*productAPI.ProductInfo, 0, len(res.Plugins))
for _, pl := range res.Plugins {
tls, err := p.toolRepo.GetPluginAllOnlineTools(ctx, pl.ID)
if err != nil {
return nil, errorx.Wrapf(err, "GetPluginAllOnlineTools failed, pluginID=%d", pl.ID)
}
pi, err := p.buildProductInfo(ctx, pl, tls)
if err != nil {
return nil, err
}
products = append(products, pi)
}
if req.GetKeyword() != "" {
filterProducts := make([]*productAPI.ProductInfo, 0, len(products))
for _, _p := range products {
if strings.Contains(strings.ToLower(_p.MetaInfo.Name), strings.ToLower(req.GetKeyword())) {
filterProducts = append(filterProducts, _p)
}
}
products = filterProducts
}
resp = &productAPI.GetProductListResponse{
Data: &productAPI.GetProductListData{
Products: products,
HasMore: false, // Finish at one time
Total: int32(res.Total),
},
}
return resp, nil
}
func (p *PluginApplicationService) buildProductInfo(ctx context.Context, plugin *entity.PluginInfo, tools []*entity.ToolInfo) (*productAPI.ProductInfo, error) {
metaInfo, err := p.buildProductMetaInfo(ctx, plugin)
if err != nil {
return nil, err
}
extraInfo, err := p.buildPluginProductExtraInfo(ctx, plugin, tools)
if err != nil {
return nil, err
}
pi := &productAPI.ProductInfo{
CommercialSetting: &productCommon.CommercialSetting{
CommercialType: productCommon.ProductPaidType_Free,
},
MetaInfo: metaInfo,
PluginExtra: extraInfo,
}
return pi, nil
}
func (p *PluginApplicationService) buildProductMetaInfo(ctx context.Context, plugin *entity.PluginInfo) (*productAPI.ProductMetaInfo, error) {
iconURL, err := p.oss.GetObjectUrl(ctx, plugin.GetIconURI())
if err != nil {
logs.CtxWarnf(ctx, "get icon url failed with '%s', err=%v", plugin.GetIconURI(), err)
}
return &productAPI.ProductMetaInfo{
ID: plugin.GetRefProductID(),
EntityID: plugin.ID,
EntityType: productCommon.ProductEntityType_Plugin,
IconURL: iconURL,
Name: plugin.GetName(),
Description: plugin.GetDesc(),
IsFree: true,
IsOfficial: true,
Status: productCommon.ProductStatus_Listed,
ListedAt: time.Now().Unix(),
UserInfo: &productCommon.UserInfo{
Name: "Coze Official",
},
}, nil
}
func (p *PluginApplicationService) buildPluginProductExtraInfo(ctx context.Context, plugin *entity.PluginInfo, tools []*entity.ToolInfo) (*productAPI.PluginExtraInfo, error) {
ei := &productAPI.PluginExtraInfo{
IsOfficial: true,
PluginType: func() *productCommon.PluginType {
if plugin.PluginType == common.PluginType_LOCAL {
return ptr.Of(productCommon.PluginType_LocalPlugin)
}
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))
for _, tl := range tools {
params, err := tl.ToToolParameters()
if err != nil {
return nil, err
}
toolInfo := &productAPI.PluginToolInfo{
ID: tl.ID,
Name: tl.GetName(),
Description: tl.GetDesc(),
Parameters: params,
}
example := plugin.GetToolExample(ctx, tl.GetName())
if example != nil {
toolInfo.Example = &productAPI.PluginToolExample{
ReqExample: example.RequestExample,
RespExample: example.ResponseExample,
}
}
toolInfos = append(toolInfos, toolInfo)
}
ei.Tools = toolInfos
authInfo := plugin.GetAuthInfo()
authMode := ptr.Of(productAPI.PluginAuthMode_NoAuth)
if authInfo != nil {
if authInfo.Type == consts.AuthzTypeOfService || authInfo.Type == consts.AuthzTypeOfOAuth {
authMode = ptr.Of(productAPI.PluginAuthMode_Required)
err := plugin.Manifest.Validate(false)
if err != nil {
logs.CtxWarnf(ctx, "validate plugin manifest failed, err=%v", err)
} else {
authMode = ptr.Of(productAPI.PluginAuthMode_Configured)
}
} else if authInfo.Type == consts.AuthTypeOfSaasInstalled {
authMode = ptr.Of(productAPI.PluginAuthMode_NeedInstalled)
}
}
ei.AuthMode = authMode
return ei, nil
}
func (p *PluginApplicationService) PublicGetProductDetail(ctx context.Context, req *productAPI.GetProductDetailRequest) (resp *productAPI.GetProductDetailResponse, err error) {
plugin, exist, err := p.pluginRepo.GetOnlinePlugin(ctx, req.GetEntityID())
if err != nil {
return nil, errorx.Wrapf(err, "GetOnlinePlugin failed, pluginID=%d", req.GetEntityID())
}
if !exist {
return nil, errorx.New(errno.ErrPluginRecordNotFound)
}
tools, err := p.toolRepo.GetPluginAllOnlineTools(ctx, plugin.ID)
if err != nil {
return nil, errorx.Wrapf(err, "GetPluginAllOnlineTools failed, pluginID=%d", plugin.ID)
}
pi, err := p.buildProductInfo(ctx, plugin, tools)
if err != nil {
return nil, err
}
resp = &productAPI.GetProductDetailResponse{
Data: &productAPI.GetProductDetailData{
MetaInfo: pi.MetaInfo,
PluginExtra: pi.PluginExtra,
},
}
return resp, nil
}
func (p *PluginApplicationService) validateDraftPluginAccess(ctx context.Context, pluginID int64) (plugin *entity.PluginInfo, err error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))
}
plugin, err = p.DomainSVC.GetDraftPlugin(ctx, pluginID)
if err != nil {
return nil, errorx.Wrapf(err, "GetDraftPlugin failed, pluginID=%d", pluginID)
}
if plugin.DeveloperID != *uid {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "you are not the plugin owner"))
}
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) {
baseConfig, err := config.Base().GetBaseConfig(ctx)
if err != nil {
logs.CtxErrorf(ctx, "GetBaseConfig failed: %v", err)
return nil, err
}
cozeSaasPluginEnabled := baseConfig.PluginConfiguration.CozeSaasPluginEnabled
saasAPIKey := baseConfig.PluginConfiguration.CozeAPIToken
enableSaasPlugin := cozeSaasPluginEnabled && len(saasAPIKey) > 0
resp = &productAPI.GetMarketPluginConfigResponse{
Code: 0,
Message: "success",
Data: &productAPI.Configuration{
EnableSaasPlugin: &enableSaasPlugin,
},
}
return resp, nil
}
+291
View File
@@ -0,0 +1,291 @@
/*
* 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 plugin
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
productAPI "github.com/coze-dev/coze-studio/backend/api/model/marketplace/product_public_api"
pluginCommon "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/consts"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/model"
"github.com/coze-dev/coze-studio/backend/domain/plugin/dto"
"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
mockPlugin "github.com/coze-dev/coze-studio/backend/internal/mock/domain/plugin"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
)
func TestPluginApplicationService_GetCozeSaasPluginList(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDomainSVC := mockPlugin.NewMockPluginService(ctrl)
service := &PluginApplicationService{
DomainSVC: mockDomainSVC,
}
ctx := context.Background()
req := &productAPI.GetProductListRequest{}
t.Run("Success - Normal case with plugins", func(t *testing.T) {
// Prepare test data
testPlugins := []*entity.PluginInfo{
createTestPluginInfo(1, "Test Plugin 1", "Description 1"),
createTestPluginInfo(2, "Test Plugin 2", "Description 2"),
}
domainResp := &dto.ListPluginProductsResponse{
Plugins: testPlugins,
Total: 2,
}
// Setup mock expectations
mockDomainSVC.EXPECT().
ListSaasPluginProducts(ctx, gomock.Any()).
Return(domainResp, nil).
Times(1)
// Mock BatchGetSaasPluginToolsInfo
mockDomainSVC.EXPECT().
BatchGetSaasPluginToolsInfo(ctx, []int64{1, 2}).
Return(map[int64][]*entity.ToolInfo{
1: {},
2: {},
}, map[int64]*entity.PluginInfo{}, nil).
Times(1)
// Execute the method
resp, err := service.GetCozeSaasPluginList(ctx, req)
// Assertions
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, int32(0), resp.Code)
assert.Equal(t, "success", resp.Message)
assert.NotNil(t, resp.Data)
assert.Equal(t, int32(2), resp.Data.Total)
assert.False(t, resp.Data.HasMore)
assert.Len(t, resp.Data.Products, 2)
// Verify product info conversion
product1 := resp.Data.Products[0]
assert.Equal(t, int64(1), product1.MetaInfo.ID)
assert.Equal(t, int64(1), product1.MetaInfo.EntityID)
assert.Equal(t, "Test Plugin 1", product1.MetaInfo.Name)
assert.Equal(t, "Description 1", product1.MetaInfo.Description)
assert.Equal(t, "https://example.com/icon.png", product1.MetaInfo.IconURL)
assert.Equal(t, int64(1640995200), product1.MetaInfo.ListedAt)
assert.NotNil(t, product1.PluginExtra)
assert.True(t, product1.PluginExtra.IsOfficial) // buildPluginProductExtraInfo always sets IsOfficial to true
})
t.Run("Error - ListSaasPluginProducts returns error", func(t *testing.T) {
// Setup mock to return error
expectedError := errors.New("failed to fetch SaaS plugins")
mockDomainSVC.EXPECT().
ListSaasPluginProducts(ctx, gomock.Any()).
Return(nil, expectedError).
Times(1)
// Execute the method
resp, err := service.GetCozeSaasPluginList(ctx, req)
// Assertions
assert.Error(t, err) // The method returns error when domain service fails
assert.Nil(t, resp)
assert.Contains(t, err.Error(), "failed to fetch SaaS plugins")
})
t.Run("Success - Empty plugin list", func(t *testing.T) {
// Prepare empty response
domainResp := &dto.ListPluginProductsResponse{
Plugins: []*entity.PluginInfo{},
Total: 0,
}
// Setup mock expectations
mockDomainSVC.EXPECT().
ListSaasPluginProducts(ctx, gomock.Any()).
Return(domainResp, nil).
Times(1)
// Mock BatchGetSaasPluginToolsInfo for empty list
mockDomainSVC.EXPECT().
BatchGetSaasPluginToolsInfo(ctx, []int64{}).
Return(map[int64][]*entity.ToolInfo{}, map[int64]*entity.PluginInfo{}, nil).
Times(1)
// Execute the method
resp, err := service.GetCozeSaasPluginList(ctx, req)
// Assertions
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, int32(0), resp.Code)
assert.Equal(t, "success", resp.Message)
assert.NotNil(t, resp.Data)
assert.Equal(t, int32(0), resp.Data.Total)
assert.False(t, resp.Data.HasMore)
assert.Len(t, resp.Data.Products, 0)
assert.Empty(t, resp.Data.Products)
})
t.Run("Success - Multiple plugins", func(t *testing.T) {
// Prepare test data with multiple plugins
testPlugins := []*entity.PluginInfo{
createTestPluginInfo(100, "Weather Plugin", "Get weather information"),
createTestPluginInfo(200, "Translation Plugin", "Translate text between languages"),
createTestPluginInfo(300, "Calculator Plugin", "Perform mathematical calculations"),
createTestPluginInfo(400, "News Plugin", "Get latest news updates"),
createTestPluginInfo(500, "Email Plugin", "Send and manage emails"),
}
domainResp := &dto.ListPluginProductsResponse{
Plugins: testPlugins,
Total: 5,
}
// Setup mock expectations
mockDomainSVC.EXPECT().
ListSaasPluginProducts(ctx, gomock.Any()).
Return(domainResp, nil).
Times(1)
// Mock BatchGetSaasPluginToolsInfo for multiple plugins
mockDomainSVC.EXPECT().
BatchGetSaasPluginToolsInfo(ctx, []int64{100, 200, 300, 400, 500}).
Return(map[int64][]*entity.ToolInfo{
100: {},
200: {},
300: {},
400: {},
500: {},
}, map[int64]*entity.PluginInfo{}, nil).
Times(1)
// Execute the method
resp, err := service.GetCozeSaasPluginList(ctx, req)
// Assertions
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, int32(0), resp.Code)
assert.Equal(t, "success", resp.Message)
assert.NotNil(t, resp.Data)
assert.Equal(t, int32(5), resp.Data.Total)
assert.False(t, resp.Data.HasMore)
assert.Len(t, resp.Data.Products, 5)
// Verify all plugins are converted correctly
expectedNames := []string{"Weather Plugin", "Translation Plugin", "Calculator Plugin", "News Plugin", "Email Plugin"}
expectedIDs := []int64{100, 200, 300, 400, 500}
for i, product := range resp.Data.Products {
assert.Equal(t, expectedIDs[i], product.MetaInfo.ID)
assert.Equal(t, expectedIDs[i], product.MetaInfo.EntityID)
assert.Equal(t, expectedNames[i], product.MetaInfo.Name)
assert.Equal(t, "https://example.com/icon.png", product.MetaInfo.IconURL)
assert.Equal(t, int64(1640995200), product.MetaInfo.ListedAt)
assert.NotNil(t, product.PluginExtra)
assert.True(t, product.PluginExtra.IsOfficial) // buildPluginProductExtraInfo always sets IsOfficial to true
}
})
}
// createTestPluginInfo creates a test PluginInfo entity for testing
func createTestPluginInfo(id int64, name, desc string) *entity.PluginInfo {
manifest := &model.PluginManifest{
SchemaVersion: "v1",
NameForModel: name,
NameForHuman: name,
DescriptionForModel: desc,
DescriptionForHuman: desc,
LogoURL: "https://example.com/icon.png",
Auth: &model.AuthV2{
Type: consts.AuthzTypeOfNone,
},
API: model.APIDesc{
Type: "openapi",
},
}
pluginInfo := &model.PluginInfo{
ID: id,
PluginType: pluginCommon.PluginType_PLUGIN,
SpaceID: 0,
DeveloperID: 0,
APPID: nil,
RefProductID: ptr.Of(id), // Set RefProductID to the same as ID
IconURI: ptr.Of("https://example.com/icon.png"),
ServerURL: ptr.Of(""),
CreatedAt: 1640995200, // 2022-01-01 00:00:00
UpdatedAt: 1640995200,
SaasPluginExtra: &model.SaasPluginExtraInfo{
IsOfficial: false,
},
Manifest: manifest,
}
return entity.NewPluginInfo(pluginInfo)
}
// createTestPluginInfoWithCustomIcon creates a test PluginInfo entity with custom icon for testing
func createTestPluginInfoWithCustomIcon(id int64, name, desc, iconURL string) *entity.PluginInfo {
manifest := &model.PluginManifest{
SchemaVersion: "v1",
NameForModel: name,
NameForHuman: name,
DescriptionForModel: desc,
DescriptionForHuman: desc,
LogoURL: iconURL,
Auth: &model.AuthV2{
Type: consts.AuthzTypeOfNone,
},
API: model.APIDesc{
Type: "openapi",
},
}
pluginInfo := &model.PluginInfo{
ID: id,
PluginType: pluginCommon.PluginType_PLUGIN,
SpaceID: 0,
DeveloperID: 0,
APPID: nil,
IconURI: ptr.Of(iconURL),
ServerURL: ptr.Of(""),
CreatedAt: 1640995200,
UpdatedAt: 1640995200,
Manifest: manifest,
}
return entity.NewPluginInfo(pluginInfo)
}
// createTestDomainResponse creates a test domain response for testing
func createTestDomainResponse(plugins []*entity.PluginInfo) *dto.ListPluginProductsResponse {
return &dto.ListPluginProductsResponse{
Plugins: plugins,
Total: int64(len(plugins)),
}
}
+216
View File
@@ -0,0 +1,216 @@
/*
* 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 plugin
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/bytedance/sonic"
"github.com/getkin/kin-openapi/openapi3"
"gopkg.in/yaml.v3"
pluginAPI "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop"
common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common"
resCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/consts"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/convert"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/model"
"github.com/coze-dev/coze-studio/backend/domain/plugin/dto"
searchEntity "github.com/coze-dev/coze-studio/backend/domain/search/entity"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
commonConsts "github.com/coze-dev/coze-studio/backend/types/consts"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
func (p *PluginApplicationService) RegisterPluginMeta(ctx context.Context, req *pluginAPI.RegisterPluginMetaRequest) (resp *pluginAPI.RegisterPluginMetaResponse, err error) {
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))
}
_authType, ok := convert.ToAuthType(req.GetAuthType())
if !ok {
return nil, fmt.Errorf("invalid auth type '%d'", req.GetAuthType())
}
authType := ptr.Of(_authType)
var authSubType *consts.AuthzSubType
if req.SubAuthType != nil {
_authSubType, ok := convert.ToAuthSubType(req.GetSubAuthType())
if !ok {
return nil, fmt.Errorf("invalid sub authz type '%d'", req.GetSubAuthType())
}
authSubType = ptr.Of(_authSubType)
}
var loc consts.HTTPParamLocation
if *authType == consts.AuthzTypeOfService {
if req.GetLocation() == common.AuthorizationServiceLocation_Query {
loc = consts.ParamInQuery
} else if req.GetLocation() == common.AuthorizationServiceLocation_Header {
loc = consts.ParamInHeader
} else {
return nil, fmt.Errorf("invalid location '%s'", req.GetLocation())
}
}
r := &dto.CreateDraftPluginRequest{
PluginType: req.GetPluginType(),
SpaceID: req.GetSpaceID(),
DeveloperID: *userID,
IconURI: req.Icon.URI,
ProjectID: req.ProjectID,
Name: req.GetName(),
Desc: req.GetDesc(),
ServerURL: req.GetURL(),
CommonParams: req.CommonParams,
AuthInfo: &dto.PluginAuthInfo{
AuthzType: authType,
Location: ptr.Of(loc),
Key: req.Key,
ServiceToken: req.ServiceToken,
OAuthInfo: req.OauthInfo,
AuthzSubType: authSubType,
AuthzPayload: req.AuthPayload,
},
}
pluginID, err := p.DomainSVC.CreateDraftPlugin(ctx, r)
if err != nil {
return nil, errorx.Wrapf(err, "CreateDraftPlugin failed")
}
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Created,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResSubType: ptr.Of(int32(req.GetPluginType())),
ResID: pluginID,
Name: &req.Name,
SpaceID: &req.SpaceID,
APPID: req.ProjectID,
OwnerID: userID,
PublishStatus: ptr.Of(resCommon.PublishStatus_UnPublished),
CreateTimeMS: ptr.Of(time.Now().UnixMilli()),
},
})
if err != nil {
return nil, fmt.Errorf("publish resource '%d' failed, err=%v", pluginID, err)
}
resp = &pluginAPI.RegisterPluginMetaResponse{
PluginID: pluginID,
}
return resp, nil
}
func (p *PluginApplicationService) RegisterPlugin(ctx context.Context, req *pluginAPI.RegisterPluginRequest) (resp *pluginAPI.RegisterPluginResponse, err error) {
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))
}
mf := &model.PluginManifest{}
err = sonic.UnmarshalString(req.AiPlugin, &mf)
if err != nil {
return nil, errorx.New(errno.ErrPluginInvalidManifest, errorx.KV(errno.PluginMsgKey, err.Error()))
}
mf.LogoURL = commonConsts.DefaultPluginIcon
doc, err := openapi3.NewLoader().LoadFromData([]byte(req.Openapi))
if err != nil {
return nil, errorx.New(errno.ErrPluginInvalidOpenapi3Doc, errorx.KV(errno.PluginMsgKey, err.Error()))
}
res, err := p.DomainSVC.CreateDraftPluginWithCode(ctx, &dto.CreateDraftPluginWithCodeRequest{
SpaceID: req.GetSpaceID(),
DeveloperID: *userID,
ProjectID: req.ProjectID,
Manifest: mf,
OpenapiDoc: ptr.Of(model.Openapi3T(*doc)),
})
if err != nil {
return nil, errorx.Wrapf(err, "CreateDraftPluginWithCode failed")
}
err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Created,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Plugin,
ResSubType: ptr.Of(int32(res.Plugin.PluginType)),
ResID: res.Plugin.ID,
Name: ptr.Of(res.Plugin.GetName()),
APPID: req.ProjectID,
SpaceID: &req.SpaceID,
OwnerID: userID,
PublishStatus: ptr.Of(resCommon.PublishStatus_UnPublished),
CreateTimeMS: ptr.Of(time.Now().UnixMilli()),
},
})
if err != nil {
return nil, fmt.Errorf("publish resource '%d' failed, err=%v", res.Plugin.ID, err)
}
resp = &pluginAPI.RegisterPluginResponse{
Data: &common.RegisterPluginData{
PluginID: res.Plugin.ID,
Openapi: req.Openapi,
},
}
return resp, nil
}
func (p *PluginApplicationService) Convert2OpenAPI(ctx context.Context, req *pluginAPI.Convert2OpenAPIRequest) (resp *pluginAPI.Convert2OpenAPIResponse, err error) {
res := p.DomainSVC.ConvertToOpenapi3Doc(ctx, &dto.ConvertToOpenapi3DocRequest{
RawInput: req.Data,
PluginServerURL: req.PluginURL,
})
if res.ErrMsg != "" {
return &pluginAPI.Convert2OpenAPIResponse{
Code: errno.ErrPluginInvalidThirdPartyCode,
Msg: res.ErrMsg,
DuplicateAPIInfos: []*common.DuplicateAPIInfo{},
PluginDataFormat: ptr.Of(res.Format),
}, nil
}
doc, err := yaml.Marshal(res.OpenapiDoc)
if err != nil {
return nil, fmt.Errorf("marshal openapi doc failed, err=%v", err)
}
mf, err := json.Marshal(res.Manifest)
if err != nil {
return nil, fmt.Errorf("marshal manifest failed, err=%v", err)
}
resp = &pluginAPI.Convert2OpenAPIResponse{
PluginDataFormat: ptr.Of(res.Format),
Openapi: ptr.Of(string(doc)),
AiPlugin: ptr.Of(string(mf)),
DuplicateAPIInfos: []*common.DuplicateAPIInfo{},
}
return resp, nil
}