feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View 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)
}

View 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,
}
}

View 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)
}

View 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,
},
}
}

View 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
}