feat: Support for Chat Flow & Agent Support for binding a single chat flow (#765)

Co-authored-by: Yu Yang <72337138+tomasyu985@users.noreply.github.com>
Co-authored-by: zengxiaohui <csu.zengxiaohui@gmail.com>
Co-authored-by: lijunwen.gigoo <lijunwen.gigoo@bytedance.com>
Co-authored-by: lvxinyu.1117 <lvxinyu.1117@bytedance.com>
Co-authored-by: liuyunchao.0510 <liuyunchao.0510@bytedance.com>
Co-authored-by: haozhenfei <37089575+haozhenfei@users.noreply.github.com>
Co-authored-by: July <jiangxujin@bytedance.com>
Co-authored-by: tecvan-fe <fanwenjie.fe@bytedance.com>
This commit is contained in:
Zhj
2025-08-28 21:53:32 +08:00
committed by GitHub
parent bbc615a18e
commit d70101c979
503 changed files with 48036 additions and 3427 deletions

View File

@@ -59,6 +59,7 @@ import (
"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/ternary"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/pkg/safego"
"github.com/coze-dev/coze-studio/backend/pkg/taskgroup"
@@ -303,6 +304,11 @@ func (a *APPApplicationService) getAPPPublishConnectorList(ctx context.Context,
if err != nil {
return nil, err
}
case consts.WebSDKConnectorID:
info, err = a.packChatSDKConnectorInfo(ctx, c)
if err != nil {
return nil, err
}
default:
logs.CtxWarnf(ctx, "unsupported connector id '%v'", c.ID)
continue
@@ -338,6 +344,22 @@ func (a *APPApplicationService) packAPIConnectorInfo(ctx context.Context, c *con
return info, nil
}
func (a *APPApplicationService) packChatSDKConnectorInfo(ctx context.Context, c *connectorModel.Connector) (*publishAPI.PublishConnectorInfo, error) {
info := &publishAPI.PublishConnectorInfo{
ID: c.ID,
BindType: publishAPI.ConnectorBindType_WebSDKBind,
ConnectorClassification: publishAPI.ConnectorClassification_APIOrSDK,
BindInfo: map[string]string{},
Name: c.Name,
IconURL: c.URL,
Description: c.Desc,
AllowPublish: true,
}
return info, nil
}
func (a *APPApplicationService) getLatestPublishRecord(ctx context.Context, appID int64) (info *publishAPI.LastPublishInfo, err error) {
record, exist, err := a.DomainSVC.GetAPPPublishRecord(ctx, &service.GetAPPPublishRecordRequest{
APPID: appID,
@@ -1089,6 +1111,91 @@ func (a *APPApplicationService) DraftProjectCopy(ctx context.Context, req *proje
return resp, nil
}
func (a *APPApplicationService) GetOnlineAppData(ctx context.Context, req *projectAPI.GetOnlineAppDataRequest) (resp *projectAPI.GetOnlineAppDataResponse, err error) {
uid := ctxutil.GetApiAuthFromCtx(ctx).UserID
record, exist, err := a.DomainSVC.GetAPPPublishRecord(ctx, &service.GetAPPPublishRecordRequest{
APPID: req.GetAppID(),
Oldest: false,
})
if err != nil {
return nil, err
}
if !exist {
return nil, errorx.Wrapf(err, "GetOnlineAppDataRequest failed, app id=%d, connector id=%v", req.GetAppID(), req.GetConnectorID())
}
if record.APP.OwnerID != uid {
return nil, errorx.New(errno.ErrAppPermissionCode, errorx.KV(errno.APPMsgKey, fmt.Sprintf("user %d does not have access to app %d", uid, req.GetAppID())))
}
valid := false
for _, v := range record.ConnectorPublishRecords {
if v.ConnectorID == req.GetConnectorID() {
valid = true
break
}
}
if !valid {
return nil, errorx.Wrapf(err, "GetOnlineAppDataRequest failed, invalid connector id, app id=%d, connector id=%v", req.GetAppID(), req.GetConnectorID())
}
app := record.APP
iconURL, err := a.oss.GetObjectUrl(ctx, app.GetIconURI())
if err != nil {
logs.CtxWarnf(ctx, "get icon url failed with '%s', err=%v", app.GetIconURI(), err)
}
varMeta, err := a.variablesSVC.GetProjectVariablesMeta(ctx, strconv.FormatInt(app.ID, 10), "")
if err != nil {
return nil, err
}
vars := make([]*common.Variable, 0, len(varMeta.Variables))
for _, v := range varMeta.Variables {
vars = append(vars, &common.Variable{
Keyword: v.Keyword,
DefaultValue: v.DefaultValue,
Description: v.Description,
Enable: v.Enable,
VariableType: ternary.IFElse(v.VariableType == project_memory.VariableType_KVVariable, common.VariableTypeKVVariable, common.VariableTypeListVariable),
Channel: func() common.VariableChannel {
switch v.Channel {
case project_memory.VariableChannel_APP:
return common.VariableChannelAPP
case project_memory.VariableChannel_System:
return common.VariableChannelSystem
case project_memory.VariableChannel_Custom:
return common.VariableChannelCustom
case project_memory.VariableChannel_Feishu:
return common.VariableChannelFeishu
case project_memory.VariableChannel_Location:
return common.VariableChannelLocation
default:
return ""
}
}(),
})
}
response := &projectAPI.GetOnlineAppDataResponse{
Data: &projectAPI.AppData{
AppID: strconv.FormatInt(record.APP.ID, 10),
Name: *app.Name,
Description: *app.Desc,
Version: *app.Version,
IconURL: iconURL,
Variables: vars,
},
}
return response, nil
}
func (a *APPApplicationService) duplicateDraftAPP(ctx context.Context, userID int64, req *projectAPI.DraftProjectCopyRequest) (newAppID int64, err error) {
newAppID, err = a.DomainSVC.CreateDraftAPP(ctx, &service.CreateDraftAPPRequest{
SpaceID: req.ToSpaceID,

View File

@@ -49,6 +49,7 @@ import (
crossmessage "github.com/coze-dev/coze-studio/backend/crossdomain/contract/message"
crossmodelmgr "github.com/coze-dev/coze-studio/backend/crossdomain/contract/modelmgr"
crossplugin "github.com/coze-dev/coze-studio/backend/crossdomain/contract/plugin"
crossupload "github.com/coze-dev/coze-studio/backend/crossdomain/contract/upload"
crossuser "github.com/coze-dev/coze-studio/backend/crossdomain/contract/user"
crossvariables "github.com/coze-dev/coze-studio/backend/crossdomain/contract/variables"
crossworkflow "github.com/coze-dev/coze-studio/backend/crossdomain/contract/workflow"
@@ -64,6 +65,7 @@ import (
pluginImpl "github.com/coze-dev/coze-studio/backend/crossdomain/impl/plugin"
searchImpl "github.com/coze-dev/coze-studio/backend/crossdomain/impl/search"
singleagentImpl "github.com/coze-dev/coze-studio/backend/crossdomain/impl/singleagent"
uploadImpl "github.com/coze-dev/coze-studio/backend/crossdomain/impl/upload"
variablesImpl "github.com/coze-dev/coze-studio/backend/crossdomain/impl/variables"
workflowImpl "github.com/coze-dev/coze-studio/backend/crossdomain/impl/workflow"
"github.com/coze-dev/coze-studio/backend/infra/contract/eventbus"
@@ -86,6 +88,7 @@ type basicServices struct {
promptSVC *prompt.PromptApplicationService
templateSVC *template.ApplicationService
openAuthSVC *openauth.OpenAuthApplicationService
uploadSVC *upload.UploadService
}
type primaryServices struct {
@@ -139,11 +142,12 @@ func Init(ctx context.Context) (err error) {
crossconversation.SetDefaultSVC(conversationImpl.InitDomainService(complexServices.conversationSVC.ConversationDomainSVC))
crossmessage.SetDefaultSVC(messageImpl.InitDomainService(complexServices.conversationSVC.MessageDomainSVC))
crossagentrun.SetDefaultSVC(agentrunImpl.InitDomainService(complexServices.conversationSVC.AgentRunDomainSVC))
crossagent.SetDefaultSVC(singleagentImpl.InitDomainService(complexServices.singleAgentSVC.DomainSVC, infra.ImageXClient))
crossagent.SetDefaultSVC(singleagentImpl.InitDomainService(complexServices.singleAgentSVC.DomainSVC))
crossuser.SetDefaultSVC(crossuserImpl.InitDomainService(basicServices.userSVC.DomainSVC))
crossdatacopy.SetDefaultSVC(dataCopyImpl.InitDomainService(basicServices.infra))
crosssearch.SetDefaultSVC(searchImpl.InitDomainService(complexServices.searchSVC.DomainSVC))
crossmodelmgr.SetDefaultSVC(modelmgrImpl.InitDomainService(infra.ModelMgr, nil))
crossupload.SetDefaultSVC(uploadImpl.InitDomainService(basicServices.uploadSVC.UploadSVC))
return nil
}
@@ -159,7 +163,7 @@ func initEventBus(infra *appinfra.AppDependencies) *eventbusImpl {
// initBasicServices init basic services that only depends on infra.
func initBasicServices(ctx context.Context, infra *appinfra.AppDependencies, e *eventbusImpl) (*basicServices, error) {
upload.InitService(infra.TOSClient, infra.CacheCli)
uploadSVC := upload.InitService(&upload.UploadComponents{Cache: infra.CacheCli, Oss: infra.TOSClient, DB: infra.DB, Idgen: infra.IDGenSVC})
openAuthSVC := openauth.InitService(infra.DB, infra.IDGenSVC)
promptSVC := prompt.InitService(infra.DB, infra.IDGenSVC, e.resourceEventBus)
modelMgrSVC := modelmgr.InitService(infra.ModelMgr, infra.TOSClient)
@@ -180,6 +184,7 @@ func initBasicServices(ctx context.Context, infra *appinfra.AppDependencies, e *
promptSVC: promptSVC,
templateSVC: templateSVC,
openAuthSVC: openAuthSVC,
uploadSVC: uploadSVC,
}, nil
}

View File

@@ -27,6 +27,7 @@ import (
conversationService "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/service"
message "github.com/coze-dev/coze-studio/backend/domain/conversation/message/service"
"github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/service"
uploadService "github.com/coze-dev/coze-studio/backend/domain/upload/service"
"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/lang/slices"
@@ -48,6 +49,7 @@ var ConversationSVC = new(ConversationApplicationService)
type OpenapiAgentRunApplication struct {
ShortcutDomainSVC service.ShortcutCmd
UploaodDomainSVC uploadService.UploadService
}
var ConversationOpenAPISVC = new(OpenapiAgentRunApplication)
@@ -177,6 +179,7 @@ func (c *ConversationApplicationService) ListConversation(ctx context.Context, r
LastSectionID: &conv.SectionID,
ConnectorID: &conv.ConnectorID,
CreatedAt: conv.CreatedAt / 1000,
Name: ptr.Of(conv.Name),
}
})
@@ -186,3 +189,70 @@ func (c *ConversationApplicationService) ListConversation(ctx context.Context, r
}
return resp, nil
}
func (c *ConversationApplicationService) DeleteConversation(ctx context.Context, req *conversation.DeleteConversationApiRequest) (*conversation.DeleteConversationApiResponse, error) {
resp := new(conversation.DeleteConversationApiResponse)
convID := req.GetConversationID()
apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx)
userID := apiKeyInfo.UserID
if userID == 0 {
return resp, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "permission check failed"))
}
conversationDO, err := c.ConversationDomainSVC.GetByID(ctx, convID)
if err != nil {
return resp, err
}
if conversationDO == nil {
return resp, errorx.New(errno.ErrConversationNotFound)
}
if conversationDO.CreatorID != userID {
return resp, errorx.New(errno.ErrConversationNotFound, errorx.KV("msg", "user not match"))
}
err = c.ConversationDomainSVC.Delete(ctx, convID)
if err != nil {
return resp, err
}
return resp, nil
}
func (c *ConversationApplicationService) UpdateConversation(ctx context.Context, req *conversation.UpdateConversationApiRequest) (*conversation.UpdateConversationApiResponse, error) {
resp := new(conversation.UpdateConversationApiResponse)
convID := req.GetConversationID()
apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx)
userID := apiKeyInfo.UserID
if userID == 0 {
return resp, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "permission check failed"))
}
conversationDO, err := c.ConversationDomainSVC.GetByID(ctx, convID)
if err != nil {
return resp, err
}
if conversationDO == nil {
return resp, errorx.New(errno.ErrConversationNotFound)
}
if conversationDO.CreatorID != userID {
return resp, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "user not match"))
}
updateResult, err := c.ConversationDomainSVC.Update(ctx, &entity.UpdateMeta{
ID: convID,
Name: req.GetName(),
})
if err != nil {
return resp, err
}
resp.ConversationData = &conversation.ConversationData{
Id: updateResult.ID,
LastSectionID: &updateResult.SectionID,
ConnectorID: &updateResult.ConnectorID,
CreatedAt: updateResult.CreatedAt / 1000,
Name: ptr.Of(updateResult.Name),
}
return resp, nil
}

View File

@@ -28,6 +28,7 @@ import (
message "github.com/coze-dev/coze-studio/backend/domain/conversation/message/service"
shortcutRepo "github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/repository"
"github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/service"
uploadService "github.com/coze-dev/coze-studio/backend/domain/upload/service"
"github.com/coze-dev/coze-studio/backend/infra/contract/idgen"
"github.com/coze-dev/coze-studio/backend/infra/contract/imagex"
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
@@ -56,6 +57,7 @@ func InitService(s *ServiceComponents) *ConversationApplicationService {
arDomainComponents := &agentrun.Components{
RunRecordRepo: repository.NewRunRecordRepo(s.DB, s.IDGen),
ImagexSVC: s.ImageX,
}
agentRunDomainSVC := agentrun.NewService(arDomainComponents)
@@ -71,6 +73,9 @@ func InitService(s *ServiceComponents) *ConversationApplicationService {
ConversationSVC.ShortcutDomainSVC = shortcutCmdDomainSVC
ConversationOpenAPISVC.ShortcutDomainSVC = shortcutCmdDomainSVC
uploadSVC := uploadService.NewUploadSVC(s.DB, s.IDGen, s.TosClient)
ConversationOpenAPISVC.UploaodDomainSVC = uploadSVC
OpenapiMessageSVC.UploaodDomainSVC = uploadSVC
return ConversationSVC
}

View File

@@ -35,7 +35,9 @@ import (
"github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/entity"
convEntity "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/entity"
cmdEntity "github.com/coze-dev/coze-studio/backend/domain/shortcutcmd/entity"
uploadService "github.com/coze-dev/coze-studio/backend/domain/upload/service"
sseImpl "github.com/coze-dev/coze-studio/backend/infra/impl/sse"
"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/logs"
@@ -237,11 +239,26 @@ func (a *OpenapiAgentRunApplication) buildMultiContent(ctx context.Context, ar *
Text: ptr.From(one.Text),
})
case message.InputTypeImage, message.InputTypeFile:
var fileUrl, fileURI string
if one.GetFileURL() != "" {
fileUrl = one.GetFileURL()
} else if one.GetFileID() != 0 {
fileInfo, err := a.UploaodDomainSVC.GetFile(ctx, &uploadService.GetFileRequest{
ID: one.GetFileID(),
})
if err != nil {
return nil, contentType, err
}
fileUrl = fileInfo.File.Url
fileURI = fileInfo.File.TosURI
}
multiContents = append(multiContents, &message.InputMetaData{
Type: message.InputType(one.Type),
FileData: []*message.FileData{
{
Url: one.GetFileURL(),
Url: fileUrl,
URI: fileURI,
},
},
})
@@ -328,3 +345,62 @@ func buildARSM2ApiChatMessage(chunk *entity.AgentRunResponse) []byte {
mCM, _ := json.Marshal(chunkMessage)
return mCM
}
func (a *OpenapiAgentRunApplication) CancelRun(ctx context.Context, req *run.CancelChatApiRequest) (*run.CancelChatApiResponse, error) {
resp := new(run.CancelChatApiResponse)
apiKeyInfo := ctxutil.GetApiAuthFromCtx(ctx)
userID := apiKeyInfo.UserID
runRecord, err := ConversationSVC.AgentRunDomainSVC.GetByID(ctx, req.ChatID)
if err != nil {
return nil, err
}
if runRecord == nil {
return nil, errorx.New(errno.ErrRecordNotFound)
}
conversationData, err := ConversationSVC.ConversationDomainSVC.GetByID(ctx, req.ConversationID)
if err != nil {
return nil, err
}
if userID != conversationData.CreatorID {
return nil, errorx.New(errno.ErrConversationPermissionCode, errorx.KV("msg", "user not match"))
}
if runRecord.Status != entity.RunStatusInProgress && runRecord.Status != entity.RunStatusCreated {
return nil, errorx.New(errno.ErrInProgressCanNotCancel)
}
runMeta, err := ConversationSVC.AgentRunDomainSVC.Cancel(ctx, &entity.CancelRunMeta{
RunID: req.ChatID,
ConversationID: req.ConversationID,
})
if err != nil {
return nil, err
}
if runMeta == nil {
return nil, errorx.New(errno.ErrConversationAgentRunError)
}
resp.ChatV3ChatDetail = &run.ChatV3ChatDetail{
ID: runMeta.ID,
ConversationID: runMeta.ConversationID,
BotID: runMeta.AgentID,
Status: string(runMeta.Status),
SectionID: ptr.Of(runMeta.SectionID),
CreatedAt: ptr.Of(int32(runMeta.CreatedAt / 1000)),
CompletedAt: ptr.Of(int32(runMeta.CompletedAt / 1000)),
FailedAt: ptr.Of(int32(runMeta.FailedAt / 1000)),
}
if runMeta.Usage != nil {
resp.ChatV3ChatDetail.Usage = &run.Usage{
TokenCount: ptr.Of(int32(runMeta.Usage.LlmTotalTokens)),
InputTokens: ptr.Of(int32(runMeta.Usage.LlmPromptTokens)),
OutputTokens: ptr.Of(int32(runMeta.Usage.LlmCompletionTokens)),
}
}
return resp, nil
}

View File

@@ -18,23 +18,28 @@ package conversation
import (
"context"
"encoding/json"
"strconv"
"github.com/coze-dev/coze-studio/backend/api/model/conversation/message"
"github.com/coze-dev/coze-studio/backend/api/model/conversation/run"
apiMessage "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/message"
message3 "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/message"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
convEntity "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/entity"
"github.com/coze-dev/coze-studio/backend/domain/conversation/message/entity"
uploadService "github.com/coze-dev/coze-studio/backend/domain/upload/service"
"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/lang/slices"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
type OpenapiMessageApplication struct{}
type OpenapiMessageApplication struct {
UploaodDomainSVC uploadService.UploadService
}
var OpenapiMessageApplicationService = new(OpenapiMessageApplication)
var OpenapiMessageSVC = new(OpenapiMessageApplication)
func (m *OpenapiMessageApplication) GetApiMessageList(ctx context.Context, mr *message.ListMessageApiRequest) (*message.ListMessageApiResponse, error) {
// Get Conversation ID by agent id & userID & scene
@@ -61,13 +66,21 @@ func (m *OpenapiMessageApplication) GetApiMessageList(ctx context.Context, mr *m
ConversationID: currentConversation.ID,
AgentID: currentConversation.AgentID,
Limit: int(ptr.From(mr.Limit)),
MessageType: []*message3.MessageType{
ptr.Of(message3.MessageTypeQuestion),
ptr.Of(message3.MessageTypeAnswer),
},
}
if mr.ChatID != nil {
msgListMeta.RunID = []*int64{mr.ChatID}
}
if mr.BeforeID != nil {
msgListMeta.Direction = entity.ScrollPageDirectionPrev
msgListMeta.Direction = entity.ScrollPageDirectionNext
msgListMeta.Cursor = *mr.BeforeID
} else {
msgListMeta.Direction = entity.ScrollPageDirectionNext
msgListMeta.Direction = entity.ScrollPageDirectionPrev
msgListMeta.Cursor = ptr.From(mr.AfterID)
}
if mr.Order == nil {
@@ -76,20 +89,14 @@ func (m *OpenapiMessageApplication) GetApiMessageList(ctx context.Context, mr *m
msgListMeta.OrderBy = mr.Order
}
mListMessages, err := ConversationSVC.MessageDomainSVC.List(ctx, msgListMeta)
mListMessages, err := ConversationSVC.MessageDomainSVC.ListWithoutPair(ctx, msgListMeta)
if err != nil {
return nil, err
}
// get agent id
var agentIDs []int64
for _, mOne := range mListMessages.Messages {
agentIDs = append(agentIDs, mOne.AgentID)
}
resp := m.buildMessageListResponse(ctx, mListMessages, currentConversation)
return resp, err
return resp, nil
}
func getConversation(ctx context.Context, conversationID int64) (*convEntity.Conversation, error) {
@@ -106,21 +113,22 @@ func (m *OpenapiMessageApplication) buildMessageListResponse(ctx context.Context
content := dm.Content
msg := &message.OpenMessageApi{
ID: dm.ID,
ConversationID: dm.ConversationID,
BotID: dm.AgentID,
Role: string(dm.Role),
Type: string(dm.MessageType),
Content: content,
ContentType: string(dm.ContentType),
SectionID: strconv.FormatInt(dm.SectionID, 10),
CreatedAt: dm.CreatedAt / 1000,
UpdatedAt: dm.UpdatedAt / 1000,
ChatID: dm.RunID,
MetaData: dm.Ext,
ID: dm.ID,
ConversationID: dm.ConversationID,
BotID: dm.AgentID,
Role: string(dm.Role),
Type: string(dm.MessageType),
Content: content,
ContentType: string(dm.ContentType),
SectionID: strconv.FormatInt(dm.SectionID, 10),
CreatedAt: dm.CreatedAt / 1000,
UpdatedAt: dm.UpdatedAt / 1000,
ChatID: dm.RunID,
MetaData: dm.Ext,
ReasoningContent: ptr.Of(dm.ReasoningContent),
}
if dm.ContentType == message3.ContentTypeMix && dm.DisplayContent != "" {
msg.Content = dm.DisplayContent
msg.Content = m.parseDisplayContent(ctx, dm)
msg.ContentType = run.ContentTypeMixApi
}
return msg
@@ -135,3 +143,38 @@ func (m *OpenapiMessageApplication) buildMessageListResponse(ctx context.Context
return resp
}
func (m *OpenapiMessageApplication) parseDisplayContent(ctx context.Context, dm *entity.Message) string {
var inputs []*run.AdditionalContent
err := json.Unmarshal([]byte(dm.DisplayContent), &inputs)
if err != nil {
return dm.DisplayContent
}
for k, one := range inputs {
if one == nil {
continue
}
switch apiMessage.InputType(one.Type) {
case apiMessage.InputTypeText:
continue
case apiMessage.InputTypeImage, apiMessage.InputTypeFile:
if one.GetFileID() != 0 {
fileInfo, err := m.UploaodDomainSVC.GetFile(ctx, &uploadService.GetFileRequest{
ID: one.GetFileID(),
})
if err == nil {
inputs[k].FileURL = ptr.Of(fileInfo.File.Url)
}
}
default:
continue
}
}
content, err := json.Marshal(inputs)
if err == nil {
dm.DisplayContent = string(content)
}
return dm.DisplayContent
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/pkg/errors"
"github.com/coze-dev/coze-studio/backend/api/model/app/bot_open_api"
openapimodel "github.com/coze-dev/coze-studio/backend/api/model/permission/openapiauth"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
openapi "github.com/coze-dev/coze-studio/backend/domain/openauth/openapiauth"
@@ -80,6 +81,7 @@ func (s *OpenAuthApplicationService) CreatePersonalAccessToken(ctx context.Conte
Name: req.Name,
Expire: req.ExpireAt,
UserID: *userID,
AkType: entity.AkTypeCustomer,
}
if req.DurationDay == "customize" {
@@ -111,6 +113,32 @@ func (s *OpenAuthApplicationService) CreatePersonalAccessToken(ctx context.Conte
return resp, nil
}
func (s *OpenAuthApplicationService) ImpersonateCozeUserAccessToken(ctx context.Context, req *bot_open_api.ImpersonateCozeUserRequest) (*bot_open_api.ImpersonateCozeUserResponse, error) {
resp := new(bot_open_api.ImpersonateCozeUserResponse)
userID := ctxutil.GetUIDFromCtx(ctx)
expiredSecond := time.Now().Add(time.Duration(time.Second * 60 * 15)).Unix()
appReq := &entity.CreateApiKey{
UserID: *userID,
AkType: entity.AkTypeTemporary,
Expire: expiredSecond,
Name: "temporary access token",
}
apiKeyResp, err := openapiAuthDomainSVC.Create(ctx, appReq)
if err != nil {
logs.CtxErrorf(ctx, "OpenAuthApplicationService.CreatePersonalAccessToken failed, err=%v", err)
return resp, errors.New("CreatePersonalAccessToken failed")
}
resp.Data = &bot_open_api.ImpersonateCozeUserResponseData{
AccessToken: apiKeyResp.ApiKey,
ExpiresIn: expiredSecond,
TokenType: "Bearer",
}
return resp, nil
}
func (s *OpenAuthApplicationService) ListPersonalAccessTokens(ctx context.Context, req *openapimodel.ListPersonalAccessTokensRequest) (*openapimodel.ListPersonalAccessTokensResponse, error) {
resp := new(openapimodel.ListPersonalAccessTokensResponse)

View File

@@ -23,10 +23,12 @@ import (
"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/database"
"github.com/coze-dev/coze-studio/backend/api/model/data/database/table"
"github.com/coze-dev/coze-studio/backend/api/model/resource/common"
"github.com/coze-dev/coze-studio/backend/api/model/workflow"
"github.com/coze-dev/coze-studio/backend/domain/knowledge/service"
dbservice "github.com/coze-dev/coze-studio/backend/domain/memory/database/service"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ternary"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
)
@@ -157,8 +159,39 @@ func (w *workflowPacker) GetDataInfo(ctx context.Context) (*dataInfo, error) {
}, nil
}
func (w *workflowPacker) GetActions(ctx context.Context) []*common.ResourceAction {
actions := []*common.ResourceAction{
{
Key: common.ActionKey_Edit,
Enable: true,
},
{
Key: common.ActionKey_Delete,
Enable: true,
},
{
Key: common.ActionKey_Copy,
Enable: true,
},
}
meta, err := w.appContext.WorkflowDomainSVC.Get(ctx, &vo.GetPolicy{
ID: w.resID,
MetaOnly: true,
})
if err != nil {
logs.CtxWarnf(ctx, "get policy failed with '%s', err=%v", w.resID, err)
return actions
}
key := ternary.IFElse(meta.Mode == workflow.WorkflowMode_Workflow, common.ActionKey_SwitchToChatflow, common.ActionKey_SwitchToFuncflow)
action := &common.ResourceAction{
Key: key,
Enable: true,
}
return append(actions, action)
}
func (w *workflowPacker) GetProjectDefaultActions(ctx context.Context) []*common.ProjectResourceAction {
return []*common.ProjectResourceAction{
actions := []*common.ProjectResourceAction{
{
Key: common.ProjectResourceActionKey_Rename,
Enable: true,
@@ -184,6 +217,21 @@ func (w *workflowPacker) GetProjectDefaultActions(ctx context.Context) []*common
Enable: true,
},
}
meta, err := w.appContext.WorkflowDomainSVC.Get(ctx, &vo.GetPolicy{
ID: w.resID,
MetaOnly: true,
})
if err != nil {
logs.CtxWarnf(ctx, "get policy failed with '%s', err=%v", w.resID, err)
return actions
}
key := ternary.IFElse(meta.Mode == workflow.WorkflowMode_Workflow, common.ProjectResourceActionKey_SwitchToChatflow, common.ProjectResourceActionKey_SwitchToFuncflow)
action := &common.ProjectResourceAction{
Enable: true,
Key: key,
}
return append(actions, action)
}
type knowledgePacker struct {

View File

@@ -370,6 +370,12 @@ func (s *SingleAgentApplicationService) applyAgentUpdates(target *entity.SingleA
}
target.Database = patch.DatabaseList
}
if patch.BotMode != nil {
target.BotMode = ptr.From(patch.BotMode)
}
if patch.LayoutInfo != nil {
target.LayoutInfo = patch.LayoutInfo
}
return target, nil
}
@@ -419,11 +425,12 @@ func (s *SingleAgentApplicationService) singleAgentDraftDo2Vo(ctx context.Contex
TaskInfo: &bot_common.TaskInfo{},
CreateTime: do.CreatedAt / 1000,
UpdateTime: do.UpdatedAt / 1000,
BotMode: bot_common.BotMode_SingleMode,
BotMode: do.BotMode,
BackgroundImageInfoList: do.BackgroundImageInfoList,
Status: bot_common.BotStatus_Using,
DatabaseList: do.Database,
ShortcutSort: do.ShortcutCommand,
LayoutInfo: do.LayoutInfo,
}
if do.VariablesMetaID != nil {
@@ -647,16 +654,24 @@ func (s *SingleAgentApplicationService) GetAgentOnlineInfo(ctx context.Context,
if connectorID == 0 {
connectorID = ctxutil.GetApiAuthFromCtx(ctx).ConnectorID
}
agentInfo, err := s.DomainSVC.ObtainAgentByIdentity(ctx, &entity.AgentIdentity{
AgentID: req.BotID,
return s.getAgentInfo(ctx, req.BotID, connectorID, uid, req.Version)
}
func (s *SingleAgentApplicationService) getAgentInfo(ctx context.Context, botID int64, connectorID int64, uid int64, version *string) (*bot_common.OpenAPIBotInfo, error) {
ae := &entity.AgentIdentity{
AgentID: botID,
ConnectorID: connectorID,
Version: ptr.From(req.Version),
})
}
if version != nil {
ae.Version = ptr.From(version)
}
agentInfo, err := s.DomainSVC.ObtainAgentByIdentity(ctx, ae)
if err != nil {
return nil, err
}
if agentInfo == nil {
logs.CtxErrorf(ctx, "agent(%d) is not exist", req.BotID)
logs.CtxErrorf(ctx, "agent(%d) is not exist", botID)
return nil, errorx.New(errno.ErrAgentPermissionCode, errorx.KV("msg", "agent not exist"))
}
if agentInfo.CreatorID != uid {
@@ -731,3 +746,21 @@ func (s *SingleAgentApplicationService) GetAgentOnlineInfo(ctx context.Context,
}
return combineInfo, nil
}
func (s *SingleAgentApplicationService) OpenGetBotInfo(ctx context.Context, req *bot_open_api.OpenGetBotInfoRequest) (*bot_open_api.OpenGetBotInfoResponse, error) {
resp := new(bot_open_api.OpenGetBotInfoResponse)
uid := ctxutil.MustGetUIDFromApiAuthCtx(ctx)
connectorID := ptr.From(req.ConnectorID)
if connectorID == 0 {
connectorID = ctxutil.GetApiAuthFromCtx(ctx).ConnectorID
}
agentInfo, err := s.getAgentInfo(ctx, req.BotID, connectorID, uid, nil)
if err != nil {
return nil, err
}
resp.Data = agentInfo
return resp, nil
}

View File

@@ -40,6 +40,7 @@ import (
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
"gorm.io/gorm"
"github.com/google/uuid"
@@ -50,7 +51,9 @@ import (
"github.com/coze-dev/coze-studio/backend/api/model/playground"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
"github.com/coze-dev/coze-studio/backend/domain/upload/entity"
"github.com/coze-dev/coze-studio/backend/domain/upload/service"
"github.com/coze-dev/coze-studio/backend/infra/contract/cache"
"github.com/coze-dev/coze-studio/backend/infra/contract/idgen"
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
@@ -61,16 +64,26 @@ import (
"github.com/coze-dev/coze-studio/backend/types/errno"
)
func InitService(oss storage.Storage, cache cache.Cmdable) {
SVC.cache = cache
SVC.oss = oss
func InitService(components *UploadComponents) *UploadService {
SVC.cache = components.Cache
SVC.oss = components.Oss
SVC.UploadSVC = service.NewUploadSVC(components.DB, components.Idgen, components.Oss)
return SVC
}
type UploadComponents struct {
Oss storage.Storage
Cache cache.Cmdable
DB *gorm.DB
Idgen idgen.IDGenerator
}
var SVC = &UploadService{}
type UploadService struct {
oss storage.Storage
cache cache.Cmdable
oss storage.Storage
cache cache.Cmdable
UploadSVC service.UploadService
}
const (
@@ -427,6 +440,23 @@ func (u *UploadService) UploadFileOpen(ctx context.Context, req *bot_open_api.Up
}
resp.File.CreatedAt = time.Now().Unix()
resp.File.URL = url
fileEntity := entity.File{
Name: fileHeader.Filename,
FileSize: fileHeader.Size,
TosURI: objName,
Status: entity.FileStatusValid,
CreatorID: strconv.FormatInt(uid, 10),
Source: entity.FileSourceAPI,
CozeAccountID: uid,
ContentType: fileHeader.Header.Get("Content-Type"),
CreatedAt: time.Now().UnixMilli(),
UpdatedAt: time.Now().UnixMilli(),
}
domainResp, err := u.UploadSVC.UploadFile(ctx, &service.UploadFileRequest{File: &fileEntity})
if err != nil {
return &resp, err
}
resp.File.ID = strconv.FormatInt(domainResp.File.ID, 10)
return &resp, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@ import (
"gorm.io/gorm"
"github.com/coze-dev/coze-studio/backend/crossdomain/impl/code"
knowledge "github.com/coze-dev/coze-studio/backend/domain/knowledge/service"
dbservice "github.com/coze-dev/coze-studio/backend/domain/memory/database/service"
variables "github.com/coze-dev/coze-studio/backend/domain/memory/variables/service"
@@ -85,15 +86,20 @@ func InitService(_ context.Context, components *ServiceComponents) (*Application
if err != nil {
return nil, err
}
workflowRepo := service.NewWorkflowRepository(components.IDGen, components.DB, components.Cache,
components.Tos, components.CPStore, components.WorkflowBuildInChatModel, cfg)
workflowRepo, err := service.NewWorkflowRepository(components.IDGen, components.DB, components.Cache,
components.Tos, components.CPStore, components.WorkflowBuildInChatModel, cfg)
if err != nil {
return nil, err
}
workflow.SetRepository(workflowRepo)
workflowDomainSVC := service.NewWorkflowService(workflowRepo)
code.SetCodeRunner(components.CodeRunner)
callbacks.AppendGlobalHandlers(workflowservice.GetTokenCallbackHandler())
setEventBus(components.DomainNotifier)
SVC.DomainSVC = workflowDomainSVC

View File

@@ -49,7 +49,6 @@ import (
crossuser "github.com/coze-dev/coze-studio/backend/crossdomain/contract/user"
search "github.com/coze-dev/coze-studio/backend/domain/search/entity"
domainWorkflow "github.com/coze-dev/coze-studio/backend/domain/workflow"
workflowDomain "github.com/coze-dev/coze-studio/backend/domain/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/infra/contract/idgen"
@@ -70,7 +69,7 @@ import (
)
type ApplicationService struct {
DomainSVC workflowDomain.Service
DomainSVC domainWorkflow.Service
ImageX imagex.ImageX // we set Imagex here, because Imagex is used as a proxy to get auth token, there is no actual correlation with the workflow domain.
TosClient storage.Storage
IDGenerator idgen.IDGenerator
@@ -169,6 +168,21 @@ func (w *ApplicationService) CreateWorkflow(ctx context.Context, req *workflow.C
if err := checkUserSpace(ctx, uID, spaceID); err != nil {
return nil, err
}
var createConversation bool
if req.ProjectID != nil && req.IsSetFlowMode() && req.GetFlowMode() == workflow.WorkflowMode_ChatFlow && req.IsSetCreateConversation() && req.GetCreateConversation() {
createConversation = true
_, err := GetWorkflowDomainSVC().CreateDraftConversationTemplate(ctx, &vo.CreateConversationTemplateMeta{
AppID: mustParseInt64(req.GetProjectID()),
UserID: uID,
SpaceID: spaceID,
Name: req.Name,
})
if err != nil {
return nil, err
}
}
wf := &vo.MetaCreate{
CreatorID: uID,
SpaceID: spaceID,
@@ -180,6 +194,14 @@ func (w *ApplicationService) CreateWorkflow(ctx context.Context, req *workflow.C
Mode: ternary.IFElse(req.IsSetFlowMode(), req.GetFlowMode(), workflow.WorkflowMode_Workflow),
InitCanvasSchema: vo.GetDefaultInitCanvasJsonSchema(i18n.GetLocale(ctx)),
}
if req.IsSetFlowMode() && req.GetFlowMode() == workflow.WorkflowMode_ChatFlow {
conversationName := req.Name
if !req.IsSetProjectID() || mustParseInt64(req.GetProjectID()) == 0 || !createConversation {
conversationName = "Default"
}
wf.InitCanvasSchema = vo.GetDefaultInitCanvasJsonSchemaChat(i18n.GetLocale(ctx), conversationName)
}
id, err := GetWorkflowDomainSVC().Create(ctx, wf)
if err != nil {
@@ -249,10 +271,12 @@ func (w *ApplicationService) UpdateWorkflowMeta(ctx context.Context, req *workfl
}
workflowID := mustParseInt64(req.GetWorkflowID())
err = GetWorkflowDomainSVC().UpdateMeta(ctx, workflowID, &vo.MetaUpdate{
Name: req.Name,
Desc: req.Desc,
IconURI: req.IconURI,
err = GetWorkflowDomainSVC().UpdateMeta(ctx, mustParseInt64(req.GetWorkflowID()), &vo.MetaUpdate{
Name: req.Name,
Desc: req.Desc,
IconURI: req.IconURI,
WorkflowMode: req.FlowMode,
})
if err != nil {
return nil, err
@@ -2106,6 +2130,10 @@ func (w *ApplicationService) ListWorkflow(ctx context.Context, req *workflow.Get
option.IDs = ids
}
if req.IsSetFlowMode() && req.GetFlowMode() != workflow.WorkflowMode_All {
option.Mode = ptr.Of(workflowModel.WorkflowMode(req.GetFlowMode()))
}
spaceID, err := strconv.ParseInt(req.GetSpaceID(), 10, 64)
if err != nil {
return nil, fmt.Errorf("space id is invalid, parse to int64 failed, err: %w", err)
@@ -2157,6 +2185,13 @@ func (w *ApplicationService) ListWorkflow(ctx context.Context, req *workflow.Get
},
}
if len(req.Checker) > 0 && status == workflow.WorkFlowListStatus_HadPublished {
ww.CheckResult, err = GetWorkflowDomainSVC().WorkflowSchemaCheck(ctx, w, req.Checker)
if err != nil {
return nil, err
}
}
if qType == workflowModel.FromDraft {
ww.UpdateTime = w.DraftMeta.Timestamp.Unix()
} else if qType == workflowModel.FromLatestVersion || qType == workflowModel.FromSpecificVersion {
@@ -3736,3 +3771,419 @@ func checkUserSpace(ctx context.Context, uid int64, spaceID int64) error {
return nil
}
func (w *ApplicationService) populateChatFlowRoleFields(role *workflow.ChatFlowRole, targetRole interface{}) error {
var avatarUri, audioStr, bgStr, obStr, srStr, uiStr string
var err error
if role.Avatar != nil {
avatarUri = role.Avatar.ImageUri
}
if role.AudioConfig != nil {
audioStr, err = sonic.MarshalString(*role.AudioConfig)
if err != nil {
return vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
}
if role.BackgroundImageInfo != nil {
bgStr, err = sonic.MarshalString(*role.BackgroundImageInfo)
if err != nil {
return vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
}
if role.OnboardingInfo != nil {
obStr, err = sonic.MarshalString(*role.OnboardingInfo)
if err != nil {
return vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
}
if role.SuggestReplyInfo != nil {
srStr, err = sonic.MarshalString(*role.SuggestReplyInfo)
if err != nil {
return vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
}
if role.UserInputConfig != nil {
uiStr, err = sonic.MarshalString(*role.UserInputConfig)
if err != nil {
return vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
}
switch r := targetRole.(type) {
case *vo.ChatFlowRoleCreate:
if role.Name != nil {
r.Name = *role.Name
}
if role.Description != nil {
r.Description = *role.Description
}
if avatarUri != "" {
r.AvatarUri = avatarUri
}
if audioStr != "" {
r.AudioConfig = audioStr
}
if bgStr != "" {
r.BackgroundImageInfo = bgStr
}
if obStr != "" {
r.OnboardingInfo = obStr
}
if srStr != "" {
r.SuggestReplyInfo = srStr
}
if uiStr != "" {
r.UserInputConfig = uiStr
}
case *vo.ChatFlowRoleUpdate:
r.Name = role.Name
r.Description = role.Description
if avatarUri != "" {
r.AvatarUri = ptr.Of(avatarUri)
}
if audioStr != "" {
r.AudioConfig = ptr.Of(audioStr)
}
if bgStr != "" {
r.BackgroundImageInfo = ptr.Of(bgStr)
}
if obStr != "" {
r.OnboardingInfo = ptr.Of(obStr)
}
if srStr != "" {
r.SuggestReplyInfo = ptr.Of(srStr)
}
if uiStr != "" {
r.UserInputConfig = ptr.Of(uiStr)
}
default:
return vo.WrapError(errno.ErrInvalidParameter, fmt.Errorf("invalid type for targetRole: %T", targetRole))
}
return nil
}
func IsChatFlow(wf *entity.Workflow) bool {
if wf == nil || wf.ID == 0 {
return false
}
return wf.Meta.Mode == workflow.WorkflowMode_ChatFlow
}
func (w *ApplicationService) CreateChatFlowRole(ctx context.Context, req *workflow.CreateChatFlowRoleRequest) (
_ *workflow.CreateChatFlowRoleResponse, err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrChatFlowRoleOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
uID := ctxutil.MustGetUIDFromCtx(ctx)
wf, err := GetWorkflowDomainSVC().Get(ctx, &vo.GetPolicy{
ID: mustParseInt64(req.GetChatFlowRole().GetWorkflowID()),
MetaOnly: true,
})
if err != nil {
return nil, err
}
if err = checkUserSpace(ctx, uID, wf.Meta.SpaceID); err != nil {
return nil, err
}
role := req.GetChatFlowRole()
if !IsChatFlow(wf) {
logs.CtxWarnf(ctx, "CreateChatFlowRole not chat flow, workflowID: %d", wf.ID)
return nil, vo.WrapError(errno.ErrChatFlowRoleOperationFail, fmt.Errorf("workflow %d is not a chat flow", wf.ID))
}
oldRole, err := GetWorkflowDomainSVC().GetChatFlowRole(ctx, mustParseInt64(role.WorkflowID), "")
if err != nil {
return nil, err
}
var roleID int64
if oldRole != nil {
role.ID = strconv.FormatInt(oldRole.ID, 10)
roleID = oldRole.ID
}
if role.GetID() == "" || role.GetID() == "0" {
chatFlowRole := &vo.ChatFlowRoleCreate{
WorkflowID: mustParseInt64(role.WorkflowID),
CreatorID: uID,
}
if err = w.populateChatFlowRoleFields(role, chatFlowRole); err != nil {
return nil, err
}
roleID, err = GetWorkflowDomainSVC().CreateChatFlowRole(ctx, chatFlowRole)
if err != nil {
return nil, err
}
} else {
chatFlowRole := &vo.ChatFlowRoleUpdate{
WorkflowID: mustParseInt64(role.WorkflowID),
}
if err = w.populateChatFlowRoleFields(role, chatFlowRole); err != nil {
return nil, err
}
err = GetWorkflowDomainSVC().UpdateChatFlowRole(ctx, chatFlowRole.WorkflowID, chatFlowRole)
if err != nil {
return nil, err
}
}
return &workflow.CreateChatFlowRoleResponse{
ID: strconv.FormatInt(roleID, 10),
}, nil
}
func (w *ApplicationService) DeleteChatFlowRole(ctx context.Context, req *workflow.DeleteChatFlowRoleRequest) (
_ *workflow.DeleteChatFlowRoleResponse, err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrChatFlowRoleOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
uID := ctxutil.MustGetUIDFromCtx(ctx)
wf, err := GetWorkflowDomainSVC().Get(ctx, &vo.GetPolicy{
ID: mustParseInt64(req.GetWorkflowID()),
MetaOnly: true,
})
if err != nil {
return nil, err
}
if err = checkUserSpace(ctx, uID, wf.Meta.SpaceID); err != nil {
return nil, err
}
err = GetWorkflowDomainSVC().DeleteChatFlowRole(ctx, mustParseInt64(req.ID), mustParseInt64(req.WorkflowID))
if err != nil {
return nil, err
}
return &workflow.DeleteChatFlowRoleResponse{}, nil
}
func (w *ApplicationService) GetChatFlowRole(ctx context.Context, req *workflow.GetChatFlowRoleRequest) (
_ *workflow.GetChatFlowRoleResponse, err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrChatFlowRoleOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
uID := ctxutil.MustGetUIDFromCtx(ctx)
wf, err := GetWorkflowDomainSVC().Get(ctx, &vo.GetPolicy{
ID: mustParseInt64(req.GetWorkflowID()),
MetaOnly: true,
})
if err != nil {
return nil, err
}
if err = checkUserSpace(ctx, uID, wf.Meta.SpaceID); err != nil {
return nil, err
}
if !IsChatFlow(wf) {
logs.CtxWarnf(ctx, "GetChatFlowRole not chat flow, workflowID: %d", wf.ID)
return nil, vo.WrapError(errno.ErrChatFlowRoleOperationFail, fmt.Errorf("workflow %d is not a chat flow", wf.ID))
}
var version string
if wf.Meta.AppID != nil {
if vl, err := GetWorkflowDomainSVC().GetWorkflowVersionsByConnector(ctx, mustParseInt64(req.GetConnectorID()), wf.ID, 1); err != nil {
return nil, err
} else if len(vl) > 0 {
version = vl[0]
}
}
role, err := GetWorkflowDomainSVC().GetChatFlowRole(ctx, mustParseInt64(req.WorkflowID), version)
if err != nil {
return nil, err
}
if role == nil {
logs.CtxWarnf(ctx, "GetChatFlowRole role nil, workflowID: %d", wf.ID)
// Return nil for the error to align with the production behavior,
// where the GET API may be called before the CREATE API during chatflow creation.
return &workflow.GetChatFlowRoleResponse{}, nil
}
wfRole, err := w.convertChatFlowRole(ctx, role)
if err != nil {
return nil, fmt.Errorf("failed to get chat flow role config, internal data processing error: %+v", err)
}
return &workflow.GetChatFlowRoleResponse{
Role: wfRole,
}, nil
}
func (w *ApplicationService) convertChatFlowRole(ctx context.Context, role *entity.ChatFlowRole) (*workflow.ChatFlowRole, error) {
var err error
res := &workflow.ChatFlowRole{
ID: strconv.FormatInt(role.ID, 10),
WorkflowID: strconv.FormatInt(role.WorkflowID, 10),
Name: ptr.Of(role.Name),
Description: ptr.Of(role.Description),
}
if role.AvatarUri != "" {
url, err := w.ImageX.GetResourceURL(ctx, role.AvatarUri)
if err != nil {
return nil, err
}
res.Avatar = &workflow.AvatarConfig{
ImageUri: role.AvatarUri,
ImageUrl: url.URL,
}
}
if role.AudioConfig != "" {
err = sonic.UnmarshalString(role.AudioConfig, &res.AudioConfig)
if err != nil {
logs.CtxErrorf(ctx, "GetChatFlowRole AudioConfig UnmarshalString err: %+v", err)
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
}
if role.OnboardingInfo != "" {
err = sonic.UnmarshalString(role.OnboardingInfo, &res.OnboardingInfo)
if err != nil {
logs.CtxErrorf(ctx, "GetChatFlowRole OnboardingInfo UnmarshalString err: %+v", err)
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
}
if role.SuggestReplyInfo != "" {
err = sonic.UnmarshalString(role.SuggestReplyInfo, &res.SuggestReplyInfo)
if err != nil {
logs.CtxErrorf(ctx, "GetChatFlowRole SuggestReplyInfo UnmarshalString err: %+v", err)
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
}
if role.UserInputConfig != "" {
err = sonic.UnmarshalString(role.UserInputConfig, &res.UserInputConfig)
if err != nil {
logs.CtxErrorf(ctx, "GetChatFlowRole UserInputConfig UnmarshalString err: %+v", err)
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
}
if role.BackgroundImageInfo != "" {
res.BackgroundImageInfo = &workflow.BackgroundImageInfo{}
err = sonic.UnmarshalString(role.BackgroundImageInfo, res.BackgroundImageInfo)
if err != nil {
logs.CtxErrorf(ctx, "GetChatFlowRole BackgroundImageInfo UnmarshalString err: %+v", err)
return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
}
if res.BackgroundImageInfo != nil {
if res.BackgroundImageInfo.WebBackgroundImage != nil && res.BackgroundImageInfo.WebBackgroundImage.OriginImageUri != nil {
url, err := w.ImageX.GetResourceURL(ctx, res.BackgroundImageInfo.WebBackgroundImage.GetOriginImageUri())
if err != nil {
logs.CtxErrorf(ctx, "get url by uri err, err:%s", err.Error())
return nil, err
}
res.BackgroundImageInfo.WebBackgroundImage.ImageUrl = &url.URL
}
if res.BackgroundImageInfo.MobileBackgroundImage != nil && res.BackgroundImageInfo.MobileBackgroundImage.OriginImageUri != nil {
url, err := w.ImageX.GetResourceURL(ctx, res.BackgroundImageInfo.MobileBackgroundImage.GetOriginImageUri())
if err != nil {
logs.CtxErrorf(ctx, "get url by uri err, err:%s", err.Error())
return nil, err
}
res.BackgroundImageInfo.MobileBackgroundImage.ImageUrl = &url.URL
}
}
}
return res, nil
}
func (w *ApplicationService) OpenAPIGetWorkflowInfo(ctx context.Context, req *workflow.OpenAPIGetWorkflowInfoRequest) (
_ *workflow.OpenAPIGetWorkflowInfoResponse, err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = safego.NewPanicErr(panicErr, debug.Stack())
}
if err != nil {
err = vo.WrapIfNeeded(errno.ErrChatFlowRoleOperationFail, err, errorx.KV("cause", vo.UnwrapRootErr(err).Error()))
}
}()
uID := ctxutil.GetApiAuthFromCtx(ctx).UserID
wf, err := GetWorkflowDomainSVC().Get(ctx, &vo.GetPolicy{
ID: mustParseInt64(req.GetWorkflowID()),
MetaOnly: true,
})
if err != nil {
return nil, err
}
if err = checkUserSpace(ctx, uID, wf.Meta.SpaceID); err != nil {
return nil, err
}
if !IsChatFlow(wf) {
logs.CtxWarnf(ctx, "GetChatFlowRole not chat flow, workflowID: %d", wf.ID)
return nil, vo.WrapError(errno.ErrChatFlowRoleOperationFail, fmt.Errorf("workflow %d is not a chat flow", wf.ID))
}
var version string
if wf.Meta.AppID != nil {
if vl, err := GetWorkflowDomainSVC().GetWorkflowVersionsByConnector(ctx, mustParseInt64(req.GetConnectorID()), wf.ID, 1); err != nil {
return nil, err
} else if len(vl) > 0 {
version = vl[0]
}
}
role, err := GetWorkflowDomainSVC().GetChatFlowRole(ctx, mustParseInt64(req.WorkflowID), version)
if err != nil {
return nil, err
}
if role == nil {
logs.CtxWarnf(ctx, "GetChatFlowRole role nil, workflowID: %d", wf.ID)
// Return nil for the error to align with the production behavior,
// where the GET API may be called before the CREATE API during chatflow creation.
return &workflow.OpenAPIGetWorkflowInfoResponse{}, nil
}
wfRole, err := w.convertChatFlowRole(ctx, role)
if err != nil {
return nil, fmt.Errorf("failed to get chat flow role config, internal data processing error: %+v", err)
}
return &workflow.OpenAPIGetWorkflowInfoResponse{
WorkflowInfo: &workflow.WorkflowInfo{
Role: wfRole,
},
}, nil
}