feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
100
backend/application/search/init.go
Normal file
100
backend/application/search/init.go
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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"
|
||||
"os"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/application/singleagent"
|
||||
app "github.com/coze-dev/coze-studio/backend/domain/app/service"
|
||||
connector "github.com/coze-dev/coze-studio/backend/domain/connector/service"
|
||||
knowledge "github.com/coze-dev/coze-studio/backend/domain/knowledge/service"
|
||||
database "github.com/coze-dev/coze-studio/backend/domain/memory/database/service"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/plugin/service"
|
||||
prompt "github.com/coze-dev/coze-studio/backend/domain/prompt/service"
|
||||
search "github.com/coze-dev/coze-studio/backend/domain/search/service"
|
||||
user "github.com/coze-dev/coze-studio/backend/domain/user/service"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow"
|
||||
"github.com/coze-dev/coze-studio/backend/infra/contract/es"
|
||||
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
|
||||
"github.com/coze-dev/coze-studio/backend/infra/impl/cache/redis"
|
||||
"github.com/coze-dev/coze-studio/backend/infra/impl/eventbus"
|
||||
"github.com/coze-dev/coze-studio/backend/pkg/logs"
|
||||
"github.com/coze-dev/coze-studio/backend/types/consts"
|
||||
)
|
||||
|
||||
type ServiceComponents struct {
|
||||
DB *gorm.DB
|
||||
Cache *redis.Client
|
||||
TOS storage.Storage
|
||||
ESClient es.Client
|
||||
ProjectEventBus ProjectEventBus
|
||||
ResourceEventBus ResourceEventBus
|
||||
SingleAgentDomainSVC singleagent.SingleAgent
|
||||
APPDomainSVC app.AppService
|
||||
KnowledgeDomainSVC knowledge.Knowledge
|
||||
PluginDomainSVC service.PluginService
|
||||
WorkflowDomainSVC workflow.Service
|
||||
UserDomainSVC user.User
|
||||
ConnectorDomainSVC connector.Connector
|
||||
PromptDomainSVC prompt.Prompt
|
||||
DatabaseDomainSVC database.Database
|
||||
}
|
||||
|
||||
func InitService(ctx context.Context, s *ServiceComponents) (*SearchApplicationService, error) {
|
||||
searchDomainSVC := search.NewDomainService(ctx, s.ESClient)
|
||||
|
||||
SearchSVC.DomainSVC = searchDomainSVC
|
||||
SearchSVC.ServiceComponents = s
|
||||
|
||||
// setup consumer
|
||||
searchConsumer := search.NewProjectHandler(ctx, s.ESClient)
|
||||
|
||||
logs.Infof("start search domain consumer...")
|
||||
nameServer := os.Getenv(consts.MQServer)
|
||||
|
||||
err := eventbus.RegisterConsumer(nameServer, consts.RMQTopicApp, consts.RMQConsumeGroupApp, searchConsumer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("register search consumer failed, err=%w", err)
|
||||
}
|
||||
|
||||
searchResourceConsumer := search.NewResourceHandler(ctx, s.ESClient)
|
||||
|
||||
err = eventbus.RegisterConsumer(nameServer, consts.RMQTopicResource, consts.RMQConsumeGroupResource, searchResourceConsumer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("register search consumer failed, err=%w", err)
|
||||
}
|
||||
|
||||
return SearchSVC, nil
|
||||
}
|
||||
|
||||
type (
|
||||
ResourceEventBus = search.ResourceEventBus
|
||||
ProjectEventBus = search.ProjectEventBus
|
||||
)
|
||||
|
||||
func NewResourceEventBus(p eventbus.Producer) search.ResourceEventBus {
|
||||
return search.NewResourceEventBus(p)
|
||||
}
|
||||
|
||||
func NewProjectEventBus(p eventbus.Producer) search.ProjectEventBus {
|
||||
return search.NewProjectEventBus(p)
|
||||
}
|
||||
194
backend/application/search/project_pack.go
Normal file
194
backend/application/search/project_pack.go
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* 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"
|
||||
"strconv"
|
||||
|
||||
"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/domain/app/entity"
|
||||
appService "github.com/coze-dev/coze-studio/backend/domain/app/service"
|
||||
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
|
||||
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
|
||||
"github.com/coze-dev/coze-studio/backend/pkg/logs"
|
||||
)
|
||||
|
||||
type projectInfo struct {
|
||||
iconURI string
|
||||
desc string
|
||||
}
|
||||
|
||||
type ProjectPacker interface {
|
||||
GetProjectInfo(ctx context.Context) (*projectInfo, error)
|
||||
GetPermissionInfo() *intelligence.IntelligencePermissionInfo
|
||||
GetPublishedInfo(ctx context.Context) *intelligence.IntelligencePublishInfo
|
||||
GetUserInfo(ctx context.Context, userID int64) *common.User
|
||||
}
|
||||
|
||||
func NewPackProject(uid, projectID int64, tp common.IntelligenceType, s *SearchApplicationService) (ProjectPacker, error) {
|
||||
base := projectBase{SVC: s, projectID: projectID, iType: tp, uid: uid}
|
||||
|
||||
switch tp {
|
||||
case common.IntelligenceType_Bot:
|
||||
return &agentPacker{projectBase: base}, nil
|
||||
case common.IntelligenceType_Project:
|
||||
return &appPacker{projectBase: base}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported project_type: %d , project_id : %d", tp, projectID)
|
||||
}
|
||||
|
||||
type projectBase struct {
|
||||
projectID int64 // agent_id or application_id
|
||||
uid int64
|
||||
SVC *SearchApplicationService
|
||||
iType common.IntelligenceType
|
||||
}
|
||||
|
||||
func (p *projectBase) GetPermissionInfo() *intelligence.IntelligencePermissionInfo {
|
||||
return &intelligence.IntelligencePermissionInfo{
|
||||
InCollaboration: false,
|
||||
CanDelete: true,
|
||||
CanView: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *projectBase) GetUserInfo(ctx context.Context, userID int64) *common.User {
|
||||
u, err := p.SVC.UserDomainSVC.GetUserInfo(ctx, userID)
|
||||
if err != nil {
|
||||
logs.CtxErrorf(ctx, "[projectBase-GetUserInfo] failed to get user info, user_id: %d, err: %v", userID, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &common.User{
|
||||
UserID: u.UserID,
|
||||
AvatarURL: u.IconURL,
|
||||
UserUniqueName: u.UniqueName,
|
||||
}
|
||||
}
|
||||
|
||||
type agentPacker struct {
|
||||
projectBase
|
||||
}
|
||||
|
||||
func (a *agentPacker) GetProjectInfo(ctx context.Context) (*projectInfo, error) {
|
||||
agent, err := a.SVC.SingleAgentDomainSVC.GetSingleAgentDraft(ctx, a.projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if agent == nil {
|
||||
return nil, fmt.Errorf("agent info is nil")
|
||||
}
|
||||
return &projectInfo{
|
||||
iconURI: agent.IconURI,
|
||||
desc: agent.Desc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *agentPacker) GetPublishedInfo(ctx context.Context) *intelligence.IntelligencePublishInfo {
|
||||
pubInfo, err := p.SVC.SingleAgentDomainSVC.GetPublishedInfo(ctx, p.projectID)
|
||||
if err != nil {
|
||||
logs.CtxErrorf(ctx, "[agent-GetPublishedInfo]failed to get published info, agent_id: %d, err: %v", p.projectID, err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
connectors := make([]*common.ConnectorInfo, 0, len(pubInfo.ConnectorID2PublishTime))
|
||||
for connectorID := range pubInfo.ConnectorID2PublishTime {
|
||||
c, err := p.SVC.ConnectorDomainSVC.GetByID(ctx, connectorID)
|
||||
if err != nil {
|
||||
logs.CtxErrorf(ctx, "failed to get connector by id: %d, err: %v", connectorID, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
connectors = append(connectors, &common.ConnectorInfo{
|
||||
ID: conv.Int64ToStr(c.ID),
|
||||
Name: c.Name,
|
||||
ConnectorStatus: common.ConnectorDynamicStatus(c.ConnectorStatus),
|
||||
Icon: c.URL,
|
||||
})
|
||||
}
|
||||
|
||||
return &intelligence.IntelligencePublishInfo{
|
||||
PublishTime: conv.Int64ToStr(pubInfo.LastPublishTimeMS / 1000),
|
||||
HasPublished: pubInfo.LastPublishTimeMS > 0,
|
||||
Connectors: connectors,
|
||||
}
|
||||
}
|
||||
|
||||
type appPacker struct {
|
||||
projectBase
|
||||
}
|
||||
|
||||
func (a *appPacker) GetProjectInfo(ctx context.Context) (*projectInfo, error) {
|
||||
app, err := a.SVC.APPDomainSVC.GetDraftAPP(ctx, a.projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &projectInfo{
|
||||
iconURI: app.GetIconURI(),
|
||||
desc: app.GetDesc(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *appPacker) GetPublishedInfo(ctx context.Context) *intelligence.IntelligencePublishInfo {
|
||||
record, exist, err := a.SVC.APPDomainSVC.GetAPPPublishRecord(ctx, &appService.GetAPPPublishRecordRequest{
|
||||
APPID: a.projectID,
|
||||
Oldest: true,
|
||||
})
|
||||
if err != nil {
|
||||
logs.CtxErrorf(ctx, "[app-GetPublishedInfo] failed to get published info, app_id=%d, err=%v", a.projectID, err)
|
||||
return nil
|
||||
}
|
||||
if !exist {
|
||||
return &intelligence.IntelligencePublishInfo{
|
||||
PublishTime: "",
|
||||
HasPublished: false,
|
||||
Connectors: nil,
|
||||
}
|
||||
}
|
||||
|
||||
connectorInfo := make([]*common.ConnectorInfo, 0, len(record.ConnectorPublishRecords))
|
||||
connectorIDs := slices.Transform(record.ConnectorPublishRecords, func(c *entity.ConnectorPublishRecord) int64 {
|
||||
return c.ConnectorID
|
||||
})
|
||||
|
||||
connectors, err := a.SVC.ConnectorDomainSVC.GetByIDs(ctx, connectorIDs)
|
||||
if err != nil {
|
||||
logs.CtxErrorf(ctx, "[app-GetPublishedInfo] failed to get connector info, app_id=%d, err=%v", a.projectID, err)
|
||||
} else {
|
||||
for _, c := range connectors {
|
||||
connectorInfo = append(connectorInfo, &common.ConnectorInfo{
|
||||
ID: conv.Int64ToStr(c.ID),
|
||||
Name: c.Name,
|
||||
ConnectorStatus: common.ConnectorDynamicStatus(c.ConnectorStatus),
|
||||
Icon: c.URL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &intelligence.IntelligencePublishInfo{
|
||||
PublishTime: strconv.FormatInt(record.APP.GetPublishedAtMS()/1000, 10),
|
||||
HasPublished: record.APP.Published(),
|
||||
Connectors: connectorInfo,
|
||||
}
|
||||
}
|
||||
452
backend/application/search/project_search.go
Normal file
452
backend/application/search/project_search.go
Normal file
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
* 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
|
||||
})
|
||||
|
||||
s.packIntelligenceData(ctx, data)
|
||||
}
|
||||
|
||||
_ = 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)
|
||||
}
|
||||
325
backend/application/search/resource_pack.go
Normal file
325
backend/application/search/resource_pack.go
Normal file
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/database"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/resource/common"
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/table"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/knowledge/service"
|
||||
dbservice "github.com/coze-dev/coze-studio/backend/domain/memory/database/service"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
|
||||
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
|
||||
"github.com/coze-dev/coze-studio/backend/pkg/logs"
|
||||
)
|
||||
|
||||
var defaultAction = []*common.ResourceAction{
|
||||
{
|
||||
Key: common.ActionKey_Edit,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ActionKey_Delete,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ActionKey_Copy,
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
|
||||
type ResourcePacker interface {
|
||||
GetDataInfo(ctx context.Context) (*dataInfo, error)
|
||||
GetActions(ctx context.Context) []*common.ResourceAction
|
||||
GetProjectDefaultActions(ctx context.Context) []*common.ProjectResourceAction
|
||||
}
|
||||
|
||||
func NewResourcePacker(resID int64, t common.ResType, appContext *ServiceComponents) (ResourcePacker, error) {
|
||||
base := resourceBasePacker{appContext: appContext, resID: resID}
|
||||
|
||||
switch t {
|
||||
case common.ResType_Plugin:
|
||||
return &pluginPacker{resourceBasePacker: base}, nil
|
||||
case common.ResType_Workflow:
|
||||
return &workflowPacker{resourceBasePacker: base}, nil
|
||||
case common.ResType_Knowledge:
|
||||
return &knowledgePacker{resourceBasePacker: base}, nil
|
||||
case common.ResType_Prompt:
|
||||
return &promptPacker{resourceBasePacker: base}, nil
|
||||
case common.ResType_Database:
|
||||
return &databasePacker{resourceBasePacker: base}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported resource type: %s , resID: %d", t, resID)
|
||||
}
|
||||
|
||||
type resourceBasePacker struct {
|
||||
resID int64
|
||||
appContext *ServiceComponents
|
||||
}
|
||||
|
||||
type dataInfo struct {
|
||||
iconURI *string
|
||||
iconURL string
|
||||
desc *string
|
||||
status *int32
|
||||
}
|
||||
|
||||
func (b *resourceBasePacker) GetActions(ctx context.Context) []*common.ResourceAction {
|
||||
return defaultAction
|
||||
}
|
||||
|
||||
func (b *resourceBasePacker) GetProjectDefaultActions(ctx context.Context) []*common.ProjectResourceAction {
|
||||
return []*common.ProjectResourceAction{}
|
||||
}
|
||||
|
||||
type pluginPacker struct {
|
||||
resourceBasePacker
|
||||
}
|
||||
|
||||
func (p *pluginPacker) GetDataInfo(ctx context.Context) (*dataInfo, error) {
|
||||
plugin, err := p.appContext.PluginDomainSVC.GetDraftPlugin(ctx, p.resID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iconURL, err := p.appContext.TOS.GetObjectUrl(ctx, plugin.GetIconURI())
|
||||
if err != nil {
|
||||
logs.CtxWarnf(ctx, "get icon url failed with '%s', err=%v", plugin.GetIconURI(), err)
|
||||
}
|
||||
|
||||
return &dataInfo{
|
||||
iconURI: ptr.Of(plugin.GetIconURI()),
|
||||
iconURL: iconURL,
|
||||
desc: ptr.Of(plugin.GetDesc()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *pluginPacker) GetProjectDefaultActions(ctx context.Context) []*common.ProjectResourceAction {
|
||||
return []*common.ProjectResourceAction{
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Rename,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Copy,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Delete,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_CopyToLibrary,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_MoveToLibrary,
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type workflowPacker struct {
|
||||
resourceBasePacker
|
||||
}
|
||||
|
||||
func (w *workflowPacker) GetDataInfo(ctx context.Context) (*dataInfo, error) {
|
||||
info, err := w.appContext.WorkflowDomainSVC.Get(ctx, &vo.GetPolicy{
|
||||
ID: w.resID,
|
||||
MetaOnly: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dataInfo{
|
||||
iconURI: &info.IconURI,
|
||||
iconURL: info.IconURL,
|
||||
desc: &info.Desc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *workflowPacker) GetProjectDefaultActions(ctx context.Context) []*common.ProjectResourceAction {
|
||||
return []*common.ProjectResourceAction{
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Rename,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Copy,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_CopyToLibrary,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_MoveToLibrary,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Delete,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_UpdateDesc,
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type knowledgePacker struct {
|
||||
resourceBasePacker
|
||||
}
|
||||
|
||||
func (k *knowledgePacker) GetDataInfo(ctx context.Context) (*dataInfo, error) {
|
||||
res, err := k.appContext.KnowledgeDomainSVC.GetKnowledgeByID(ctx, &service.GetKnowledgeByIDRequest{
|
||||
KnowledgeID: k.resID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kn := res.Knowledge
|
||||
|
||||
return &dataInfo{
|
||||
iconURI: ptr.Of(kn.IconURI),
|
||||
iconURL: kn.IconURL,
|
||||
desc: ptr.Of(kn.Description),
|
||||
status: ptr.Of(int32(kn.Status)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *knowledgePacker) GetActions(ctx context.Context) []*common.ResourceAction {
|
||||
return []*common.ResourceAction{
|
||||
{
|
||||
Key: common.ActionKey_Delete,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ActionKey_EnableSwitch,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ActionKey_Edit,
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
func (k *knowledgePacker) GetProjectDefaultActions(ctx context.Context) []*common.ProjectResourceAction {
|
||||
return []*common.ProjectResourceAction{
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Rename,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Copy,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_CopyToLibrary,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_MoveToLibrary,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Delete,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Disable,
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type promptPacker struct {
|
||||
resourceBasePacker
|
||||
}
|
||||
|
||||
func (p *promptPacker) GetDataInfo(ctx context.Context) (*dataInfo, error) {
|
||||
pInfo, err := p.appContext.PromptDomainSVC.GetPromptResource(ctx, p.resID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dataInfo{
|
||||
iconURI: nil, // prompt don't have custom icon
|
||||
iconURL: "",
|
||||
desc: &pInfo.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type databasePacker struct {
|
||||
resourceBasePacker
|
||||
}
|
||||
|
||||
func (d *databasePacker) GetDataInfo(ctx context.Context) (*dataInfo, error) {
|
||||
listResp, err := d.appContext.DatabaseDomainSVC.MGetDatabase(ctx, &dbservice.MGetDatabaseRequest{Basics: []*database.DatabaseBasic{
|
||||
{
|
||||
ID: d.resID,
|
||||
TableType: table.TableType_OnlineTable,
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(listResp.Databases) == 0 {
|
||||
return nil, fmt.Errorf("online database not found, id: %d", d.resID)
|
||||
}
|
||||
|
||||
return &dataInfo{
|
||||
iconURI: ptr.Of(listResp.Databases[0].IconURI),
|
||||
iconURL: listResp.Databases[0].IconURL,
|
||||
desc: ptr.Of(listResp.Databases[0].TableDesc),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *databasePacker) GetActions(ctx context.Context) []*common.ResourceAction {
|
||||
return []*common.ResourceAction{
|
||||
{
|
||||
Key: common.ActionKey_Delete,
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *databasePacker) GetProjectDefaultActions(ctx context.Context) []*common.ProjectResourceAction {
|
||||
return []*common.ProjectResourceAction{
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Copy,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_CopyToLibrary,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_MoveToLibrary,
|
||||
Enable: true,
|
||||
},
|
||||
{
|
||||
Key: common.ProjectResourceActionKey_Delete,
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
392
backend/application/search/resource_search.go
Normal file
392
backend/application/search/resource_search.go
Normal file
@@ -0,0 +1,392 @@
|
||||
/*
|
||||
* 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))
|
||||
for idx := range searchResp.Data {
|
||||
v := searchResp.Data[idx]
|
||||
index := idx
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
_ = tasks.Wait()
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user