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(),
|
|
}
|
|
|
|
// 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
|
|
}
|