coze-studio/backend/application/search/resource_search.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(),
}
// Set up user filtering
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 = di.status
if di.status != nil && *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
}