coze-studio/backend/application/search/project_search.go

451 lines
14 KiB
Go

/*
* 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 search
import (
"context"
"fmt"
"sync"
"time"
search2 "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/search"
"github.com/coze-dev/coze-studio/backend/api/model/flow/marketplace/marketplace_common"
"github.com/coze-dev/coze-studio/backend/api/model/flow/marketplace/product_common"
"github.com/coze-dev/coze-studio/backend/api/model/flow/marketplace/product_public_api"
"github.com/coze-dev/coze-studio/backend/api/model/intelligence"
"github.com/coze-dev/coze-studio/backend/api/model/intelligence/common"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
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/ternary"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/pkg/taskgroup"
"github.com/coze-dev/coze-studio/backend/types/consts"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
var projectType2iconURI = map[common.IntelligenceType]string{
common.IntelligenceType_Bot: consts.DefaultAgentIcon,
common.IntelligenceType_Project: consts.DefaultAppIcon,
}
func (s *SearchApplicationService) GetDraftIntelligenceList(ctx context.Context, req *intelligence.GetDraftIntelligenceListRequest) (
resp *intelligence.GetDraftIntelligenceListResponse, err error,
) {
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrSearchPermissionCode, errorx.KV("msg", "session is required"))
}
do := searchRequestTo2Do(*userID, req)
searchResp, err := s.DomainSVC.SearchProjects(ctx, do)
if err != nil {
return nil, err
}
if len(searchResp.Data) == 0 {
return &intelligence.GetDraftIntelligenceListResponse{
Data: &intelligence.DraftIntelligenceListData{
Intelligences: make([]*intelligence.IntelligenceData, 0),
Total: 0,
HasMore: false,
NextCursorID: "",
},
}, nil
}
tasks := taskgroup.NewUninterruptibleTaskGroup(ctx, len(searchResp.Data))
lock := sync.Mutex{}
intelligenceDataList := make([]*intelligence.IntelligenceData, len(searchResp.Data))
logs.CtxDebugf(ctx, "[GetDraftIntelligenceList] searchResp.Data: %v", conv.DebugJsonToStr(searchResp.Data))
for idx := range searchResp.Data {
data := searchResp.Data[idx]
index := idx
tasks.Go(func() error {
info, err := s.packIntelligenceData(ctx, data)
if err != nil {
logs.CtxErrorf(ctx, "[packIntelligenceData] failed id %v, type %d , name %s, err: %v", data.ID, data.Type, data.GetName(), err)
return err
}
lock.Lock()
defer lock.Unlock()
intelligenceDataList[index] = info
return nil
})
}
_ = tasks.Wait()
filterDataList := make([]*intelligence.IntelligenceData, 0)
for _, data := range intelligenceDataList {
if data != nil {
filterDataList = append(filterDataList, data)
}
}
return &intelligence.GetDraftIntelligenceListResponse{
Code: 0,
Data: &intelligence.DraftIntelligenceListData{
Intelligences: filterDataList,
Total: int32(len(filterDataList)),
HasMore: searchResp.HasMore,
NextCursorID: searchResp.NextCursor,
},
}, nil
}
func (s *SearchApplicationService) PublicFavoriteProduct(ctx context.Context, req *product_public_api.FavoriteProductRequest) (*product_public_api.FavoriteProductResponse, error) {
isFav := !req.GetIsCancel()
entityID := req.GetEntityID()
typ := req.GetEntityType()
switch req.GetEntityType() {
case product_common.ProductEntityType_Bot, product_common.ProductEntityType_Project:
err := s.favoriteProject(ctx, entityID, typ, isFav)
if err != nil {
return nil, err
}
default:
return nil, errorx.New(errno.ErrSearchInvalidParamCode, errorx.KV("msg", fmt.Sprintf("invalid entity type '%d'", req.GetEntityType())))
}
return &product_public_api.FavoriteProductResponse{
IsFirstFavorite: ptr.Of(false),
}, nil
}
func (s *SearchApplicationService) favoriteProject(ctx context.Context, projectID int64, typ product_common.ProductEntityType, isFav bool) error {
var entityType common.IntelligenceType
if typ == product_common.ProductEntityType_Bot {
entityType = common.IntelligenceType_Bot
} else {
entityType = common.IntelligenceType_Project
}
err := s.ProjectEventBus.PublishProject(ctx, &searchEntity.ProjectDomainEvent{
OpType: searchEntity.Updated,
Project: &searchEntity.ProjectDocument{
ID: projectID,
IsFav: ptr.Of(ternary.IFElse(isFav, 1, 0)),
FavTimeMS: ptr.Of(time.Now().UnixMilli()),
Type: entityType,
},
})
if err != nil {
return err
}
return nil
}
func (s *SearchApplicationService) PublicGetUserFavoriteList(ctx context.Context, req *product_public_api.GetUserFavoriteListV2Request) (resp *product_public_api.GetUserFavoriteListV2Response, err error) {
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrSearchPermissionCode, errorx.KV("msg", "session required"))
}
var data *product_public_api.GetUserFavoriteListDataV2
switch req.GetEntityType() {
case product_common.ProductEntityType_Project, product_common.ProductEntityType_Bot, product_common.ProductEntityType_Common:
data, err = s.searchFavProjects(ctx, *userID, req)
default:
return nil, errorx.New(errno.ErrSearchInvalidParamCode, errorx.KV("msg", fmt.Sprintf("invalid entity type '%d'", req.GetEntityType())))
}
if err != nil {
return nil, err
}
resp = &product_public_api.GetUserFavoriteListV2Response{
Data: data,
}
return resp, nil
}
func (s *SearchApplicationService) searchFavProjects(ctx context.Context, userID int64, req *product_public_api.GetUserFavoriteListV2Request) (*product_public_api.GetUserFavoriteListDataV2, error) {
var types []common.IntelligenceType
if req.GetEntityType() == product_common.ProductEntityType_Common {
types = []common.IntelligenceType{common.IntelligenceType_Bot, common.IntelligenceType_Project}
} else if req.GetEntityType() == product_common.ProductEntityType_Bot {
types = []common.IntelligenceType{common.IntelligenceType_Bot}
} else {
types = []common.IntelligenceType{common.IntelligenceType_Project}
}
res, err := SearchSVC.DomainSVC.SearchProjects(ctx, &searchEntity.SearchProjectsRequest{
OwnerID: userID,
Types: types,
IsFav: true,
OrderFiledName: search2.FieldOfFavTime,
OrderAsc: false,
Limit: req.PageSize,
Cursor: req.GetCursorID(),
})
if err != nil {
return nil, err
}
if len(res.Data) == 0 {
return &product_public_api.GetUserFavoriteListDataV2{
FavoriteEntities: []*product_common.FavoriteEntity{},
CursorID: res.NextCursor,
HasMore: res.HasMore,
}, nil
}
favEntities := make([]*product_common.FavoriteEntity, 0, len(res.Data))
for _, r := range res.Data {
favEntity, err := s.projectResourceToProductInfo(ctx, userID, r)
if err != nil {
logs.CtxErrorf(ctx, "[pluginResourceToProductInfo] failed to get project info, id=%v, type=%d, err=%v",
r.ID, r.Type, err)
continue
}
favEntities = append(favEntities, favEntity)
}
data := &product_public_api.GetUserFavoriteListDataV2{
FavoriteEntities: favEntities,
CursorID: res.NextCursor,
HasMore: res.HasMore,
}
return data, nil
}
func (s *SearchApplicationService) projectResourceToProductInfo(ctx context.Context, userID int64, doc *searchEntity.ProjectDocument) (favEntity *product_common.FavoriteEntity, err error) {
typ := func() product_common.ProductEntityType {
if doc.Type == common.IntelligenceType_Bot {
return product_common.ProductEntityType_Bot
}
return product_common.ProductEntityType_Project
}()
packer, err := NewPackProject(userID, doc.ID, doc.Type, s)
if err != nil {
return nil, err
}
pi, err := packer.GetProjectInfo(ctx)
if err != nil {
return nil, err
}
ui := packer.GetUserInfo(ctx, userID)
var userInfo *product_common.UserInfo
if ui != nil {
userInfo = &product_common.UserInfo{
UserID: ui.UserID,
UserName: ui.UserUniqueName,
Name: ui.Nickname,
AvatarURL: ui.AvatarURL,
FollowType: ptr.Of(marketplace_common.FollowType_Unknown),
}
}
e := &product_common.FavoriteEntity{
EntityID: doc.ID,
EntityType: typ,
Name: doc.GetName(),
IconURL: pi.iconURI,
Description: pi.desc,
SpaceID: doc.GetSpaceID(),
HasSpacePermission: true,
FavoriteAt: doc.GetFavTime(),
UserInfo: userInfo,
}
return e, nil
}
func (s *SearchApplicationService) GetUserRecentlyEditIntelligence(ctx context.Context, req intelligence.GetUserRecentlyEditIntelligenceRequest) (
resp *intelligence.GetUserRecentlyEditIntelligenceResponse, err error,
) {
userID := ctxutil.GetUIDFromCtx(ctx)
if userID == nil {
return nil, errorx.New(errno.ErrSearchPermissionCode, errorx.KV("msg", "session required"))
}
res, err := SearchSVC.DomainSVC.SearchProjects(ctx, &searchEntity.SearchProjectsRequest{
OwnerID: *userID,
Types: req.Types,
IsRecentlyOpen: true,
OrderFiledName: search2.FieldOfRecentlyOpenTime,
OrderAsc: false,
Limit: req.Size,
})
if err != nil {
return nil, err
}
intelligenceDataList := make([]*intelligence.IntelligenceData, 0, len(res.Data))
for idx := range res.Data {
data := res.Data[idx]
info, err := s.packIntelligenceData(ctx, data)
if err != nil {
logs.CtxErrorf(ctx, "[packIntelligenceData] failed id %v, type %d, name %s, err: %v", data.ID, data.Type, data.GetName(), err)
continue
}
intelligenceDataList = append(intelligenceDataList, info)
}
resp = &intelligence.GetUserRecentlyEditIntelligenceResponse{
Data: &intelligence.GetUserRecentlyEditIntelligenceData{
IntelligenceInfoList: intelligenceDataList,
},
}
return resp, nil
}
func (s *SearchApplicationService) packIntelligenceData(ctx context.Context, doc *searchEntity.ProjectDocument) (*intelligence.IntelligenceData, error) {
intelligenceData := &intelligence.IntelligenceData{
Type: doc.Type,
BasicInfo: &common.IntelligenceBasicInfo{
ID: doc.ID,
Name: doc.GetName(),
SpaceID: doc.GetSpaceID(),
OwnerID: doc.GetOwnerID(),
Status: doc.Status,
CreateTime: doc.GetCreateTime() / 1000,
UpdateTime: doc.GetUpdateTime() / 1000,
PublishTime: doc.GetPublishTime() / 1000,
},
}
uid := ctxutil.MustGetUIDFromCtx(ctx)
packer, err := NewPackProject(uid, doc.ID, doc.Type, s)
if err != nil {
return nil, err
}
projInfo, err := packer.GetProjectInfo(ctx)
if err != nil {
return nil, errorx.Wrapf(err, "GetProjectInfo failed, id: %v, type: %v", doc.ID, doc.Type)
}
intelligenceData.BasicInfo.Description = projInfo.desc
intelligenceData.BasicInfo.IconURI = projInfo.iconURI
intelligenceData.BasicInfo.IconURL = s.getProjectIconURL(ctx, projInfo.iconURI, doc.Type)
intelligenceData.PermissionInfo = packer.GetPermissionInfo()
publishedInf := packer.GetPublishedInfo(ctx)
if publishedInf != nil {
intelligenceData.PublishInfo = packer.GetPublishedInfo(ctx)
} else {
intelligenceData.PublishInfo = &intelligence.IntelligencePublishInfo{
HasPublished: false,
}
}
intelligenceData.OwnerInfo = packer.GetUserInfo(ctx, doc.GetOwnerID())
intelligenceData.LatestAuditInfo = &common.AuditInfo{}
intelligenceData.FavoriteInfo = s.buildProjectFavoriteInfo(doc)
intelligenceData.OtherInfo = s.buildProjectOtherInfo(doc)
return intelligenceData, nil
}
func (s *SearchApplicationService) buildProjectFavoriteInfo(doc *searchEntity.ProjectDocument) *intelligence.FavoriteInfo {
isFav := doc.GetIsFav()
favTime := doc.GetFavTime()
return &intelligence.FavoriteInfo{
IsFav: isFav,
FavTime: conv.Int64ToStr(favTime / 1000),
}
}
func (s *SearchApplicationService) buildProjectOtherInfo(doc *searchEntity.ProjectDocument) *intelligence.OtherInfo {
otherInfo := &intelligence.OtherInfo{
BotMode: intelligence.BotMode_SingleMode,
RecentlyOpenTime: conv.Int64ToStr(doc.GetRecentlyOpenTime() / 1000),
}
if doc.Type == common.IntelligenceType_Project {
otherInfo.BotMode = intelligence.BotMode_WorkflowMode
}
return otherInfo
}
func searchRequestTo2Do(userID int64, req *intelligence.GetDraftIntelligenceListRequest) *searchEntity.SearchProjectsRequest {
orderBy := func() string {
switch req.GetOrderBy() {
case intelligence.OrderBy_PublishTime:
return search2.FieldOfPublishTime
case intelligence.OrderBy_UpdateTime:
return search2.FieldOfUpdateTime
case intelligence.OrderBy_CreateTime:
return search2.FieldOfCreateTime
default:
return search2.FieldOfUpdateTime
}
}()
searchReq := &searchEntity.SearchProjectsRequest{
SpaceID: req.GetSpaceID(),
Name: req.GetName(),
OwnerID: 0,
Limit: req.GetSize(),
Cursor: req.GetCursorID(),
OrderFiledName: orderBy,
OrderAsc: false,
Types: req.GetTypes(),
Status: req.GetStatus(),
IsFav: req.GetIsFav(),
IsRecentlyOpen: req.GetRecentlyOpen(),
IsPublished: req.GetHasPublished(),
}
if req.GetSearchScope() == intelligence.SearchScope_CreateByMe {
searchReq.OwnerID = userID
}
return searchReq
}
func (s *SearchApplicationService) getProjectDefaultIconURL(ctx context.Context, tp common.IntelligenceType) string {
iconURL, ok := projectType2iconURI[tp]
if !ok {
logs.CtxWarnf(ctx, "[getProjectDefaultIconURL] don't have type: %d default icon", tp)
return ""
}
return s.getURL(ctx, iconURL)
}
func (s *SearchApplicationService) getProjectIconURL(ctx context.Context, uri string, tp common.IntelligenceType) string {
if uri == "" {
return s.getProjectDefaultIconURL(ctx, tp)
}
url := s.getURL(ctx, uri)
if url != "" {
return url
}
return s.getProjectDefaultIconURL(ctx, tp)
}