407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			407 lines
		
	
	
		
			12 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"
 | |
| 	"errors"
 | |
| 	"slices"
 | |
| 	"strconv"
 | |
| 	"sync"
 | |
| 
 | |
| 	knowledgeModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/knowledge"
 | |
| 	"github.com/coze-dev/coze-studio/backend/api/model/resource"
 | |
| 	"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/domain/search/entity"
 | |
| 	search "github.com/coze-dev/coze-studio/backend/domain/search/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/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 SearchSVC = &SearchApplicationService{}
 | |
| 
 | |
| type SearchApplicationService struct {
 | |
| 	*ServiceComponents
 | |
| 	DomainSVC search.Search
 | |
| }
 | |
| 
 | |
| var resType2iconURI = map[common.ResType]string{
 | |
| 	common.ResType_Plugin:    consts.DefaultPluginIcon,
 | |
| 	common.ResType_Workflow:  consts.DefaultWorkflowIcon,
 | |
| 	common.ResType_Knowledge: consts.DefaultDatasetIcon,
 | |
| 	common.ResType_Prompt:    consts.DefaultPromptIcon,
 | |
| 	common.ResType_Database:  consts.DefaultDatabaseIcon,
 | |
| 	// ResType_UI:        consts.DefaultWorkflowIcon,
 | |
| 	// ResType_Voice:     consts.DefaultPluginIcon,
 | |
| 	// ResType_Imageflow: consts.DefaultPluginIcon,
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) LibraryResourceList(ctx context.Context, req *resource.LibraryResourceListRequest) (resp *resource.LibraryResourceListResponse, err error) {
 | |
| 	userID := ctxutil.GetUIDFromCtx(ctx)
 | |
| 	if userID == nil {
 | |
| 		return nil, errorx.New(errno.ErrSearchPermissionCode, errorx.KV("msg", "session required"))
 | |
| 	}
 | |
| 
 | |
| 	searchReq := &entity.SearchResourcesRequest{
 | |
| 		SpaceID:             req.GetSpaceID(),
 | |
| 		OwnerID:             0,
 | |
| 		Name:                req.GetName(),
 | |
| 		ResTypeFilter:       req.GetResTypeFilter(),
 | |
| 		PublishStatusFilter: req.GetPublishStatusFilter(),
 | |
| 		SearchKeys:          req.GetSearchKeys(),
 | |
| 		Cursor:              req.GetCursor(),
 | |
| 		Limit:               req.GetSize(),
 | |
| 	}
 | |
| 
 | |
| 	// 设置用户过滤
 | |
| 	if req.IsSetUserFilter() && req.GetUserFilter() > 0 {
 | |
| 		searchReq.OwnerID = ptr.From(userID)
 | |
| 	}
 | |
| 
 | |
| 	searchResp, err := s.DomainSVC.SearchResources(ctx, searchReq)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	lock := sync.Mutex{}
 | |
| 	tasks := taskgroup.NewUninterruptibleTaskGroup(ctx, 10)
 | |
| 	resources := make([]*common.ResourceInfo, len(searchResp.Data))
 | |
| 	if len(searchResp.Data) > 1 {
 | |
| 		for idx := range searchResp.Data[1:] {
 | |
| 			index := idx + 1
 | |
| 			v := searchResp.Data[index]
 | |
| 			tasks.Go(func() error {
 | |
| 				ri, err := s.packResource(ctx, v)
 | |
| 				if err != nil {
 | |
| 					logs.CtxErrorf(ctx, "[LibraryResourceList] packResource failed, will ignore resID: %d, Name : %s, resType: %d, err: %v",
 | |
| 						v.ResID, v.GetName(), v.ResType, err)
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 				lock.Lock()
 | |
| 				defer lock.Unlock()
 | |
| 				resources[index] = ri
 | |
| 				return nil
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| 	if len(searchResp.Data) != 0 {
 | |
| 		ri, err := s.packResource(ctx, searchResp.Data[0])
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		lock.Lock()
 | |
| 		resources[0] = ri
 | |
| 		lock.Unlock()
 | |
| 	}
 | |
| 	err = tasks.Wait()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	filterResource := make([]*common.ResourceInfo, 0)
 | |
| 	for _, res := range resources {
 | |
| 		if res == nil {
 | |
| 			continue
 | |
| 		}
 | |
| 		filterResource = append(filterResource, res)
 | |
| 	}
 | |
| 
 | |
| 	return &resource.LibraryResourceListResponse{
 | |
| 		Code:         0,
 | |
| 		ResourceList: filterResource,
 | |
| 		Cursor:       ptr.Of(searchResp.NextCursor),
 | |
| 		HasMore:      searchResp.HasMore,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) getResourceDefaultIconURL(ctx context.Context, tp common.ResType) string {
 | |
| 	iconURL, ok := resType2iconURI[tp]
 | |
| 	if !ok {
 | |
| 		logs.CtxWarnf(ctx, "[getDefaultIconURL] don't have type: %d  default icon", tp)
 | |
| 
 | |
| 		return ""
 | |
| 	}
 | |
| 
 | |
| 	return s.getURL(ctx, iconURL)
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) getURL(ctx context.Context, uri string) string {
 | |
| 	url, err := s.TOS.GetObjectUrl(ctx, uri)
 | |
| 	if err != nil {
 | |
| 		logs.CtxWarnf(ctx, "[getDefaultIconURLWitURI] GetObjectUrl failed, uri: %s, err: %v", uri, err)
 | |
| 
 | |
| 		return ""
 | |
| 	}
 | |
| 
 | |
| 	return url
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) getResourceIconURL(ctx context.Context, uri *string, tp common.ResType) string {
 | |
| 	if uri == nil || *uri == "" {
 | |
| 		return s.getResourceDefaultIconURL(ctx, tp)
 | |
| 	}
 | |
| 
 | |
| 	url := s.getURL(ctx, *uri)
 | |
| 	if url != "" {
 | |
| 		return url
 | |
| 	}
 | |
| 
 | |
| 	return s.getResourceDefaultIconURL(ctx, tp)
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) packUserInfo(ctx context.Context, ri *common.ResourceInfo, ownerID int64) *common.ResourceInfo {
 | |
| 	u, err := s.UserDomainSVC.GetUserInfo(ctx, ownerID)
 | |
| 	if err != nil {
 | |
| 		logs.CtxWarnf(ctx, "[LibraryResourceList] GetUserInfo failed, uid: %d, resID: %d, Name : %s, err: %v",
 | |
| 			ownerID, ri.ResID, ri.GetName(), err)
 | |
| 	} else {
 | |
| 		ri.CreatorName = ptr.Of(u.Name)
 | |
| 		ri.CreatorAvatar = ptr.Of(u.IconURL)
 | |
| 	}
 | |
| 
 | |
| 	if ri.GetCreatorAvatar() == "" {
 | |
| 		ri.CreatorAvatar = ptr.Of(s.getURL(ctx, consts.DefaultUserIcon))
 | |
| 	}
 | |
| 
 | |
| 	return ri
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) packResource(ctx context.Context, doc *entity.ResourceDocument) (*common.ResourceInfo, error) {
 | |
| 	ri := &common.ResourceInfo{
 | |
| 		ResID:         ptr.Of(doc.ResID),
 | |
| 		ResType:       ptr.Of(doc.ResType),
 | |
| 		Name:          doc.Name,
 | |
| 		SpaceID:       doc.SpaceID,
 | |
| 		CreatorID:     doc.OwnerID,
 | |
| 		ResSubType:    doc.ResSubType,
 | |
| 		PublishStatus: doc.PublishStatus,
 | |
| 		EditTime:      ptr.Of(doc.GetUpdateTime() / 1000),
 | |
| 	}
 | |
| 
 | |
| 	if doc.BizStatus != nil {
 | |
| 		ri.BizResStatus = ptr.Of(int32(*doc.BizStatus))
 | |
| 	}
 | |
| 
 | |
| 	packer, err := NewResourcePacker(doc.ResID, doc.ResType, s.ServiceComponents)
 | |
| 	if err != nil {
 | |
| 		return nil, errorx.Wrapf(err, "NewResourcePacker failed")
 | |
| 	}
 | |
| 
 | |
| 	ri = s.packUserInfo(ctx, ri, doc.GetOwnerID())
 | |
| 	ri.Actions = packer.GetActions(ctx)
 | |
| 
 | |
| 	data, err := packer.GetDataInfo(ctx)
 | |
| 	if err != nil {
 | |
| 		logs.CtxWarnf(ctx, "[packResource] GetDataInfo failed, resID: %d, Name : %s, resType: %d, err: %v",
 | |
| 			doc.ResID, doc.GetName(), doc.ResType, err)
 | |
| 
 | |
| 		ri.Icon = ptr.Of(s.getResourceDefaultIconURL(ctx, doc.ResType))
 | |
| 
 | |
| 		return ri, nil // Warn : weak dependency data
 | |
| 	}
 | |
| 	ri.BizResStatus = data.status
 | |
| 	ri.Desc = data.desc
 | |
| 	ri.Icon = ternary.IFElse(len(data.iconURL) > 0,
 | |
| 		&data.iconURL, ptr.Of(s.getResourceIconURL(ctx, data.iconURI, doc.ResType)))
 | |
| 	ri.BizExtend = map[string]string{
 | |
| 		"url": ptr.From(ri.Icon),
 | |
| 	}
 | |
| 	return ri, nil
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) ProjectResourceList(ctx context.Context, req *resource.ProjectResourceListRequest) (resp *resource.ProjectResourceListResponse, err error) {
 | |
| 	resources, err := s.getAPPAllResources(ctx, req.GetProjectID())
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	resourceGroups, err := s.packAPPResources(ctx, resources)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	resourceGroups = s.sortAPPResources(resourceGroups)
 | |
| 
 | |
| 	return &resource.ProjectResourceListResponse{
 | |
| 		ResourceGroups: resourceGroups,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) getAPPAllResources(ctx context.Context, appID int64) ([]*entity.ResourceDocument, error) {
 | |
| 	cursor := ""
 | |
| 	resources := make([]*entity.ResourceDocument, 0, 100)
 | |
| 
 | |
| 	for {
 | |
| 		res, err := s.DomainSVC.SearchResources(ctx, &entity.SearchResourcesRequest{
 | |
| 			APPID:  appID,
 | |
| 			Cursor: cursor,
 | |
| 			Limit:  100,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		resources = append(resources, res.Data...)
 | |
| 
 | |
| 		hasMore := res.HasMore
 | |
| 		cursor = res.NextCursor
 | |
| 
 | |
| 		if !hasMore {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return resources, nil
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) packAPPResources(ctx context.Context, resources []*entity.ResourceDocument) ([]*common.ProjectResourceGroup, error) {
 | |
| 	workflowGroup := &common.ProjectResourceGroup{
 | |
| 		GroupType:    common.ProjectResourceGroupType_Workflow,
 | |
| 		ResourceList: []*common.ProjectResourceInfo{},
 | |
| 	}
 | |
| 	dataGroup := &common.ProjectResourceGroup{
 | |
| 		GroupType:    common.ProjectResourceGroupType_Data,
 | |
| 		ResourceList: []*common.ProjectResourceInfo{},
 | |
| 	}
 | |
| 	pluginGroup := &common.ProjectResourceGroup{
 | |
| 		GroupType:    common.ProjectResourceGroupType_Plugin,
 | |
| 		ResourceList: []*common.ProjectResourceInfo{},
 | |
| 	}
 | |
| 
 | |
| 	lock := sync.Mutex{}
 | |
| 	tasks := taskgroup.NewUninterruptibleTaskGroup(ctx, 10)
 | |
| 	for idx := range resources {
 | |
| 		v := resources[idx]
 | |
| 
 | |
| 		tasks.Go(func() error {
 | |
| 			ri, err := s.packProjectResource(ctx, v)
 | |
| 			if err != nil {
 | |
| 				logs.CtxErrorf(ctx, "packAPPResources failed, will ignore resID: %d, Name : %s, resType: %d, err: %v",
 | |
| 					v.ResID, v.GetName(), v.ResType, err)
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			lock.Lock()
 | |
| 			defer lock.Unlock()
 | |
| 
 | |
| 			switch v.ResType {
 | |
| 			case common.ResType_Workflow:
 | |
| 				workflowGroup.ResourceList = append(workflowGroup.ResourceList, ri)
 | |
| 			case common.ResType_Plugin:
 | |
| 				pluginGroup.ResourceList = append(pluginGroup.ResourceList, ri)
 | |
| 			case common.ResType_Database, common.ResType_Knowledge:
 | |
| 				dataGroup.ResourceList = append(dataGroup.GetResourceList(), ri)
 | |
| 			default:
 | |
| 				logs.CtxWarnf(ctx, "unsupported resType: %d", v.ResType)
 | |
| 			}
 | |
| 
 | |
| 			return nil
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	_ = tasks.Wait()
 | |
| 
 | |
| 	resourceGroups := []*common.ProjectResourceGroup{
 | |
| 		workflowGroup,
 | |
| 		pluginGroup,
 | |
| 		dataGroup,
 | |
| 	}
 | |
| 
 | |
| 	return resourceGroups, nil
 | |
| }
 | |
| 
 | |
| func (s *SearchApplicationService) packProjectResource(ctx context.Context, resource *entity.ResourceDocument) (*common.ProjectResourceInfo, error) {
 | |
| 	packer, err := NewResourcePacker(resource.ResID, resource.ResType, s.ServiceComponents)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	info := &common.ProjectResourceInfo{
 | |
| 		ResID:      resource.ResID,
 | |
| 		ResType:    resource.ResType,
 | |
| 		ResSubType: resource.ResSubType,
 | |
| 		Name:       resource.GetName(),
 | |
| 		Actions:    packer.GetProjectDefaultActions(ctx),
 | |
| 	}
 | |
| 
 | |
| 	if resource.ResType == common.ResType_Knowledge {
 | |
| 		info.BizExtend = map[string]string{
 | |
| 			"format_type": strconv.FormatInt(int64(resource.GetResSubType()), 10),
 | |
| 		}
 | |
| 		di, err := packer.GetDataInfo(ctx)
 | |
| 		if err != nil {
 | |
| 			logs.CtxErrorf(ctx, "GetDataInfo failed, resID=%d, resType=%d, err=%v",
 | |
| 				resource.ResID, resource.ResType, err)
 | |
| 		} else {
 | |
| 			info.BizResStatus = ptr.Of(*di.status)
 | |
| 			if *di.status == int32(knowledgeModel.KnowledgeStatusDisable) {
 | |
| 				actions := slices.Clone(info.Actions)
 | |
| 				for _, a := range actions {
 | |
| 					if a.Key == common.ProjectResourceActionKey_Disable {
 | |
| 						a.Key = common.ProjectResourceActionKey_Enable
 | |
| 						break
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if resource.ResType == common.ResType_Plugin {
 | |
| 		err = s.PluginDomainSVC.CheckPluginToolsDebugStatus(ctx, resource.ResID)
 | |
| 		if err != nil {
 | |
| 			var e errorx.StatusError
 | |
| 			if !errors.As(err, &e) {
 | |
| 				logs.CtxErrorf(ctx, "CheckPluginToolsDebugStatus failed, resID=%d, resType=%d, err=%v",
 | |
| 					resource.ResID, resource.ResType, err)
 | |
| 			} else {
 | |
| 				actions := slices.Clone(info.Actions)
 | |
| 				for _, a := range actions {
 | |
| 					if a.Key == common.ProjectResourceActionKey_MoveToLibrary ||
 | |
| 						a.Key == common.ProjectResourceActionKey_CopyToLibrary {
 | |
| 						a.Enable = false
 | |
| 						a.Hint = ptr.Of(e.Msg())
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return info, nil
 | |
| }
 | |
| 
 | |
| func (s *ServiceComponents) sortAPPResources(resourceGroups []*common.ProjectResourceGroup) []*common.ProjectResourceGroup {
 | |
| 	for _, g := range resourceGroups {
 | |
| 		slices.SortFunc(g.ResourceList, func(a, b *common.ProjectResourceInfo) int {
 | |
| 			if a.Name == b.Name {
 | |
| 				return 0
 | |
| 			}
 | |
| 			if a.Name < b.Name {
 | |
| 				return -1
 | |
| 			}
 | |
| 			return 1
 | |
| 		})
 | |
| 	}
 | |
| 	return resourceGroups
 | |
| }
 |