feat(plugin): exec coze saas tool

This commit is contained in:
lijunwen.gigoo
2025-10-13 21:27:48 +08:00
parent 8011bd39f4
commit 58aa07ab7b
6 changed files with 239 additions and 32 deletions

View File

@ -51,6 +51,8 @@ func newPluginTools(ctx context.Context, conf *toolConfig) ([]tool.InvokableTool
return model.VersionAgentTool{
ToolID: a.GetApiId(),
AgentVersion: ptr.Of(conf.agentIdentity.Version),
PluginSource: a.PluginSource,
PluginID: a.GetPluginId(),
}
}),
}
@ -69,10 +71,11 @@ func newPluginTools(ctx context.Context, conf *toolConfig) ([]tool.InvokableTool
tools := make([]tool.InvokableTool, 0, len(agentTools))
for _, ti := range agentTools {
tools = append(tools, &pluginInvokableTool{
userID: conf.userID,
isDraft: conf.agentIdentity.IsDraft,
projectInfo: projectInfo,
toolInfo: ti,
userID: conf.userID,
isDraft: conf.agentIdentity.IsDraft,
projectInfo: projectInfo,
toolInfo: ti,
pluginSource: ti.Source,
conversationID: conf.conversationID,
})
@ -87,6 +90,8 @@ type pluginInvokableTool struct {
toolInfo *pluginEntity.ToolInfo
projectInfo *model.ProjectInfo
pluginSource *bot_common.PluginSource
conversationID int64
}
@ -117,6 +122,7 @@ func (p *pluginInvokableTool) InvokableRun(ctx context.Context, argumentsInJSON
PluginID: p.toolInfo.PluginID,
ToolID: p.toolInfo.ID,
ExecDraftTool: false,
PluginSource: p.pluginSource,
ArgumentsInJson: argumentsInJSON,
ExecScene: func() consts.ExecuteScene {
if p.isDraft {

View File

@ -81,7 +81,7 @@ func (p *pluginServiceImpl) MGetAgentTools(ctx context.Context, req *model.MGetA
localToolIDs = append(localToolIDs, v.ToolID)
}
}
// todo :: saas plugin or local plugin
existTools, err := p.toolRepo.MGetOnlineTools(ctx, localToolIDs, repository.WithToolID())
if err != nil {
return nil, errorx.Wrapf(err, "MGetOnlineTools failed, toolIDs=%v", localToolIDs)

View File

@ -189,8 +189,20 @@ func (p *pluginServiceImpl) getDraftAgentPluginInfo(ctx context.Context, req *mo
exist bool
)
if req.PluginSource != nil && *req.PluginSource == bot_common.PluginSource_FromSaas {
//TODOget plugin info from saas
tools, err := p.toolRepo.BatchGetSaasPluginToolsInfo(ctx, []int64{req.PluginID})
if err != nil {
return nil, nil, errorx.Wrapf(err, "BatchGetSaasPluginToolsInfo failed, pluginID=%d", req.PluginID)
}
if len(tools) == 0 {
return nil, nil, errorx.New(errno.ErrPluginRecordNotFound)
}
for _, tool := range tools[req.PluginID] {
if tool.ID == req.ToolID {
onlineTool = tool
break
}
}
} else {
onlineTool, exist, err = p.toolRepo.GetOnlineTool(ctx, req.ToolID)
if err != nil {
@ -209,24 +221,35 @@ func (p *pluginServiceImpl) getDraftAgentPluginInfo(ctx context.Context, req *mo
return nil, nil, errorx.New(errno.ErrPluginRecordNotFound)
}
if execOpt.ToolVersion == "" {
onlinePlugin, exist, err = p.pluginRepo.GetOnlinePlugin(ctx, req.PluginID)
if req.PluginSource != nil && *req.PluginSource == bot_common.PluginSource_FromSaas {
saasPlugins, err := p.GetSaasPluginInfo(ctx, []int64{req.PluginID})
if err != nil {
return nil, nil, errorx.Wrapf(err, "GetOnlinePlugin failed, pluginID=%d", req.PluginID)
return nil, nil, errorx.Wrapf(err, "GetSaasPluginInfo failed, pluginID=%d", req.PluginID)
}
if !exist {
if len(saasPlugins) == 0 {
return nil, nil, errorx.New(errno.ErrPluginRecordNotFound)
}
onlinePlugin = saasPlugins[0]
} else {
onlinePlugin, exist, err = p.pluginRepo.GetVersionPlugin(ctx, model.VersionPlugin{
PluginID: req.PluginID,
Version: execOpt.ToolVersion,
})
if err != nil {
return nil, nil, errorx.Wrapf(err, "GetVersionPlugin failed, pluginID=%d, version=%s", req.PluginID, execOpt.ToolVersion)
}
if !exist {
return nil, nil, errorx.New(errno.ErrPluginRecordNotFound)
if execOpt.ToolVersion == "" {
onlinePlugin, exist, err = p.pluginRepo.GetOnlinePlugin(ctx, req.PluginID)
if err != nil {
return nil, nil, errorx.Wrapf(err, "GetOnlinePlugin failed, pluginID=%d", req.PluginID)
}
if !exist {
return nil, nil, errorx.New(errno.ErrPluginRecordNotFound)
}
} else {
onlinePlugin, exist, err = p.pluginRepo.GetVersionPlugin(ctx, model.VersionPlugin{
PluginID: req.PluginID,
Version: execOpt.ToolVersion,
})
if err != nil {
return nil, nil, errorx.Wrapf(err, "GetVersionPlugin failed, pluginID=%d, version=%s", req.PluginID, execOpt.ToolVersion)
}
if !exist {
return nil, nil, errorx.New(errno.ErrPluginRecordNotFound)
}
}
}
@ -566,8 +589,13 @@ func (t *toolExecutor) execute(ctx context.Context, argumentsInJson, accessToken
}
}
toolInvocation := newToolInvocation(t)
requestStr, rawResp, err := toolInvocation.Do(ctx, invocation)
var requestStr, rawResp string
if t.plugin.Source != nil && *t.plugin.Source == bot_common.PluginSource_FromSaas {
requestStr, rawResp, err = tool.NewSaasCallImpl().Do(ctx, invocation)
} else {
requestStr, rawResp, err = newToolInvocation(t).Do(ctx, invocation)
}
if err != nil {
return nil, err
}

View File

@ -181,7 +181,7 @@ func convertCozePluginToEntity(cozePlugin *dto.SaasPluginToolsList) *entity.Plug
DeveloperID: 0,
APPID: nil,
IconURL: &cozePlugin.IconURL,
ServerURL: ptr.Of(""),
ServerURL: ptr.Of("https://api.coze.cn"),
CreatedAt: cozePlugin.CreatedAt,
UpdatedAt: cozePlugin.UpdatedAt,
Manifest: manifest,

View File

@ -31,15 +31,14 @@ import (
"github.com/go-resty/resty/v2"
"github.com/tidwall/sjson"
"github.com/coze-dev/coze-studio/backend/api/model/app/bot_common"
pluginConsts "github.com/coze-dev/coze-studio/backend/crossdomain/contract/plugin/consts"
"github.com/coze-dev/coze-studio/backend/crossdomain/contract/plugin/model"
"github.com/coze-dev/coze-studio/backend/domain/plugin/internal/encoder"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/i18n"
"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/logs"
"github.com/coze-dev/coze-studio/backend/pkg/saasapi"
"github.com/coze-dev/coze-studio/backend/types/consts"
"github.com/coze-dev/coze-studio/backend/types/errno"
@ -158,10 +157,6 @@ func (h *httpCallImpl) buildHTTPRequest(ctx context.Context, args *InvocationArg
func (h *httpCallImpl) injectAuthInfo(ctx context.Context, httpReq *http.Request, args *InvocationArgs) (errMsg string, err error) {
if ptr.From(args.Tool.Source) == bot_common.PluginSource_FromSaas {
return h.injectCozeSaasAPIToken(ctx, httpReq)
}
if args.AuthInfo.MetaInfo.Type == pluginConsts.AuthzTypeOfNone {
return "", nil
}
@ -287,12 +282,11 @@ func (h *httpCallImpl) buildRequestBody(ctx context.Context, op *model.Openapi3O
func (h *httpCallImpl) injectCozeSaasAPIToken(ctx context.Context, httpReq *http.Request) (errMsg string, err error) {
// apiToken := os.Getenv(consts.CozeSaasAPIKey)
apiToken := "pat_OjlRXGYdXDLHDv10dZuav02A7SomHZkXTjx0fbZ9xUDIrssE7tZ07gI2TzABBQ7M"
if apiToken == "" {
saasapiClient := saasapi.NewCozeAPIClient()
if saasapiClient.APIKey == "" {
return "", fmt.Errorf("coze saas api token is empty")
}
httpReq.Header.Set("Authorization", "Bearer "+apiToken)
httpReq.Header.Set("Authorization", "Bearer "+saasapiClient.APIKey)
return "", nil
}

View File

@ -0,0 +1,179 @@
package tool
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/bytedance/sonic"
"github.com/coze-dev/coze-studio/backend/domain/plugin/internal/encoder"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/pkg/saasapi"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
type saasCallImpl struct {
}
func NewSaasCallImpl() Invocation {
return &saasCallImpl{}
}
func (s *saasCallImpl) Do(ctx context.Context, args *InvocationArgs) (request string, resp string, err error) {
httpReq, err := s.buildHTTPRequest(ctx, args)
if err != nil {
return "", "", err
}
err = s.injectAuthInfo(ctx, httpReq, args)
if err != nil {
return "", "", err
}
var reqBodyBytes []byte
if httpReq.GetBody != nil {
reqBody, err := httpReq.GetBody()
if err != nil {
return "", "", err
}
defer reqBody.Close()
reqBodyBytes, err = io.ReadAll(reqBody)
if err != nil {
return "", "", err
}
}
requestStr, err := genRequestString(httpReq, reqBodyBytes)
if err != nil {
return "", "", err
}
restyReq := defaultHttpCli.NewRequest()
restyReq.Header = httpReq.Header
restyReq.Method = httpReq.Method
restyReq.URL = httpReq.URL.String()
if reqBodyBytes != nil {
restyReq.SetBody(reqBodyBytes)
}
restyReq.SetContext(ctx)
logs.CtxDebugf(ctx, "[execute] url=%s, header=%s, method=%s, body=%s",
restyReq.URL, restyReq.Header, restyReq.Method, restyReq.Body)
httpResp, err := restyReq.Send()
if err != nil {
return "", "", errorx.New(errno.ErrPluginExecuteToolFailed, errorx.KVf(errno.PluginMsgKey, "http request failed, err=%s", err))
}
logs.CtxDebugf(ctx, "[execute] status=%s, response=%s", httpResp.Status(), httpResp.String())
if httpResp.StatusCode() != http.StatusOK {
return "", "", errorx.New(errno.ErrPluginExecuteToolFailed,
errorx.KVf(errno.PluginMsgKey, "http request failed, status=%s\nresp=%s", httpResp.Status(), httpResp.String()))
}
httpRespBody := httpResp.String()
type CozeAPIResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data map[string]any `json:"data"`
}
var apiResp CozeAPIResponse
if err := sonic.UnmarshalString(httpRespBody, &apiResp); err != nil {
return "", "", fmt.Errorf("failed to parse API response: %w", err)
}
rawResp := apiResp.Data["result"]
return requestStr, encoder.MustString(rawResp), nil
}
func (s *saasCallImpl) injectAuthInfo(ctx context.Context, httpReq *http.Request, args *InvocationArgs) (err error) {
saasapiClient := saasapi.NewCozeAPIClient()
if saasapiClient.APIKey == "" {
return fmt.Errorf("coze saas api token is empty")
}
httpReq.Header.Set("Authorization", "Bearer "+saasapiClient.APIKey)
return nil
}
func (s *saasCallImpl) buildHTTPRequest(ctx context.Context, args *InvocationArgs) (httpReq *http.Request, err error) {
tool := args.Tool
rawURL := args.ServerURL + tool.GetSubURL()
reqURL, err := s.buildHTTPRequestURL(ctx, rawURL, args)
if err != nil {
return nil, err
}
type callSaasTool struct {
Arguments map[string]any `json:"arguments"`
ToolName string `json:"tool_name"`
}
callSaasToolData := &callSaasTool{
ToolName: tool.GetName(),
Arguments: args.Body,
}
bodyBytes, err := sonic.MarshalString(callSaasToolData)
if err != nil {
return nil, err
}
httpReq, err = http.NewRequestWithContext(ctx, tool.GetMethod(), reqURL.String(), bytes.NewBufferString(bodyBytes))
if err != nil {
return nil, err
}
return httpReq, nil
}
func (s *saasCallImpl) buildHTTPRequestURL(ctx context.Context, rawURL string, args *InvocationArgs) (reqURL *url.URL, err error) {
if len(args.Path) > 0 {
for k, v := range args.Path {
p := args.groupedKeySchema.PathKeys[k]
vStr, eErr := encoder.EncodeParameter(p, v)
if eErr != nil {
return nil, eErr
}
rawURL = strings.ReplaceAll(rawURL, "{"+k+"}", vStr)
}
}
query := url.Values{}
if len(args.Query) > 0 {
for k, val := range args.Query {
switch v := val.(type) {
case []any:
for _, _v := range v {
query.Add(k, encoder.MustString(_v))
}
default:
query.Add(k, encoder.MustString(v))
}
}
}
encodeQuery := query.Encode()
reqURL, err = url.Parse(rawURL)
if err != nil {
return nil, err
}
if len(reqURL.RawQuery) > 0 && len(encodeQuery) > 0 {
reqURL.RawQuery += "&" + encodeQuery
} else if len(encodeQuery) > 0 {
reqURL.RawQuery = encodeQuery
}
return reqURL, nil
}