463 lines
15 KiB
Go
463 lines
15 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"
|
|
|
|
"github.com/coze-dev/coze-studio/backend/api/model/app/intelligence"
|
|
"github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/common"
|
|
search2 "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/search"
|
|
"github.com/coze-dev/coze-studio/backend/api/model/marketplace/marketplace_common"
|
|
"github.com/coze-dev/coze-studio/backend/api/model/marketplace/product_common"
|
|
"github.com/coze-dev/coze-studio/backend/api/model/marketplace/product_public_api"
|
|
"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))
|
|
if len(searchResp.Data) > 1 {
|
|
for idx := range searchResp.Data[1:] {
|
|
index := idx + 1
|
|
data := searchResp.Data[index]
|
|
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
|
|
})
|
|
}
|
|
}
|
|
if len(searchResp.Data) != 0 {
|
|
info, err := s.packIntelligenceData(ctx, searchResp.Data[0])
|
|
if err != nil {
|
|
logs.CtxErrorf(ctx, "[packIntelligenceData] failed id %v, type %d , name %s, err: %v", searchResp.Data[0].ID, searchResp.Data[0].Type, searchResp.Data[0].GetName(), err)
|
|
return nil, err
|
|
}
|
|
lock.Lock()
|
|
intelligenceDataList[0] = info
|
|
lock.Unlock()
|
|
}
|
|
err = tasks.Wait()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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)
|
|
}
|