/* * 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 app import ( "context" "errors" "fmt" "strconv" "sync" "time" "github.com/google/uuid" intelligenceAPI "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence" "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/common" taskStruct "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/common" projectAPI "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/project" publishAPI "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/publish" taskAPI "github.com/coze-dev/coze-studio/backend/api/model/app/intelligence/task" connectorModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/connector" knowledgeModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/knowledge" pluginModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin" "github.com/coze-dev/coze-studio/backend/api/model/data/database/table" "github.com/coze-dev/coze-studio/backend/api/model/data/variable/project_memory" "github.com/coze-dev/coze-studio/backend/api/model/playground" resourceAPI "github.com/coze-dev/coze-studio/backend/api/model/resource" resourceCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common" workflowAPI "github.com/coze-dev/coze-studio/backend/api/model/workflow" "github.com/coze-dev/coze-studio/backend/application/base/ctxutil" "github.com/coze-dev/coze-studio/backend/application/knowledge" "github.com/coze-dev/coze-studio/backend/application/memory" "github.com/coze-dev/coze-studio/backend/application/plugin" "github.com/coze-dev/coze-studio/backend/application/workflow" "github.com/coze-dev/coze-studio/backend/domain/app/entity" "github.com/coze-dev/coze-studio/backend/domain/app/repository" "github.com/coze-dev/coze-studio/backend/domain/app/service" connector "github.com/coze-dev/coze-studio/backend/domain/connector/service" variables "github.com/coze-dev/coze-studio/backend/domain/memory/variables/service" searchEntity "github.com/coze-dev/coze-studio/backend/domain/search/entity" 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/infra/contract/modelmgr" "github.com/coze-dev/coze-studio/backend/infra/contract/storage" "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/logs" "github.com/coze-dev/coze-studio/backend/pkg/safego" "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 APPApplicationSVC = &APPApplicationService{} type APPApplicationService struct { DomainSVC service.AppService appRepo repository.AppRepository oss storage.Storage projectEventBus search.ProjectEventBus modelMgr modelmgr.Manager userSVC user.User connectorSVC connector.Connector variablesSVC variables.Variables } func (a *APPApplicationService) DraftProjectCreate(ctx context.Context, req *projectAPI.DraftProjectCreateRequest) (resp *projectAPI.DraftProjectCreateResponse, err error) { userID := ctxutil.GetUIDFromCtx(ctx) if userID == nil { return nil, errorx.New(errno.ErrAppPermissionCode, errorx.KV(errno.APPMsgKey, "session is required")) } respModel, err := a.modelMgr.ListInUseModel(ctx, 1, nil) if err != nil { return nil, err } if len(respModel.ModelList) == 0 { return nil, errorx.New(errno.ErrAppNoModelInUseCode) } appID, err := a.DomainSVC.CreateDraftAPP(ctx, &service.CreateDraftAPPRequest{ SpaceID: req.SpaceID, OwnerID: *userID, IconURI: req.IconURI, Name: req.Name, Desc: req.Description, }) if err != nil { return nil, errorx.Wrapf(err, "CreateDraftAPP failed, spaceID=%d", req.SpaceID) } err = a.projectEventBus.PublishProject(ctx, &searchEntity.ProjectDomainEvent{ OpType: searchEntity.Created, Project: &searchEntity.ProjectDocument{ Status: common.IntelligenceStatus_Using, Type: common.IntelligenceType_Project, ID: appID, SpaceID: &req.SpaceID, OwnerID: userID, Name: &req.Name, }, }) if err != nil { return nil, errorx.Wrapf(err, "publish project '%d' failed", appID) } resp = &projectAPI.DraftProjectCreateResponse{ Data: &projectAPI.DraftProjectCreateData{ ProjectID: appID, }, } return resp, nil } func (a *APPApplicationService) GetDraftIntelligenceInfo(ctx context.Context, req *intelligenceAPI.GetDraftIntelligenceInfoRequest) (resp *intelligenceAPI.GetDraftIntelligenceInfoResponse, err error) { draftAPP, err := a.ValidateDraftAPPAccess(ctx, req.IntelligenceID) if err != nil { return nil, errorx.Wrapf(err, "GetDraftAPP failed, id=%d", req.IntelligenceID) } basicInfo, published, err := a.getAPPBasicInfo(ctx, draftAPP) if err != nil { return nil, err } publishRecord := &intelligenceAPI.IntelligencePublishInfo{ HasPublished: published, PublishTime: strconv.FormatInt(basicInfo.PublishTime, 10), } ownerInfo := a.getAPPUserInfo(ctx, draftAPP.OwnerID) resp = &intelligenceAPI.GetDraftIntelligenceInfoResponse{ Data: &intelligenceAPI.GetDraftIntelligenceInfoData{ IntelligenceType: common.IntelligenceType_Project, BasicInfo: basicInfo, PublishInfo: publishRecord, OwnerInfo: ownerInfo, }, } return resp, nil } func (a *APPApplicationService) DraftProjectDelete(ctx context.Context, req *projectAPI.DraftProjectDeleteRequest) (resp *projectAPI.DraftProjectDeleteResponse, err error) { _, err = a.ValidateDraftAPPAccess(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "ValidateDraftAPPAccess failed, id=%d", req.ProjectID) } err = a.DomainSVC.DeleteDraftAPP(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "DeleteDraftAPP failed, id=%d", req.ProjectID) } err = a.projectEventBus.PublishProject(ctx, &searchEntity.ProjectDomainEvent{ OpType: searchEntity.Deleted, Project: &searchEntity.ProjectDocument{ ID: req.ProjectID, Type: common.IntelligenceType_Project, }, }) if err != nil { logs.CtxErrorf(ctx, "publish project '%d' failed, err=%v", req.ProjectID, err) } safego.Go(ctx, func() { // When an app is deleted, resource deletion is currently handled as a weak dependency, meaning some resources might not be deleted, but they will be inaccessible to the user. // TODO:: Application resources need to check the deletion status of the application a.deleteAPPResources(ctx, req.ProjectID) }) resp = &projectAPI.DraftProjectDeleteResponse{} return resp, nil } func (a *APPApplicationService) deleteAPPResources(ctx context.Context, appID int64) { err := plugin.PluginApplicationSVC.DeleteAPPAllPlugins(ctx, appID) if err != nil { logs.CtxErrorf(ctx, "delete app '%d' plugins failed, err=%v", appID, err) } err = memory.DatabaseApplicationSVC.DeleteDatabaseByAppID(ctx, appID) if err != nil { logs.CtxErrorf(ctx, "delete app '%d' databases failed, err=%v", appID, err) } err = a.variablesSVC.DeleteAllVariable(ctx, project_memory.VariableConnector_Project, conv.Int64ToStr(appID)) if err != nil { logs.CtxErrorf(ctx, "delete app '%d' variables failed, err=%v", appID, err) } err = knowledge.KnowledgeSVC.DeleteAppKnowledge(ctx, &knowledge.DeleteAppKnowledgeRequest{AppID: appID}) if err != nil { logs.CtxErrorf(ctx, "delete app '%d' knowledge failed, err=%v", appID, err) } err = workflow.SVC.DeleteWorkflowsByAppID(ctx, appID) if err != nil { logs.CtxErrorf(ctx, "delete app '%d' workflow failed, err=%v", appID, err) } } func (a *APPApplicationService) DraftProjectUpdate(ctx context.Context, req *projectAPI.DraftProjectUpdateRequest) (resp *projectAPI.DraftProjectUpdateResponse, err error) { _, err = a.ValidateDraftAPPAccess(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "ValidateDraftAPPAccess failed, id=%d", req.ProjectID) } err = a.DomainSVC.UpdateDraftAPP(ctx, &service.UpdateDraftAPPRequest{ APPID: req.ProjectID, Name: req.Name, Desc: req.Description, IconURI: req.IconURI, }) if err != nil { return nil, errorx.Wrapf(err, "UpdateDraftAPP failed, id=%d", req.ProjectID) } err = a.projectEventBus.PublishProject(ctx, &searchEntity.ProjectDomainEvent{ OpType: searchEntity.Updated, Project: &searchEntity.ProjectDocument{ ID: req.ProjectID, Type: common.IntelligenceType_Project, Name: req.Name, }, }) if err != nil { return nil, errorx.Wrapf(err, "publish project '%d' failed", req.ProjectID) } resp = &projectAPI.DraftProjectUpdateResponse{} return resp, nil } func (a *APPApplicationService) ProjectPublishConnectorList(ctx context.Context, req *publishAPI.PublishConnectorListRequest) (resp *publishAPI.PublishConnectorListResponse, err error) { _, err = a.ValidateDraftAPPAccess(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "ValidateDraftAPPAccess failed, id=%d", req.ProjectID) } connectorList, err := a.getAPPPublishConnectorList(ctx, req.ProjectID) if err != nil { return nil, err } latestPublishRecord, err := a.getLatestPublishRecord(ctx, req.ProjectID) if err != nil { return nil, err } resp = &publishAPI.PublishConnectorListResponse{ Data: &publishAPI.PublishConnectorListData{ ConnectorList: connectorList, LastPublishInfo: latestPublishRecord, ConnectorUnionInfoMap: map[int64]*publishAPI.ConnectorUnionInfo{}, }, } return resp, nil } func (a *APPApplicationService) getAPPPublishConnectorList(ctx context.Context, appID int64) ([]*publishAPI.PublishConnectorInfo, error) { res, err := a.DomainSVC.GetPublishConnectorList(ctx, &service.GetPublishConnectorListRequest{}) if err != nil { return nil, errorx.Wrapf(err, "GetPublishConnectorList failed, appID=%d", appID) } hasWorkflow, err := workflow.SVC.CheckWorkflowsExistByAppID(ctx, appID) if err != nil { return nil, errorx.Wrapf(err, "CheckWorkflowsExistByAppID failed, appID=%d", appID) } connectorList := make([]*publishAPI.PublishConnectorInfo, 0, len(res.Connectors)) for _, c := range res.Connectors { var info *publishAPI.PublishConnectorInfo switch c.ID { case consts.APIConnectorID: info, err = a.packAPIConnectorInfo(ctx, c, hasWorkflow) if err != nil { return nil, err } default: logs.CtxWarnf(ctx, "unsupported connector id '%v'", c.ID) continue } connectorList = append(connectorList, info) } return connectorList, nil } func (a *APPApplicationService) packAPIConnectorInfo(ctx context.Context, c *connectorModel.Connector, hasWorkflow bool) (*publishAPI.PublishConnectorInfo, error) { const noWorkflowText = "请在应用内至少添加一个工作流" info := &publishAPI.PublishConnectorInfo{ ID: c.ID, BindType: publishAPI.ConnectorBindType_ApiBind, ConnectorClassification: publishAPI.ConnectorClassification_APIOrSDK, BindInfo: map[string]string{}, Name: c.Name, IconURL: c.URL, Description: c.Desc, AllowPublish: true, } if hasWorkflow { return info, nil } info.AllowPublish = false info.NotAllowPublishReason = ptr.Of(noWorkflowText) return info, nil } func (a *APPApplicationService) getLatestPublishRecord(ctx context.Context, appID int64) (info *publishAPI.LastPublishInfo, err error) { record, exist, err := a.DomainSVC.GetAPPPublishRecord(ctx, &service.GetAPPPublishRecordRequest{ APPID: appID, Oldest: false, }) if err != nil { return nil, errorx.Wrapf(err, "GetAPPPublishRecord failed, appID=%d", appID) } if !exist { return &publishAPI.LastPublishInfo{ VersionNumber: "", ConnectorIds: []int64{}, ConnectorPublishConfig: map[int64]*publishAPI.ConnectorPublishConfig{}, }, nil } latestRecord := &publishAPI.LastPublishInfo{ VersionNumber: record.APP.GetVersion(), ConnectorIds: []int64{}, ConnectorPublishConfig: map[int64]*publishAPI.ConnectorPublishConfig{}, } for _, r := range record.ConnectorPublishRecords { latestRecord.ConnectorIds = append(latestRecord.ConnectorIds, r.ConnectorID) } return latestRecord, nil } func (a *APPApplicationService) ReportUserBehavior(ctx context.Context, req *playground.ReportUserBehaviorRequest) (resp *playground.ReportUserBehaviorResponse, err error) { err = a.projectEventBus.PublishProject(ctx, &searchEntity.ProjectDomainEvent{ OpType: searchEntity.Updated, Project: &searchEntity.ProjectDocument{ ID: req.ResourceID, SpaceID: req.SpaceID, Type: common.IntelligenceType_Project, IsRecentlyOpen: ptr.Of(1), RecentlyOpenMS: ptr.Of(time.Now().UnixMilli()), }, }) if err != nil { logs.CtxWarnf(ctx, "publish project '%d' event failed err=%s", req.ResourceID, err) } return &playground.ReportUserBehaviorResponse{}, nil } func (a *APPApplicationService) CheckProjectVersionNumber(ctx context.Context, req *publishAPI.CheckProjectVersionNumberRequest) (resp *publishAPI.CheckProjectVersionNumberResponse, err error) { _, err = a.ValidateDraftAPPAccess(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "ValidateDraftAPPAccess failed, id=%d", req.ProjectID) } exist, err := a.appRepo.CheckAPPVersionExist(ctx, req.ProjectID, req.VersionNumber) if err != nil { return nil, errorx.Wrapf(err, "CheckAPPVersionExist failed, appID=%d, version=%s", req.ProjectID, req.VersionNumber) } resp = &publishAPI.CheckProjectVersionNumberResponse{ Data: &publishAPI.CheckProjectVersionNumberData{ IsDuplicate: exist, }, } return resp, nil } func (a *APPApplicationService) PublishAPP(ctx context.Context, req *publishAPI.PublishProjectRequest) (resp *publishAPI.PublishProjectResponse, err error) { _, err = a.ValidateDraftAPPAccess(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "ValidateDraftAPPAccess failed, id=%d", req.ProjectID) } connectorIDs := make([]int64, 0, len(req.Connectors)) for connectorID := range req.Connectors { connectorIDs = append(connectorIDs, connectorID) } connectorPublishConfigs, err := a.getConnectorPublishConfigs(ctx, connectorIDs, req.ConnectorPublishConfig) if err != nil { return nil, err } res, err := a.DomainSVC.PublishAPP(ctx, &service.PublishAPPRequest{ APPID: req.ProjectID, Version: req.VersionNumber, VersionDesc: req.GetDescription(), ConnectorPublishConfigs: connectorPublishConfigs, }) if err != nil { return nil, errorx.Wrapf(err, "PublishAPP failed, id=%d", req.ProjectID) } resp = &publishAPI.PublishProjectResponse{ Data: &publishAPI.PublishProjectData{ PublishRecordID: res.PublishRecordID, }, } if !res.Success { return resp, nil } err = a.projectEventBus.PublishProject(ctx, &searchEntity.ProjectDomainEvent{ OpType: searchEntity.Updated, Project: &searchEntity.ProjectDocument{ ID: req.ProjectID, Type: common.IntelligenceType_Project, HasPublished: ptr.Of(1), PublishTimeMS: ptr.Of(time.Now().UnixMilli()), }, }) if err != nil { logs.CtxErrorf(ctx, "publish project '%d' failed, err=%v", req.ProjectID, err) } return resp, nil } func (a *APPApplicationService) getConnectorPublishConfigs(ctx context.Context, connectorIDs []int64, configs map[int64]*publishAPI.ConnectorPublishConfig) (map[int64]entity.PublishConfig, error) { publishConfigs := make(map[int64]entity.PublishConfig, len(configs)) for _, connectorID := range connectorIDs { publishConfigs[connectorID] = entity.PublishConfig{} config := configs[connectorID] if config == nil { continue } selectedWorkflows := make([]*entity.SelectedWorkflow, 0, len(config.SelectedWorkflows)) for _, w := range config.SelectedWorkflows { if w.WorkflowID == 0 { return nil, errorx.New(errno.ErrAppInvalidParamCode, errorx.KV(errno.APPMsgKey, "invalid workflow id")) } selectedWorkflows = append(selectedWorkflows, &entity.SelectedWorkflow{ WorkflowID: w.WorkflowID, WorkflowName: w.WorkflowName, }) } publishConfigs[connectorID] = entity.PublishConfig{ SelectedWorkflows: selectedWorkflows, } } return publishConfigs, nil } func (a *APPApplicationService) GetPublishRecordList(ctx context.Context, req *publishAPI.GetPublishRecordListRequest) (resp *publishAPI.GetPublishRecordListResponse, err error) { _, err = a.ValidateDraftAPPAccess(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "ValidateDraftAPPAccess failed, id=%d", req.ProjectID) } connectorInfo, err := a.connectorSVC.GetByIDs(ctx, entity.ConnectorIDWhiteList) if err != nil { return nil, errorx.Wrapf(err, "GetByIDs failed, ids=%v", entity.ConnectorIDWhiteList) } records, err := a.DomainSVC.GetAPPAllPublishRecords(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "GetAPPAllPublishRecords failed, appID=%d", req.ProjectID) } if len(records) == 0 { return &publishAPI.GetPublishRecordListResponse{ Data: []*publishAPI.PublishRecordDetail{}, }, nil } data := make([]*publishAPI.PublishRecordDetail, 0, len(records)) for _, r := range records { connectorPublishRecords := make([]*publishAPI.ConnectorPublishResult, 0, len(r.ConnectorPublishRecords)) for _, c := range r.ConnectorPublishRecords { info, exist := connectorInfo[c.ConnectorID] if !exist { logs.CtxErrorf(ctx, "connector '%d' not exist", c.ConnectorID) continue } connectorPublishRecords = append(connectorPublishRecords, &publishAPI.ConnectorPublishResult{ ConnectorID: c.ConnectorID, ConnectorName: info.Name, ConnectorIconURL: info.URL, ConnectorPublishStatus: publishAPI.ConnectorPublishStatus(c.PublishStatus), ConnectorPublishConfig: c.PublishConfig.ToVO(), }) } data = append(data, &publishAPI.PublishRecordDetail{ PublishRecordID: r.APP.GetPublishRecordID(), VersionNumber: r.APP.GetVersion(), ConnectorPublishResult: connectorPublishRecords, PublishStatus: publishAPI.PublishRecordStatus(r.APP.GetPublishStatus()), PublishStatusDetail: r.APP.PublishExtraInfo.ToVO(), }) } resp = &publishAPI.GetPublishRecordListResponse{ Data: data, } return resp, nil } func (a *APPApplicationService) GetPublishRecordDetail(ctx context.Context, req *publishAPI.GetPublishRecordDetailRequest) (resp *publishAPI.GetPublishRecordDetailResponse, err error) { _, err = a.ValidateDraftAPPAccess(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "ValidateDraftAPPAccess failed, id=%d", req.ProjectID) } connectorInfo, err := a.connectorSVC.GetByIDs(ctx, entity.ConnectorIDWhiteList) if err != nil { return nil, errorx.Wrapf(err, "GetByIDs failed, ids=%v", entity.ConnectorIDWhiteList) } record, exist, err := a.DomainSVC.GetAPPPublishRecord(ctx, &service.GetAPPPublishRecordRequest{ APPID: req.ProjectID, RecordID: req.PublishRecordID, }) if err != nil { return nil, errorx.Wrapf(err, "GetAPPPublishRecord failed, appID=%d, recordID=%d", req.ProjectID, req.PublishRecordID) } if !exist { return &publishAPI.GetPublishRecordDetailResponse{ Data: nil, }, nil } connectorPublishRecords := make([]*publishAPI.ConnectorPublishResult, 0, len(record.ConnectorPublishRecords)) for _, c := range record.ConnectorPublishRecords { info, exist := connectorInfo[c.ConnectorID] if !exist { logs.CtxErrorf(ctx, "connector '%d' not exist", c.ConnectorID) continue } connectorPublishRecords = append(connectorPublishRecords, &publishAPI.ConnectorPublishResult{ ConnectorID: c.ConnectorID, ConnectorName: info.Name, ConnectorIconURL: info.URL, ConnectorPublishStatus: publishAPI.ConnectorPublishStatus(c.PublishStatus), ConnectorPublishConfig: c.PublishConfig.ToVO(), }) } detail := &publishAPI.PublishRecordDetail{ PublishRecordID: record.APP.GetPublishRecordID(), VersionNumber: record.APP.GetVersion(), ConnectorPublishResult: connectorPublishRecords, PublishStatus: publishAPI.PublishRecordStatus(record.APP.GetPublishStatus()), PublishStatusDetail: record.APP.PublishExtraInfo.ToVO(), } resp = &publishAPI.GetPublishRecordDetailResponse{ Data: detail, } return resp, nil } func (a *APPApplicationService) ResourceCopyDispatch(ctx context.Context, req *resourceAPI.ResourceCopyDispatchRequest) (resp *resourceAPI.ResourceCopyDispatchResponse, err error) { app, err := a.ValidateDraftAPPAccess(ctx, req.GetProjectID()) if err != nil { return nil, errorx.Wrapf(err, "ValidateDraftAPPAccess failed, id=%d", req.ProjectID) } userID := ctxutil.GetUIDFromCtx(ctx) if userID == nil { return nil, errorx.New(errno.ErrAppPermissionCode, errorx.KV(errno.APPMsgKey, "session is required")) } taskID, err := a.initTask(ctx, req) if err != nil { return nil, err } var toAppID *int64 if req.Scene != resourceCommon.ResourceCopyScene_CopyResourceToLibrary { toAppID = req.ProjectID } metaInfo := ©MetaInfo{ scene: req.Scene, userID: *userID, appSpaceID: app.SpaceID, copyTaskID: taskID, fromAppID: app.ID, toAppID: toAppID, } resType, err := toResourceType(req.ResType) if err != nil { return nil, err } res := &entity.Resource{ ResID: req.ResID, ResType: resType, ResName: req.GetResName(), } var ( handleErr error newResID int64 ) switch req.ResType { case resourceCommon.ResType_Plugin: newResID, handleErr = pluginCopyDispatchHandler(ctx, metaInfo, res) case resourceCommon.ResType_Database: newResID, handleErr = databaseCopyDispatchHandler(ctx, metaInfo, res) case resourceCommon.ResType_Knowledge: newResID, handleErr = knowledgeCopyDispatchHandler(ctx, metaInfo, res) case resourceCommon.ResType_Workflow: newResID, handleErr = workflowCopyDispatchHandler(ctx, metaInfo, res) default: return nil, errorx.New(errno.ErrAppInvalidParamCode, errorx.KVf(errno.APPMsgKey, "unsupported resource type '%s'", req.ResType)) } if handleErr != nil { logs.CtxErrorf(ctx, "copy resource failed, taskID=%s, err=%v", taskID, handleErr) } failedReason, err := a.handleCopyResult(ctx, taskID, newResID, req, handleErr) if err != nil { return nil, err } resp = &resourceAPI.ResourceCopyDispatchResponse{ TaskID: ptr.Of(taskID), FailedReasons: []*resourceCommon.ResourceCopyFailedReason{}, } if failedReason != "" { resp.FailedReasons = append(resp.FailedReasons, &resourceCommon.ResourceCopyFailedReason{ ResID: req.ResID, ResType: req.ResType, ResName: req.GetResName(), Reason: "\n" + failedReason, }) } return resp, nil } func (a *APPApplicationService) initTask(ctx context.Context, req *resourceAPI.ResourceCopyDispatchRequest) (taskID string, err error) { resType, err := toResourceType(req.ResType) if err != nil { return "", err } taskID, err = a.appRepo.InitResourceCopyTask(ctx, &entity.ResourceCopyResult{ ResID: req.ResID, ResType: resType, ResName: req.GetResName(), CopyScene: req.Scene, CopyStatus: entity.ResourceCopyStatusOfProcessing, }) if err != nil { return "", errorx.Wrapf(err, "InitResourceCopyTask failed, resID=%d, resType=%s", req.ResID, req.ResType) } return taskID, nil } func (a *APPApplicationService) handleCopyResult(ctx context.Context, taskID string, newResID int64, req *resourceAPI.ResourceCopyDispatchRequest, copyErr error, ) (failedReason string, err error) { resType, err := toResourceType(req.ResType) if err != nil { return "", err } result := &entity.ResourceCopyResult{ ResID: req.ResID, ResType: resType, ResName: req.GetResName(), CopyScene: req.Scene, } if copyErr == nil { result.ResID = newResID result.CopyStatus = entity.ResourceCopyStatusOfSuccess err = a.appRepo.SaveResourceCopyTaskResult(ctx, taskID, result) if err != nil { return "", errorx.Wrapf(err, "SaveResourceCopyTaskResult failed, taskID=%s", taskID) } return "", nil } var customErr errorx.StatusError if errors.As(copyErr, &customErr) { result.FailedReason = customErr.Msg() } else { result.FailedReason = "internal server error" } result.CopyStatus = entity.ResourceCopyStatusOfFailed err = a.appRepo.SaveResourceCopyTaskResult(ctx, taskID, result) if err != nil { return "", errorx.Wrapf(err, "SaveResourceCopyTaskResult failed, taskID=%s", taskID) } return result.FailedReason, nil } func pluginCopyDispatchHandler(ctx context.Context, metaInfo *copyMetaInfo, res *entity.Resource) (newPluginID int64, err error) { switch metaInfo.scene { case resourceCommon.ResourceCopyScene_CopyProjectResource, resourceCommon.ResourceCopyScene_CopyResourceToLibrary, resourceCommon.ResourceCopyScene_CopyResourceFromLibrary: resp, err := copyPlugin(ctx, metaInfo, res) if err != nil { return 0, err } return resp.Plugin.ID, nil case resourceCommon.ResourceCopyScene_MoveResourceToLibrary: err = moveAPPPlugin(ctx, metaInfo, res) if err != nil { return 0, err } return res.ResID, nil default: return 0, fmt.Errorf("unsupported copy scene '%s'", metaInfo.scene) } } func copyPlugin(ctx context.Context, metaInfo *copyMetaInfo, res *entity.Resource) (resp *plugin.CopyPluginResponse, err error) { var copyScene pluginModel.CopyScene switch metaInfo.scene { case resourceCommon.ResourceCopyScene_CopyProjectResource: copyScene = pluginModel.CopySceneOfDuplicate case resourceCommon.ResourceCopyScene_CopyResourceToLibrary: copyScene = pluginModel.CopySceneOfToLibrary case resourceCommon.ResourceCopyScene_CopyResourceFromLibrary: copyScene = pluginModel.CopySceneOfToAPP case resourceCommon.ResourceCopyScene_CopyProject: copyScene = pluginModel.CopySceneOfAPPDuplicate default: return nil, fmt.Errorf("unsupported copy scene '%s'", metaInfo.scene) } resp, err = plugin.PluginApplicationSVC.CopyPlugin(ctx, &plugin.CopyPluginRequest{ CopyScene: copyScene, PluginID: res.ResID, UserID: metaInfo.userID, TargetAPPID: metaInfo.toAppID, }) if err != nil { return nil, errorx.Wrapf(err, "CopyPlugin failed, pluginID=%d, scene=%s", res.ResID, metaInfo.scene) } return resp, nil } func moveAPPPlugin(ctx context.Context, _ *copyMetaInfo, res *entity.Resource) (err error) { _, err = plugin.PluginApplicationSVC.MoveAPPPluginToLibrary(ctx, res.ResID) if err != nil { return errorx.Wrapf(err, "MoveAPPPluginToLibrary failed, pluginID=%d", res.ResID) } return nil } func databaseCopyDispatchHandler(ctx context.Context, metaInfo *copyMetaInfo, res *entity.Resource) (newDatabaseID int64, err error) { switch metaInfo.scene { case resourceCommon.ResourceCopyScene_CopyProjectResource, resourceCommon.ResourceCopyScene_CopyResourceToLibrary, resourceCommon.ResourceCopyScene_CopyResourceFromLibrary: return copyDatabase(ctx, metaInfo, res) case resourceCommon.ResourceCopyScene_MoveResourceToLibrary: err = moveAPPDatabase(ctx, metaInfo, res) if err != nil { return 0, err } return res.ResID, nil default: return 0, fmt.Errorf("unsupported copy scene '%s'", metaInfo.scene) } } func copyDatabase(ctx context.Context, metaInfo *copyMetaInfo, res *entity.Resource) (newDatabaseID int64, err error) { var suffix *string if metaInfo.scene == resourceCommon.ResourceCopyScene_CopyProject || metaInfo.scene == resourceCommon.ResourceCopyScene_CopyResourceFromLibrary { suffix = ptr.Of("") } resp, err := memory.DatabaseApplicationSVC.CopyDatabase(ctx, &memory.CopyDatabaseRequest{ DatabaseIDs: []int64{res.ResID}, TableType: table.TableType_OnlineTable, CreatorID: metaInfo.userID, IsCopyData: true, TargetAppID: ptr.FromOrDefault(metaInfo.toAppID, 0), Suffix: suffix, }) if err != nil { return 0, errorx.Wrapf(err, "CopyDatabase failed, databaseID=%d, scene=%s", res.ResID, metaInfo.scene) } if _, ok := resp.Databases[res.ResID]; !ok { return 0, fmt.Errorf("copy database failed, databaseID=%d", res.ResID) } return resp.Databases[res.ResID].ID, nil } func moveAPPDatabase(ctx context.Context, _ *copyMetaInfo, res *entity.Resource) (err error) { _, err = memory.DatabaseApplicationSVC.MoveDatabaseToLibrary(ctx, &memory.MoveDatabaseToLibraryRequest{ DatabaseIDs: []int64{res.ResID}, TableType: table.TableType_OnlineTable, }) if err != nil { return errorx.Wrapf(err, "MoveDatabaseToLibrary failed, databaseID=%d", res.ResID) } return nil } func knowledgeCopyDispatchHandler(ctx context.Context, metaInfo *copyMetaInfo, res *entity.Resource) (newKnowledgeID int64, err error) { switch metaInfo.scene { case resourceCommon.ResourceCopyScene_CopyProjectResource, resourceCommon.ResourceCopyScene_CopyResourceToLibrary, resourceCommon.ResourceCopyScene_CopyResourceFromLibrary: return copyKnowledge(ctx, metaInfo, res) case resourceCommon.ResourceCopyScene_MoveResourceToLibrary: err = moveAPPKnowledge(ctx, metaInfo, res) if err != nil { return 0, err } return res.ResID, nil default: return 0, fmt.Errorf("unsupported copy scene '%s'", metaInfo.scene) } } func copyKnowledge(ctx context.Context, metaInfo *copyMetaInfo, res *entity.Resource) (newKnowledgeID int64, err error) { copyReq := &knowledgeModel.CopyKnowledgeRequest{ KnowledgeID: res.ResID, TargetUserID: metaInfo.userID, TaskUniqKey: metaInfo.copyTaskID, } switch metaInfo.scene { case resourceCommon.ResourceCopyScene_CopyProjectResource: copyReq.TargetAppID = *metaInfo.toAppID copyReq.TargetSpaceID = metaInfo.appSpaceID case resourceCommon.ResourceCopyScene_CopyResourceToLibrary: copyReq.TargetSpaceID = metaInfo.appSpaceID case resourceCommon.ResourceCopyScene_CopyResourceFromLibrary: copyReq.TargetAppID = *metaInfo.toAppID copyReq.TargetSpaceID = metaInfo.appSpaceID case resourceCommon.ResourceCopyScene_CopyProject: copyReq.TargetAppID = *metaInfo.toAppID copyReq.TargetSpaceID = metaInfo.appSpaceID default: return 0, fmt.Errorf("unsupported copy scene '%s'", metaInfo.scene) } resp, err := knowledge.KnowledgeSVC.CopyKnowledge(ctx, copyReq) if err != nil { return 0, errorx.Wrapf(err, "CopyKnowledge failed, knowledgeID=%d, scene=%s", res.ResID, metaInfo.scene) } if resp.CopyStatus != knowledgeModel.CopyStatus_Successful { return 0, fmt.Errorf("copy knowledge failed, knowledgeID=%d, scene=%s", res.ResID, metaInfo.scene) } return resp.TargetKnowledgeID, nil } func moveAPPKnowledge(ctx context.Context, _ *copyMetaInfo, res *entity.Resource) (err error) { err = knowledge.KnowledgeSVC.MoveKnowledgeToLibrary(ctx, &knowledgeModel.MoveKnowledgeToLibraryRequest{ KnowledgeID: res.ResID, }) if err != nil { return errorx.Wrapf(err, "MoveKnowledgeToLibrary failed, knowledgeID=%d", res.ResID) } return nil } func workflowCopyDispatchHandler(ctx context.Context, metaInfo *copyMetaInfo, res *entity.Resource) (newWorkflowID int64, err error) { switch metaInfo.scene { case resourceCommon.ResourceCopyScene_CopyProjectResource, resourceCommon.ResourceCopyScene_CopyResourceToLibrary, resourceCommon.ResourceCopyScene_CopyResourceFromLibrary: return copyWorkflow(ctx, metaInfo, res) case resourceCommon.ResourceCopyScene_MoveResourceToLibrary: newWfId, err := moveAPPWorkflow(ctx, metaInfo, res) if err != nil { return 0, err } return newWfId, nil default: return 0, fmt.Errorf("unsupported copy scene '%s'", metaInfo.scene) } } func copyWorkflow(ctx context.Context, metaInfo *copyMetaInfo, res *entity.Resource) (newWorkflowID int64, err error) { switch metaInfo.scene { case resourceCommon.ResourceCopyScene_CopyProjectResource: resp, err := workflow.SVC.CopyWorkflow(ctx, &workflowAPI.CopyWorkflowRequest{ WorkflowID: strconv.FormatInt(res.ResID, 10), SpaceID: strconv.FormatInt(metaInfo.appSpaceID, 10), }) if err != nil { return 0, errorx.Wrapf(err, "CopyWorkflow failed, workflowID=%d", res.ResID) } newWorkflowID, _ = strconv.ParseInt(resp.Data.WorkflowID, 10, 64) return newWorkflowID, nil case resourceCommon.ResourceCopyScene_CopyResourceToLibrary: newWorkflowID, issues, err := workflow.SVC.CopyWorkflowFromAppToLibrary(ctx, res.ResID, metaInfo.appSpaceID, metaInfo.fromAppID) if err != nil { return 0, errorx.Wrapf(err, "CopyWorkflowFromAppToLibrary failed, workflowID=%d", res.ResID) } if len(issues) > 0 { return 0, errorx.New(errno.ErrAppInvalidParamCode, errorx.KVf(errno.APPMsgKey, "workflow validates failed")) } return newWorkflowID, nil case resourceCommon.ResourceCopyScene_CopyResourceFromLibrary: newWorkflowID, err = workflow.SVC.CopyWorkflowFromLibraryToApp(ctx, res.ResID, *metaInfo.toAppID) if err != nil { return 0, errorx.Wrapf(err, "CopyWorkflowFromLibraryToApp failed, workflowID=%d", res.ResID) } return newWorkflowID, nil default: return 0, fmt.Errorf("unsupported copy scene '%s'", metaInfo.scene) } } func moveAPPWorkflow(ctx context.Context, metaInfo *copyMetaInfo, res *entity.Resource) (wid int64, err error) { newWfId, issues, err := workflow.SVC.MoveWorkflowFromAppToLibrary(ctx, res.ResID, metaInfo.appSpaceID, metaInfo.fromAppID) if err != nil { return 0, errorx.Wrapf(err, "MoveWorkflowFromAppToLibrary failed, workflowID=%d", res.ResID) } if len(issues) > 0 { return 0, errorx.New(errno.ErrAppInvalidParamCode, errorx.KVf(errno.APPMsgKey, "workflow validate failed")) } return newWfId, nil } func (a *APPApplicationService) ResourceCopyDetail(ctx context.Context, req *resourceAPI.ResourceCopyDetailRequest) (resp *resourceAPI.ResourceCopyDetailResponse, err error) { result, exist, err := a.appRepo.GetResourceCopyTaskResult(ctx, req.TaskID) if err != nil { return nil, errorx.Wrapf(err, "GetResourceCopyTaskResult failed, taskID=%s", req.TaskID) } detail := &resourceCommon.ResourceCopyTaskDetail{ TaskID: req.TaskID, Status: resourceCommon.TaskStatus_Processing, Scene: result.CopyScene, } resp = &resourceAPI.ResourceCopyDetailResponse{ TaskDetail: detail, } if !exist { return resp, nil // Default return processing } detail.Status = resourceCommon.TaskStatus(result.CopyStatus) detail.ResID = result.ResID detail.ResType, err = toThriftResourceType(result.ResType) if err != nil { return nil, err } return resp, nil } func (a *APPApplicationService) DraftProjectInnerTaskList(ctx context.Context, req *taskAPI.DraftProjectInnerTaskListRequest) (resp *taskAPI.DraftProjectInnerTaskListResponse, err error) { resp = &taskAPI.DraftProjectInnerTaskListResponse{ Data: &taskAPI.DraftProjectInnerTaskListData{ TaskList: []*taskStruct.ProjectInnerTaskInfo{}, }, } return resp, nil } func (a *APPApplicationService) DraftProjectCopy(ctx context.Context, req *projectAPI.DraftProjectCopyRequest) (resp *projectAPI.DraftProjectCopyResponse, err error) { draftAPP, err := a.ValidateDraftAPPAccess(ctx, req.ProjectID) if err != nil { return nil, errorx.Wrapf(err, "validateDraftProjectCopyRequest failed") } userID := ctxutil.GetUIDFromCtx(ctx) if userID == nil { return nil, errorx.New(errno.ErrAppPermissionCode, errorx.KV(errno.APPMsgKey, "session is required")) } newAPPID, err := a.duplicateDraftAPP(ctx, *userID, req) if err != nil { return nil, err } err = a.projectEventBus.PublishProject(ctx, &searchEntity.ProjectDomainEvent{ OpType: searchEntity.Created, Project: &searchEntity.ProjectDocument{ Status: common.IntelligenceStatus_Using, Type: common.IntelligenceType_Project, ID: newAPPID, SpaceID: &req.ToSpaceID, OwnerID: userID, Name: &req.Name, }, }) if err != nil { return nil, err } draftAPP.ID = newAPPID draftAPP.Name = &req.Name draftAPP.Desc = &req.Description draftAPP.IconURI = &req.IconURI userInfo := a.getAPPUserInfo(ctx, *userID) basicInfo, _, err := a.getAPPBasicInfo(ctx, draftAPP) if err != nil { return nil, err } resp = &projectAPI.DraftProjectCopyResponse{ Data: &projectAPI.DraftProjectCopyResponseData{ BasicInfo: basicInfo, UserInfo: userInfo, }, } return resp, nil } func (a *APPApplicationService) duplicateDraftAPP(ctx context.Context, userID int64, req *projectAPI.DraftProjectCopyRequest) (newAppID int64, err error) { newAppID, err = a.DomainSVC.CreateDraftAPP(ctx, &service.CreateDraftAPPRequest{ SpaceID: req.ToSpaceID, OwnerID: userID, Name: req.Name, Desc: req.Description, IconURI: req.IconURI, }) if err != nil { return 0, errorx.Wrapf(err, "CreateDraftAPP failed, spaceID=%d", req.ToSpaceID) } err = a.duplicateDraftAPPResources(ctx, userID, newAppID, req) if err != nil { return 0, err } return newAppID, nil } func (a *APPApplicationService) duplicateDraftAPPResources(ctx context.Context, userID, newAppID int64, req *projectAPI.DraftProjectCopyRequest) (err error) { err = a.duplicateAPPVariables(ctx, userID, req.ProjectID, newAppID) if err != nil { return err } resources, err := a.DomainSVC.GetDraftAPPResources(ctx, req.GetProjectID()) if err != nil { return errorx.Wrapf(err, "GetDraftAPPResources failed, appID=%d", req.GetProjectID()) } metaInfo := ©MetaInfo{ scene: resourceCommon.ResourceCopyScene_CopyProject, userID: userID, appSpaceID: req.ToSpaceID, copyTaskID: uuid.New().String(), fromAppID: req.ProjectID, toAppID: &newAppID, } copyPluginIDMap := make(map[int64]int64) copyToolIDMap := make(map[int64]int64) copyDatabaseIDMap := make(map[int64]int64) copyKnowledgeIDMap := make(map[int64]int64) taskGroup := taskgroup.NewTaskGroup(ctx, 5) mu := sync.Mutex{} for _, res := range resources { if res.ResType == entity.ResourceTypeOfPlugin { taskGroup.Go(func() error { resp, err := copyPlugin(ctx, metaInfo, res) if err != nil { return err } mu.Lock() defer mu.Unlock() copyPluginIDMap[res.ResID] = resp.Plugin.ID for oldToolID, tool := range resp.Tools { copyToolIDMap[oldToolID] = tool.ID } return nil }) } if res.ResType == entity.ResourceTypeOfDatabase { taskGroup.Go(func() error { newDatabaseID, err := copyDatabase(ctx, metaInfo, res) if err != nil { return err } mu.Lock() defer mu.Unlock() copyDatabaseIDMap[res.ResID] = newDatabaseID return nil }) } if res.ResType == entity.ResourceTypeOfKnowledge { taskGroup.Go(func() error { newKnowledgeID, err := copyKnowledge(ctx, metaInfo, res) if err != nil { return err } mu.Lock() defer mu.Unlock() copyKnowledgeIDMap[res.ResID] = newKnowledgeID return nil }) } } err = taskGroup.Wait() if err != nil { return err } err = workflow.SVC.DuplicateWorkflowsByAppID(ctx, req.GetProjectID(), newAppID, workflow.ExternalResource{ PluginMap: copyPluginIDMap, PluginToolMap: copyToolIDMap, DatabaseMap: copyDatabaseIDMap, KnowledgeMap: copyKnowledgeIDMap, }) if err != nil { return errorx.Wrapf(err, "DuplicateWorkflowsByAppID failed, appID=%d", req.GetProjectID()) } return nil } func (a *APPApplicationService) duplicateAPPVariables(ctx context.Context, userID, fromAPPID, toAPPID int64) (err error) { vars, err := a.variablesSVC.GetProjectVariablesMeta(ctx, strconv.FormatInt(fromAPPID, 10), "") if err != nil { return err } if vars == nil { return nil } vars.ID = 0 vars.BizID = conv.Int64ToStr(toAPPID) vars.BizType = project_memory.VariableConnector_Project vars.Version = "" vars.CreatorID = userID _, err = a.variablesSVC.UpsertMeta(ctx, vars) if err != nil { return err } return nil } func (a *APPApplicationService) getAPPUserInfo(ctx context.Context, userID int64) (userInfo *common.User) { ui, err := a.userSVC.GetUserInfo(ctx, userID) if err != nil { logs.CtxErrorf(ctx, "GetUserInfo failed, userID=%d, err=%v", userID, err) return nil } userInfo = &common.User{ UserID: ui.UserID, Nickname: ui.Name, UserUniqueName: ui.UniqueName, AvatarURL: ui.IconURL, } return userInfo } func (a *APPApplicationService) getAPPBasicInfo(ctx context.Context, draftAPP *entity.APP) (info *common.IntelligenceBasicInfo, published bool, err error) { record, exist, err := a.DomainSVC.GetAPPPublishRecord(ctx, &service.GetAPPPublishRecordRequest{ APPID: draftAPP.ID, Oldest: false, }) if err != nil { return nil, false, err } var publishAt int64 if exist { publishAt = record.APP.GetPublishedAtMS() / 1000 published = record.APP.Published() } iconURL, err := a.oss.GetObjectUrl(ctx, draftAPP.GetIconURI()) if err != nil { logs.CtxWarnf(ctx, "get icon url failed with '%s', err=%v", draftAPP.GetIconURI(), err) } basicInfo := &common.IntelligenceBasicInfo{ ID: draftAPP.ID, SpaceID: draftAPP.SpaceID, OwnerID: draftAPP.OwnerID, Name: draftAPP.GetName(), Description: draftAPP.GetDesc(), IconURI: draftAPP.GetIconURI(), IconURL: iconURL, CreateTime: draftAPP.CreatedAtMS / 1000, UpdateTime: draftAPP.UpdatedAtMS / 1000, PublishTime: publishAt, Status: common.IntelligenceStatus_Using, } return basicInfo, published, nil } func (a *APPApplicationService) ValidateDraftAPPAccess(ctx context.Context, appID int64) (app *entity.APP, err error) { uid := ctxutil.GetUIDFromCtx(ctx) if uid == nil { return nil, errorx.New(errno.ErrAppPermissionCode, errorx.KV(errno.APPMsgKey, "session is required")) } app, err = a.DomainSVC.GetDraftAPP(ctx, appID) if err != nil { return nil, errorx.Wrapf(err, "GetDraftAPP failed, appID=%d", appID) } if app.OwnerID != *uid { return nil, errorx.New(errno.ErrAppPermissionCode, errorx.KV(errno.APPMsgKey, "you are not the application owner")) } return app, nil }