From e612f5e434c767e1358b617d5b7be6348ac90c92 Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Mon, 13 Jul 2026 09:13:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=99=BA=E8=83=BD=E4=BD=93?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E5=90=8E=E7=AB=AF=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/AgentApplicationService.java | 16 +- .../AgentWorkspaceApplicationService.java | 49 +- .../ComponentApplicationService.java | 3 +- .../ConversationApplicationService.java | 15 +- .../IComputerFileApplicationService.java | 22 +- .../application/ModelApplicationService.java | 3 + .../ProjectApplicationService.java | 9 + .../PublishApplicationService.java | 10 + .../RecommendApplicationService.java | 27 + .../ResourceGroupApplicationService.java | 34 + .../application/SkillApplicationService.java | 18 + ...ableWorkflowForPageApplicationService.java | 12 + .../WorkflowApplicationService.java | 2 + .../agent/core/adapter/dto/AgentAddDto.java | 3 + .../adapter/dto/AgentConfigCacheStatusVo.java | 21 + .../adapter/dto/AgentConfigUpdateDto.java | 9 + .../core/adapter/dto/AgentDetailDto.java | 12 + .../core/adapter/dto/AgentOutputDto.java | 1 + .../adapter/dto/AgentPublishVersionDto.java | 34 + .../core/adapter/dto/ChatMessageDto.java | 86 +- .../agent/core/adapter/dto/ComponentDto.java | 17 +- .../core/adapter/dto/ConversationDto.java | 10 + .../adapter/dto/ConversationUpdateDto.java | 3 + .../core/adapter/dto/CreateWorkspaceDto.java | 14 + .../agent/core/adapter}/dto/EnNameDto.java | 2 +- .../core/adapter/dto/GenerateInfoAiDto.java | 25 + .../core/adapter/dto/GenerateInfoReqDto.java | 22 + .../adapter/dto/GenerateInfoResultDto.java | 26 + .../agent/core/adapter/dto/HookEntryDto.java | 25 + .../agent/core/adapter/dto/HookItemDto.java | 32 + .../agent/core/adapter/dto/HookScriptDto.java | 24 + .../adapter/dto/InitProjectTemplateDto.java | 89 ++ .../core/adapter/dto/InstallProjectDto.java | 29 + .../core/adapter/dto/McpServerConfigDto.java | 34 + .../core/adapter/dto/PackageAgentDto.java | 30 + .../core/adapter/dto/PageAppNewSqlDTO.java | 38 + .../core/adapter/dto/PageAppUpdateSqlDTO.java | 29 + .../core/adapter/dto/PageSqlResultDTO.java | 14 + .../adapter/dto/PermissionsConfigDto.java | 27 + .../adapter/dto/PluginExecuteRequestDto.java | 2 + .../core/adapter/dto/PluginUpdateDto.java | 3 + .../core/adapter/dto/ProjectCreateDTO.java | 24 + .../adapter/dto/ProjectCreateResultDTO.java | 18 + .../agent/core/adapter/dto/PublishedDto.java | 16 +- .../core/adapter/dto/PublishedQueryDto.java | 15 + .../core/adapter/dto/ResourceGroupDto.java | 46 + .../adapter/dto/ResourceGroupRelationDto.java | 6 + .../core/adapter/dto/SkillConfigDto.java | 3 + .../adapter/dto/SkillPublishedConfigDto.java | 2 + .../agent/core/adapter/dto/SortUpdateDTO.java | 10 + .../agent/core/adapter/dto/TopicGenDto.java | 2 + .../agent/core/adapter/dto/TryReqDto.java | 3 + .../dto/WorkflowExecuteRequestDto.java | 2 + .../adapter/dto/config/AgentConfigDto.java | 47 +- .../agent/core/adapter/dto/config/Arg.java | 10 + .../adapter/dto/config/HookConfigDto.java | 30 + .../dto/config/bind/McpBindConfigDto.java | 3 + .../dto/config/bind/PluginBindConfigDto.java | 3 + .../config/bind/WorkflowBindConfigDto.java | 3 + .../adapter/dto/config/plugin/PluginDto.java | 5 + .../config/workflow/AgentNodeConfigDto.java | 33 + .../IntentRecognitionNodeConfigDto.java | 15 +- .../KnowledgeInsertNodeConfigDto.java | 95 +++ .../dto/config/workflow/NodeConfigDto.java | 7 + .../dto/config/workflow/QaNodeConfigDto.java | 7 +- .../config/workflow/WorkflowConfigDto.java | 17 + .../dto/recommend/RecommendHomeResponse.java | 20 + .../recommend/TargetRecommendPageRequest.java | 15 + .../recommend/TargetRecommendResponse.java | 28 + .../recommend/TargetRecommendSaveRequest.java | 30 + .../ResourceGroupRelationRepository.java | 7 + .../repository/ResourceGroupRepository.java | 7 + .../repository/TargetRecommendRepository.java | 7 + .../entity/AgentComponentConfig.java | 2 +- .../repository/entity/AgentConfig.java | 18 +- .../repository/entity/Conversation.java | 9 +- .../repository/entity/ModelConfig.java | 2 + .../repository/entity/PluginConfig.java | 2 + .../adapter/repository/entity/Published.java | 6 +- .../repository/entity/ResourceGroup.java | 44 + .../entity/ResourceGroupRelation.java | 36 + .../repository/entity/SkillConfig.java | 2 + .../repository/entity/TargetRecommend.java | 72 ++ .../repository/entity/WorkflowConfig.java | 4 + .../repository/entity/WorkflowNodeConfig.java | 2 + .../AgentPublishVersionListTypeHandler.java | 15 + .../application/api/AgentApiServiceImpl.java | 78 +- .../application/api/SkillRpcServiceImpl.java | 25 + .../service/AgentApplicationServiceImpl.java | 393 ++++++++- .../AgentWorkspaceApplicationServiceImpl.java | 715 +++++++++++++++- .../ComponentApplicationServiceImpl.java | 188 +++-- .../ComputerFileApplicationServiceImpl.java | 81 +- .../ConversationApplicationServiceImpl.java | 796 +++++++++++++----- .../service/ModelApplicationServiceImpl.java | 92 +- .../service/PluginApplicationServiceImpl.java | 2 + .../ProjectApplicationServiceImpl.java | 178 ++++ .../PublishApplicationServiceImpl.java | 532 +++++++++++- .../RecommendApplicationServiceImpl.java | 194 +++++ .../ResourceGroupApplicationServiceImpl.java | 231 +++++ .../service/SkillApplicationServiceImpl.java | 118 ++- ...WorkflowForPageApplicationServiceImpl.java | 172 ++++ .../service/TenantCreatedTaskServiceImpl.java | 48 +- .../TenantVersionUpdateServiceImpl.java | 16 + .../WorkflowApplicationServiceImpl.java | 156 +++- .../support/PlatformEnvHookSupport.java | 153 ++++ .../templates/deepagents-flow-ts.zip | Bin 0 -> 548442 bytes .../main/resources/templates/package-ts.md | 0 .../resources/templates}/skill-template.json | 0 .../domain/service/AgentDomainService.java | 2 + .../service/ConversationDomainService.java | 2 + .../service/IComputerFileDomainService.java | 24 +- .../domain/service/PublishDomainService.java | 2 + .../service/ResourceGroupDomainService.java | 19 + .../ResourceGroupRelationDomainService.java | 25 + .../service/impl/AgentDomainServiceImpl.java | 5 + .../impl/ComputerFileDomainServiceImpl.java | 40 +- .../impl/ConversationDomainServiceImpl.java | 14 +- .../impl/PublishDomainServiceImpl.java | 92 +- .../impl/ResourceGroupDomainServiceImpl.java | 43 + ...esourceGroupRelationDomainServiceImpl.java | 68 ++ .../impl/WorkflowDomainServiceImpl.java | 3 +- .../app-platform-agent-core-infra/pom.xml | 7 + .../core/infra/component/ArgExtractUtil.java | 1 + .../infra/component/agent/AgentContext.java | 70 ++ .../infra/component/agent/AgentExecutor.java | 222 +++-- .../component/agent/SandboxAgentClient.java | 446 ++++++++-- .../component/agent/dto/AgentRequest.java | 5 + .../knowledge/KnowledgeBaseSearcher.java | 18 + .../component/model/JdkClientHttpRequest.java | 2 +- .../infra/component/model/JsonExtractor.java | 86 ++ .../component/model/ModelClientFactory.java | 26 +- .../infra/component/model/ModelInvoker.java | 67 +- .../model/dto/ComponentExecutingDto.java | 6 +- .../component/plugin/PluginExecutor.java | 50 ++ .../plugin/handler/CodePluginHandler.java | 3 + .../plugin/handler/HttpPluginHandler.java | 14 +- .../component/workflow/ExecuteStatusMap.java | 24 - .../component/workflow/WorkflowContext.java | 40 + .../component/workflow/WorkflowExecutor.java | 18 + .../workflow/dto/FlowChatMessage.java | 10 + .../handler/ConditionNodeHandler.java | 8 +- .../workflow/handler/HandlerFactory.java | 1 + .../handler/IntentRecognitionNodeHandler.java | 9 +- .../handler/KnowledgeInsertNodeHandler.java | 56 ++ .../workflow/handler/StartNodeHandler.java | 3 + .../workflow/handler/WrappedNodeHandler.java | 7 +- .../infra/dao/mapper/ResourceGroupMapper.java | 9 + .../mapper/ResourceGroupRelationMapper.java | 9 + .../dao/mapper/TargetRecommendMapper.java | 9 + .../proxy/ComputerProxyRequestHandler.java | 130 ++- .../proxy/ComputerProxyServerContainer.java | 16 +- .../ResourceGroupRelationRepositoryImpl.java | 11 + .../ResourceGroupRepositoryImpl.java | 11 + .../TargetRecommendRepositoryImpl.java | 11 + .../core/infra/rpc/ComputerFileClient.java | 140 ++- .../core/infra/rpc/ComputerPodClient.java | 71 +- .../core/infra/rpc/CustomPageRpcService.java | 22 +- .../agent/core/infra/rpc/GitRpcClient.java | 714 ++++++++++++++++ .../infra/rpc/MarketClientRpcService.java | 17 +- .../infra/rpc/ResourcePricingRpcService.java | 37 + .../infra/rpc/SandboxServerConfigService.java | 7 +- .../agent/core/infra/rpc/SkillFileClient.java | 222 ----- .../core/infra/rpc/WorkspaceRpcClient.java | 456 ++++++++++ .../infra/sub/ChatMessagePubSubService.java | 151 ++++ .../core/infra/sub/RedisPubSubConfig.java | 31 + .../agent/core/infra/sub/RedisPublisher.java | 20 + .../agent/core/sdk/ISkillRpcService.java | 22 + .../core/sdk/dto/PluginExecuteRequestDto.java | 4 +- .../sdk/dto/WorkflowExecuteRequestDto.java | 4 +- .../agent/core/spec/constant/Prompts.java | 4 - .../agent/core/spec/utils/FileTypeUtils.java | 82 +- .../agent/core/spec/utils/SvgIconUtil.java | 377 +++++++++ .../agent/core/spec/utils/ZipStringUtils.java | 63 ++ .../web/ui/controller/AgentController.java | 58 +- .../controller/AgentWorkspaceController.java | 59 ++ .../ui/controller/ComponentController.java | 35 +- .../ui/controller/ComputerFileController.java | 147 +++- .../ui/controller/ComputerPodController.java | 50 +- .../ui/controller/ConversationController.java | 111 ++- .../controller/ExecuteForPageController.java | 84 +- .../web/ui/controller/GitController.java | 277 ++++++ .../web/ui/controller/HomeController.java | 17 +- .../ui/controller/ModelManageController.java | 10 +- .../web/ui/controller/ProjectController.java | 68 ++ .../web/ui/controller/PublishController.java | 376 ++------- .../controller/PublishManageController.java | 3 - .../ui/controller/PublishedController.java | 70 +- .../ui/controller/RecommendController.java | 90 ++ .../controller/ResourceGroupController.java | 136 +++ .../web/ui/controller/SkillController.java | 242 +++--- .../ui/controller/UserAgentController.java | 11 + .../web/ui/controller/WorkflowController.java | 31 +- .../api/AgentDevApiForSandboxController.java | 639 ++++++++++++++ .../ui/controller/api/ChatApiController.java | 5 +- .../api/PluginAndWorkflowApiController.java | 42 +- .../controller/api/dto/DevAgentConfigDTO.java | 15 + .../api/dto/DevAgentConfigUpdateDTO.java | 17 + .../controller/api/dto/KbQueryResultDTO.java | 22 + .../ui/controller/api/dto/McpResultDTO.java | 17 + .../api/dto/SkillResultItemDTO.java | 17 + .../web/ui/controller/api/dto/ToolAddDTO.java | 13 + .../ui/controller/api/dto/ToolExecuteDTO.java | 23 + .../ui/controller/api/dto/ToolSearchDTO.java | 23 + .../api/dto/ToolSearchResultItemDTO.java | 30 + .../api/dto/WorkflowExecutingDTO.java | 28 + .../base/ConversationPermissionChecker.java | 103 +++ .../dto/PermissionRequestResponseDto.java | 22 + .../web/ui/controller/dto/PublishItemDto.java | 3 + .../controller/dto/ResourceGroupQueryDto.java | 14 + .../ui/controller/dto/ResourceTargetDto.java | 11 + .../web/ui/controller/dto/git/GitBaseReq.java | 18 + .../ui/controller/dto/git/GitBranchReq.java | 20 + .../ui/controller/dto/git/GitCheckoutReq.java | 14 + .../ui/controller/dto/git/GitCommitReq.java | 19 + .../web/ui/controller/dto/git/GitDiffReq.java | 25 + .../controller/dto/git/GitFileContentReq.java | 17 + .../web/ui/controller/dto/git/GitLogReq.java | 23 + .../ui/controller/dto/git/GitResetReq.java | 17 + .../ui/controller/dto/git/GitRevertReq.java | 17 + .../web/ui/controller/dto/git/GitTagReq.java | 17 + .../ui/controller/dto/git/GitUnstageReq.java | 16 + .../manage/RecommendManageController.java | 257 ++++++ .../manage/dto/ManageQueryRequest.java | 6 + .../agent/web/ui/dto/ComponentQueryDto.java | 20 + .../xspaceagi/agent/web/ui/dto/SkillDto.java | 3 + 225 files changed, 11826 insertions(+), 1569 deletions(-) create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ProjectApplicationService.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/RecommendApplicationService.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ResourceGroupApplicationService.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/TableWorkflowForPageApplicationService.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentConfigCacheStatusVo.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentPublishVersionDto.java rename qiming-backend/app-platform-modules/app-platform-agent/{app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller => app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter}/dto/EnNameDto.java (90%) create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoAiDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoReqDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoResultDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookEntryDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookItemDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookScriptDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/InitProjectTemplateDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/InstallProjectDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/McpServerConfigDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PackageAgentDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageAppNewSqlDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageAppUpdateSqlDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageSqlResultDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PermissionsConfigDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ProjectCreateDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ProjectCreateResultDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ResourceGroupDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ResourceGroupRelationDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SortUpdateDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/HookConfigDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/AgentNodeConfigDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/KnowledgeInsertNodeConfigDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/RecommendHomeResponse.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendPageRequest.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendResponse.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendSaveRequest.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/ResourceGroupRelationRepository.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/ResourceGroupRepository.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/TargetRecommendRepository.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ResourceGroup.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ResourceGroupRelation.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/TargetRecommend.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/typehandler/AgentPublishVersionListTypeHandler.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/api/SkillRpcServiceImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ProjectApplicationServiceImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/RecommendApplicationServiceImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ResourceGroupApplicationServiceImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TableWorkflowForPageApplicationServiceImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/support/PlatformEnvHookSupport.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/resources/templates/deepagents-flow-ts.zip create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/resources/templates/package-ts.md rename qiming-backend/app-platform-modules/app-platform-agent/{app-platform-agent-core-ui/src/main/resources => app-platform-agent-core-application/src/main/resources/templates}/skill-template.json (100%) create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ResourceGroupDomainService.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ResourceGroupRelationDomainService.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ResourceGroupDomainServiceImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ResourceGroupRelationDomainServiceImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/JsonExtractor.java delete mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/ExecuteStatusMap.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/dto/FlowChatMessage.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/KnowledgeInsertNodeHandler.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/ResourceGroupMapper.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/ResourceGroupRelationMapper.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/TargetRecommendMapper.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/ResourceGroupRelationRepositoryImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/ResourceGroupRepositoryImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/TargetRecommendRepositoryImpl.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/GitRpcClient.java delete mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/SkillFileClient.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/WorkspaceRpcClient.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/ChatMessagePubSubService.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/RedisPubSubConfig.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/RedisPublisher.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/ISkillRpcService.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/SvgIconUtil.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/ZipStringUtils.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/AgentWorkspaceController.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/GitController.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ProjectController.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/RecommendController.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ResourceGroupController.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/AgentDevApiForSandboxController.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/DevAgentConfigDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/DevAgentConfigUpdateDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/KbQueryResultDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/McpResultDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/SkillResultItemDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolAddDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolExecuteDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolSearchDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolSearchResultItemDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/WorkflowExecutingDTO.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/base/ConversationPermissionChecker.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/PermissionRequestResponseDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/ResourceGroupQueryDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/ResourceTargetDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitBaseReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitBranchReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitCheckoutReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitCommitReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitDiffReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitFileContentReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitLogReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitResetReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitRevertReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitTagReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitUnstageReq.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/manage/RecommendManageController.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/dto/ComponentQueryDto.java diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/AgentApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/AgentApplicationService.java index 8a32f29f..25d0e265 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/AgentApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/AgentApplicationService.java @@ -1,12 +1,11 @@ package com.xspaceagi.agent.core.adapter.application; -import com.xspaceagi.agent.core.adapter.dto.AgentDetailDto; -import com.xspaceagi.agent.core.adapter.dto.CardDto; -import com.xspaceagi.agent.core.adapter.dto.UserAgentDto; +import com.xspaceagi.agent.core.adapter.dto.*; import com.xspaceagi.agent.core.adapter.dto.config.AgentComponentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.Arg; import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; +import com.xspaceagi.agent.core.adapter.repository.entity.AgentComponentConfig; import java.util.List; @@ -121,6 +120,8 @@ public interface AgentApplicationService { */ AgentComponentConfigDto queryComponentConfig(Long id); + List queryComponentConfigListByAgentIdAndType(Long agentId, AgentComponentConfig.Type type); + List getAgentNoneSystemVariables(Long agentId, Long spaceId); /** @@ -232,10 +233,17 @@ public interface AgentApplicationService { /** * 获取用户可选择的模型列表 * - * @param userId 用户ID + * @param userId 用户ID * @param agentId 智能体ID * @return 模型列表 */ List queryUserCanSelectModelListForAgent(Long userId, Long agentId); + + /** + * AI生成项目信息(名称、描述、图标) + */ + GenerateInfoResultDto generateInfo(GenerateInfoReqDto req); + + String uploadSvgIcon(String svgIcon, String fileName); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/AgentWorkspaceApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/AgentWorkspaceApplicationService.java index e78169f1..70704c2f 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/AgentWorkspaceApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/AgentWorkspaceApplicationService.java @@ -1,7 +1,8 @@ package com.xspaceagi.agent.core.adapter.application; -import com.xspaceagi.agent.core.adapter.dto.AddSkillsToWorkspaceDto; -import com.xspaceagi.agent.core.adapter.dto.CreateWorkspaceDto; +import com.xspaceagi.agent.core.adapter.dto.*; + +import java.util.List; public interface AgentWorkspaceApplicationService { @@ -19,4 +20,48 @@ public interface AgentWorkspaceApplicationService { */ void addSkillsToWorkspace(AddSkillsToWorkspaceDto addSkillsToWorkspaceDto); + /** + * 初始化项目模板 + * 根据项目类型和编程语言从 classpath 读取模板 zip,传到工作空间, + * 根据 git版本开关决定是否执行 git init + commit + */ + void initProjectTemplate(InitProjectTemplateDto dto); + + /** + * 安装项目依赖 + * TypeScript 项目执行 pnpm install,Python 项目执行 pip install + */ + void installProject(InstallProjectDto dto); + + /** + * 打包 Agent:在沙箱工作空间执行打包脚本,生成各平台压缩包,上传到平台文件存储 + */ + List packageAgent(PackageAgentDto dto); + + /** + * 从沙箱工作空间取回技能文件,上传到文件服务,用于技能发布 + */ + List snapshotSkillFilesFromSandbox(Long userId, Long cId, String targetType, Long skillId); + + /** + * 删除沙箱工作空间 + */ + void deleteWorkspace(Long userId, Long cId); + + /** + * 从沙箱导出技能为 zip 包 + */ + SkillExportResultDto exportSkillFromSandbox(Long userId, Long cId, String skillName); + + /** + * 复制源沙箱工作空间到新的工作空间,并初始化 git + * 采用先打包下载,后上传到新空间的方式,可以实现跨主机的复制 + */ + void copySandboxWorkspace(Long srcUserId, Long srcCId, Long destUserId, Long destCId); + + /** + * 从指定 url 地址下载 zip 文件并上传到工作空间,初始化项目模板 + */ + void uploadZipToWorkspace(Long userId, Long cId, String zipFileUrl); + } \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ComponentApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ComponentApplicationService.java index 5c9d6bba..fed0ce37 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ComponentApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ComponentApplicationService.java @@ -3,8 +3,9 @@ package com.xspaceagi.agent.core.adapter.application; import java.util.List; import com.xspaceagi.agent.core.adapter.dto.ComponentDto; +import com.xspaceagi.agent.core.adapter.repository.entity.Published; public interface ComponentApplicationService { - List getComponentListBySpaceId(Long spaceId); + List getComponentListBySpaceId(Long spaceId, Long groupId, List types); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ConversationApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ConversationApplicationService.java index 0e8db166..58abde11 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ConversationApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ConversationApplicationService.java @@ -5,7 +5,6 @@ import com.xspaceagi.agent.core.adapter.repository.entity.Conversation; import org.springframework.ai.chat.messages.Message; import reactor.core.publisher.Flux; -import java.util.Collection; import java.util.List; import java.util.Map; @@ -26,6 +25,8 @@ public interface ConversationApplicationService { ConversationDto createConversationForTaskCenter(Long tenantId, Long userId, Long agentId); + ConversationDto createConversationForProjectDevelopment(Long tenantId, Long userId, Long spaceId, Long devAgentId, String targetType, Long targetId); + /** * 创建任务会话,返回会话 * @@ -52,6 +53,8 @@ public interface ConversationApplicationService { void updateConversationStatus(Long cid, Conversation.ConversationTaskStatus status); + void updateConversationVariables(Long id, Map variables); + /** * 删除会话 * @@ -134,7 +137,15 @@ public interface ConversationApplicationService { Long nextConversationId(Long agentId, String sandboxServerId); - Collection getRoundMessages(String conversationId, int i); + List getRoundMessages(String conversationId, int i); + + List getRoundMessages(String conversationId, Long minId); void addRoundMessage(String conversationId, Message message); + + void setChatStopStatus(String conversationId); + + boolean isChatStop(String conversationId); + + void clearChatStopStatus(String conversationId); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/IComputerFileApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/IComputerFileApplicationService.java index d8268cf5..72f3737a 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/IComputerFileApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/IComputerFileApplicationService.java @@ -19,31 +19,41 @@ public interface IComputerFileApplicationService { /** * 查询文件列表 */ - Map getFileList(Long userId, Long cId, String proxyPath, UserContext userContext); + Map getFileList(Long userId, Long cId, String proxyPath, UserContext userContext, String customTargetDir); /** * 更新文件 */ - Map filesUpdate(Long userId, Long cId, List files, UserContext userContext); + Map filesUpdate(Long userId, Long cId, List files, UserContext userContext, String customTargetDir); /** * 上传文件 */ - Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file, UserContext userContext); + Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file, UserContext userContext, String customTargetDir); /** * 批量上传文件 */ - Map uploadFiles(Long userId, Long cId, List filePaths, List files, UserContext userContext); + Map uploadFiles(Long userId, Long cId, List filePaths, List files, UserContext userContext, String customTargetDir); + + /** + * 导入项目(zip 替换工作空间,保留白名单目录) + */ + Map importProject(Long userId, Long cId, MultipartFile file, UserContext userContext, String customTargetDir); /** * 获取静态文件 */ - ResponseEntity getStaticFile(Long cId, HttpServletRequest request); + ResponseEntity getStaticFile(Long cId, HttpServletRequest request, String customTargetDir); /** * 下载全部文件(zip) */ - ResponseEntity downloadAllFiles(Long userId, Long cId, UserContext userContext); + ResponseEntity downloadAllFiles(Long userId, Long cId, UserContext userContext, String customTargetDir); + + /** + * 获取沙盒日志 + */ + Map getLogs(Long userId, Long cId, int tailLines); } \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ModelApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ModelApplicationService.java index 40e56d42..a3d6e0fe 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ModelApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ModelApplicationService.java @@ -2,6 +2,7 @@ package com.xspaceagi.agent.core.adapter.application; import com.xspaceagi.agent.core.adapter.dto.CodeCheckResultDto; import com.xspaceagi.agent.core.adapter.dto.ModelQueryDto; +import com.xspaceagi.agent.core.adapter.dto.SortUpdateDTO; import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; import org.springframework.core.ParameterizedTypeReference; @@ -142,4 +143,6 @@ public interface ModelApplicationService { String testModelConnectivity(ModelConfigDto modelConfig, String testPrompt); List getMySystemModels(Long userId, String tab); + + void updateModelSort(List updateDTOS); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ProjectApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ProjectApplicationService.java new file mode 100644 index 00000000..bb4407fc --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ProjectApplicationService.java @@ -0,0 +1,9 @@ +package com.xspaceagi.agent.core.adapter.application; + +import com.xspaceagi.agent.core.adapter.dto.ProjectCreateDTO; +import com.xspaceagi.agent.core.adapter.dto.ProjectCreateResultDTO; + +public interface ProjectApplicationService { + + ProjectCreateResultDTO createProject(ProjectCreateDTO projectCreateDTO); +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/PublishApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/PublishApplicationService.java index bcd2ebfa..71b7047b 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/PublishApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/PublishApplicationService.java @@ -5,6 +5,8 @@ import com.xspaceagi.agent.core.adapter.dto.*; import com.xspaceagi.agent.core.adapter.repository.entity.Published; import com.xspaceagi.system.spec.dto.PageQueryVo; import com.xspaceagi.system.spec.page.SuperPage; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.RequestBody; import java.util.List; @@ -15,6 +17,8 @@ public interface PublishApplicationService { */ SuperPage queryPublishedList(PublishedQueryDto publishedQueryDto); + List queryGroupedPublishedList(PublishedQueryDto publishedQueryDto); + SuperPage queryPublishedListForAt(PublishedQueryDto publishedQueryDto); List queryPublishedList(Published.TargetType targetType, List targetIds); @@ -23,6 +27,8 @@ public interface PublishApplicationService { List queryPublishedList(Published.TargetType targetType, List targetIds, String kw); + List queryPublishedList(Published.TargetType targetType, List targetIds, String kw, boolean returnConfig); + IPage queryPublishedListForManage(PublishedQueryDto publishedQueryDto); /** @@ -39,6 +45,8 @@ public interface PublishApplicationService { */ Long publishApply(PublishApplyDto publishApplyDto); + String publishOrApply(PublishApplySubmitDto publishApplySubmitDto); + /** * 通过发布 */ @@ -108,4 +116,6 @@ public interface PublishApplicationService { void updateAccessControlStatus(Published.TargetType targetType, Long targetId, Integer status); void updatePublishName(Published.TargetType targetType, Long targetId, String name); + + Object checkPermissionAndReturnTargetConfig(Published.TargetType targetType, Long targetId); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/RecommendApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/RecommendApplicationService.java new file mode 100644 index 00000000..c7ff263f --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/RecommendApplicationService.java @@ -0,0 +1,27 @@ +package com.xspaceagi.agent.core.adapter.application; + +import com.xspaceagi.agent.core.adapter.dto.recommend.RecommendHomeResponse; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendPageRequest; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendSaveRequest; + +import java.util.List; + +public interface RecommendApplicationService { + + void save(TargetRecommendSaveRequest request); + + void update(TargetRecommendSaveRequest request); + + void delete(Long id); + + TargetRecommendResponse getById(Long id); + + List page(TargetRecommendPageRequest request); + + List list(String recType, String targetType); + + void updateSort(Long id, Integer sort); + + RecommendHomeResponse getRecommendations(); +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ResourceGroupApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ResourceGroupApplicationService.java new file mode 100644 index 00000000..e323638b --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/ResourceGroupApplicationService.java @@ -0,0 +1,34 @@ +package com.xspaceagi.agent.core.adapter.application; + +import com.xspaceagi.agent.core.adapter.dto.ResourceGroupDto; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; + +import java.util.List; +import java.util.Map; + +public interface ResourceGroupApplicationService { + + Long add(ResourceGroupDto dto); + + void update(ResourceGroupDto dto); + + void delete(Long id); + + ResourceGroupDto queryById(Long id); + + ResourceGroupDto queryByName(Long spaceId, String type, String name); + + List queryList(String name, List types, Long spaceId); + + List queryList(List groupIds); + + void addResourceToGroup(Long groupId, String targetType, Long targetId); + + void removeResourceFromGroup(Long groupId, String targetType, Long targetId); + + List queryGroupRelations(Long groupId); + + List queryGroupRelations(List> targets); + + ResourceGroupRelation queryResourceGroupRelation(String type, Long targetId); +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/SkillApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/SkillApplicationService.java index 10fe508b..b521b0bb 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/SkillApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/SkillApplicationService.java @@ -1,6 +1,7 @@ package com.xspaceagi.agent.core.adapter.application; import com.xspaceagi.agent.core.adapter.dto.*; +import com.xspaceagi.agent.core.spec.enums.UsageScenarioEnum; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; @@ -68,6 +69,18 @@ public interface SkillApplicationService { */ Long importSkill(MultipartFile file, SkillConfigDto existSkill, Long targetSpaceId, SkillExtDto ext); + /** + * 导入技能(支持文件或URL导入) + * + * @param url 技能文件URL(与file二选一) + * @param file 上传的技能文件(与url二选一) + * @param targetSkillId 目标技能ID(导入到已有技能),为null时创建新技能 + * @param targetSpaceId 目标空间ID(创建新技能时必填) + * @param usageScenarios 使用场景列表 + * @return 技能ID + */ + Long importSkill(String url, MultipartFile file, Long targetSkillId, Long targetSpaceId, List usageScenarios); + /** * 复制技能 */ @@ -78,6 +91,11 @@ public interface SkillApplicationService { */ SkillConfigDto getSkillTemplate(InputStream inputStream); + /** + * 获取技能模板(从 classpath 自动读取 skill-template.json) + */ + SkillConfigDto getSkillTemplate(); + /** * 校验空间技能权限 */ diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/TableWorkflowForPageApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/TableWorkflowForPageApplicationService.java new file mode 100644 index 00000000..010d47db --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/TableWorkflowForPageApplicationService.java @@ -0,0 +1,12 @@ +package com.xspaceagi.agent.core.adapter.application; + +import com.xspaceagi.agent.core.adapter.dto.PageAppNewSqlDTO; +import com.xspaceagi.agent.core.adapter.dto.PageAppUpdateSqlDTO; +import com.xspaceagi.agent.core.adapter.dto.PageSqlResultDTO; + +public interface TableWorkflowForPageApplicationService { + + PageSqlResultDTO tableNewSql(PageAppNewSqlDTO pageAppNewSqlDTO); + + PageSqlResultDTO tableUpdateSql(PageAppUpdateSqlDTO pageAppUpdateSqlDTO); +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/WorkflowApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/WorkflowApplicationService.java index e6a641ff..02156bcb 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/WorkflowApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/WorkflowApplicationService.java @@ -88,6 +88,8 @@ public interface WorkflowApplicationService { WorkflowConfigDto queryPublishedWorkflowConfig(Long workflowId, Long spaceId); + List queryPublishedWorkflowConfigs(List workflowIds); + /** * 添加节点 */ diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentAddDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentAddDto.java index 7b63da12..bc927fde 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentAddDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentAddDto.java @@ -31,4 +31,7 @@ public class AgentAddDto implements Serializable { @Schema(description = "类型,ChatBot 对话智能体;TaskAgent 任务型智能体", requiredMode = Schema.RequiredMode.REQUIRED) private String type; + + @Schema(description = "子类型,ChatBot 问答型、General 通用型、Custom 自定义、Flow 流程协作、Group 群组协作") + private String subType; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentConfigCacheStatusVo.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentConfigCacheStatusVo.java new file mode 100644 index 00000000..8f568192 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentConfigCacheStatusVo.java @@ -0,0 +1,21 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import lombok.Data; + +import java.util.List; + +@Data +public class AgentConfigCacheStatusVo { + + private AgentConfigDto agentConfig; + private List components; + + @Data + public static class AgentComponentCacheStatusVo { + private Published.TargetType targetType; + private Long targetId; + private Long lastUpdated; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentConfigUpdateDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentConfigUpdateDto.java index 8dca4ba3..f4a60c1d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentConfigUpdateDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentConfigUpdateDto.java @@ -74,4 +74,13 @@ public class AgentConfigUpdateDto implements Serializable { @Schema(description = "是否隐藏聊天区域,1 隐藏;0 不隐藏") private Integer hideChatArea; + + @Schema(description = "是否允许用户在对话框中选择模式, 1 允许,其他不允许") + private Integer allowChooseMode; + + @Schema(description = "是否开启询问用户, 1 允许,其他不允许") + private Integer enableAskQuestion; + + @Schema(description = "是否开启版本控制, 1 允许,其他不允许") + private Integer enableVersionControl; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentDetailDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentDetailDto.java index 6154ba76..cdcec448 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentDetailDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentDetailDto.java @@ -27,6 +27,9 @@ public class AgentDetailDto implements Serializable { @Schema(description = "智能体类型,ChatBot 对话智能体;PageApp 网页应用智能体") private String type; + @Schema(description = "子类型,ChatBot->ChatBot、PageApp->PageApp, TaskAgent -> General、Custom、Flow、Group") + private String subType; + @Schema(description = "智能体名称") private String name; @@ -107,6 +110,12 @@ public class AgentDetailDto implements Serializable { @Schema(description = "是否允许用户在对话框中选择自己的电脑, 1 允许,其他不允许") private Integer allowPrivateSandbox; + @Schema(description = "是否允许用户在对话框中选择模式, 1 允许,其他不允许") + private Integer allowChooseMode; + + @Schema(description = "是否开启版本控制, 1 允许,其他不允许") + private Integer enableVersionControl; + @Schema(description = "扩展页面首页") private String pageHomeIndex; @@ -130,4 +139,7 @@ public class AgentDetailDto implements Serializable { @Schema(description = "超出调用限制提示") private boolean overCallLimit; + + @Schema(description = "是否显示发布按钮") + private boolean showPublishBtn; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentOutputDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentOutputDto.java index b4c7097a..9fd0f4b1 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentOutputDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentOutputDto.java @@ -25,6 +25,7 @@ public class AgentOutputDto implements Serializable { PROCESSING_MESSAGE, // 输出消息 MESSAGE, + STATE, // 最终统计等消息 FINAL_RESULT, diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentPublishVersionDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentPublishVersionDto.java new file mode 100644 index 00000000..ef8b3a81 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/AgentPublishVersionDto.java @@ -0,0 +1,34 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AgentPublishVersionDto implements Serializable { + + private String version; + + private String gitCommit; + + @Builder.Default + private Boolean latest = true; + + private List packages; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class PackageArtifact implements Serializable { + private String platform; + private String url; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ChatMessageDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ChatMessageDto.java index 3f46a849..4562fbd4 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ChatMessageDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ChatMessageDto.java @@ -1,17 +1,16 @@ package com.xspaceagi.agent.core.adapter.dto; import com.xspaceagi.agent.core.spec.enums.MessageTypeEnum; +import com.xspaceagi.agent.core.spec.utils.TikTokensUtil; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.MessageType; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.ai.chat.messages.*; -import java.util.Date; -import java.util.List; -import java.util.Map; +import java.util.*; @Data @Builder @@ -19,6 +18,8 @@ import java.util.Map; @NoArgsConstructor public class ChatMessageDto implements Message { + private static final int MAX_TOKEN_WINDOW_SIZE = 64 * 1024; + private Long index; private Long tenantId; @@ -72,7 +73,10 @@ public class ChatMessageDto implements Message { @Override public MessageType getMessageType() { - return MessageType.valueOf(role.name()); + if (role != null) { + return MessageType.valueOf(role.name()); + } + return MessageType.USER; } public enum Role { @@ -84,6 +88,74 @@ public class ChatMessageDto implements Message { public enum SenderType { USER, - AGENT + AGENT, + REMINDER + } + + public static List toMessages(List messages) { + List all = new ArrayList<>(); + //cachedMessageList转Message + for (ChatMessageDto message : messages) { + if (message.getRole().name().equals(MessageType.USER.name())) { + String text = message.getText(); + if (text != null) { + text = text.replaceAll("[\\s\\S]*?", "").trim(); + } + all.add(new UserMessage(text == null ? "" : text)); + } + if (message.getRole().name().equals(MessageType.ASSISTANT.name())) { + String text = removeSystemTagContent(message.getText()); + if (text != null) { + text = text.replaceAll("\n
]*>.*?
\n\n", ""); + text = text.replaceAll("]*>.*?", ""); + } + all.add(new AssistantMessage(text)); + } + if (message.getRole().name().equals(MessageType.SYSTEM.name())) { + all.add(new SystemMessage(message.getText())); + } + } + + Collections.reverse(all); + Iterator iterator = all.iterator(); + int tokenCount = 0; + MessageType messageType = MessageType.ASSISTANT; + while (iterator.hasNext()) { + Message next = iterator.next(); + if (messageType != next.getMessageType()) { + iterator.remove(); + continue; + } + messageType = messageType == MessageType.USER ? MessageType.ASSISTANT : MessageType.USER; + if (tokenCount >= MAX_TOKEN_WINDOW_SIZE) { + iterator.remove(); + } else { + tokenCount += TikTokensUtil.tikTokensCount(next.getText()); + } + } + Collections.reverse(all); + removeIfFirstMessageIsAssistant(all); + return all; + } + + public static String removeSystemTagContent(String text) { + if (text == null) { + return null; + } + return text.replaceAll("[\\s\\S]*?", "").trim() + .replaceAll("```xml[\\s\\S]*?[\\s\\S]*?[\\s\\S]*?```", " ") + .replaceAll("[\\s\\S]*?", " "); + } + + + //移除第一条消息不是User的内容,避免像deepseek第一条消息不是User消息时,导致无法正常工具调用 + private static void removeIfFirstMessageIsAssistant(List all) { + if (CollectionUtils.isEmpty(all)) { + return; + } + if (all.get(0).getMessageType() == MessageType.ASSISTANT) { + all.remove(0); + removeIfFirstMessageIsAssistant(all); + } } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ComponentDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ComponentDto.java index 1798b042..c67cebc3 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ComponentDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ComponentDto.java @@ -1,15 +1,14 @@ package com.xspaceagi.agent.core.adapter.dto; +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.spec.enums.ComponentTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + import java.io.Serializable; import java.util.Date; import java.util.List; -import com.xspaceagi.agent.core.adapter.repository.entity.Published; -import com.xspaceagi.agent.core.spec.enums.ComponentTypeEnum; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - @Data public class ComponentDto implements Serializable { @@ -19,6 +18,9 @@ public class ComponentDto implements Serializable { @Schema(description = "空间ID") private Long spaceId; + @Schema(description = "分组ID") + private Long groupId; + @Schema(description = "组件类型") private ComponentTypeEnum type; @@ -54,4 +56,7 @@ public class ComponentDto implements Serializable { @Schema(description = "是否启用") private Integer enabled; + @Schema(description = "[插件专用]开发时使用的会话ID") + private Long devAgentConversationId; + } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ConversationDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ConversationDto.java index 5948ad96..ddfe026d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ConversationDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ConversationDto.java @@ -36,6 +36,9 @@ public class ConversationDto { @Schema(description = "会话摘要,当开启长期记忆时,会对每次会话进行总结") private String summary; + @Schema(description = "会话图标") + private String icon; + @Schema(description = "用户填写的会话变量内容") private Map variables; @@ -68,6 +71,13 @@ public class ConversationDto { private String sandboxSessionId; + @Schema(description = "开发项目所在的空间ID") + private Long devSpaceId; + @Schema(description = "开发模式目标类型 Agent,PageApp,Skill,Plugin") + private String devTargetType; + @Schema(description = "开发模式目标ID") + private String devTargetId; + @Schema(description = "已分享的URI地址,比对上了则不需要认证") private List sharedUris; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ConversationUpdateDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ConversationUpdateDto.java index ff9649d9..bff35f47 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ConversationUpdateDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ConversationUpdateDto.java @@ -18,4 +18,7 @@ public class ConversationUpdateDto implements Serializable { @Schema(description = "会话主题,可以不传,firstMessage与topic二选一") private String topic; + + @Schema(description = "会话图标") + private String icon; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/CreateWorkspaceDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/CreateWorkspaceDto.java index 67938bb7..a5ed5ef1 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/CreateWorkspaceDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/CreateWorkspaceDto.java @@ -10,6 +10,7 @@ import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; import java.io.Serializable; import java.util.List; +import java.util.Map; @Data @NoArgsConstructor @@ -31,4 +32,17 @@ public class CreateWorkspaceDto implements Serializable { @Schema(description = "子代理列表") private List subagents; + + @Schema(description = "MCP servers配置,key为server名称,value为server配置") + private Map mcpServersConfig; + + @Schema(description = "工具权限配置") + private PermissionsConfigDto permissionsConfig; + + @Schema(description = "Hooks配置,key为事件类型(如PreToolUse),value为Hook规则列表") + private Map> hooksConfig; + + @Schema(description = "Hook外挂脚本列表") + private List hookScripts; + } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/EnNameDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/EnNameDto.java similarity index 90% rename from qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/EnNameDto.java rename to qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/EnNameDto.java index a22e4947..e8127c90 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/EnNameDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/EnNameDto.java @@ -1,4 +1,4 @@ -package com.xspaceagi.agent.web.ui.controller.dto; +package com.xspaceagi.agent.core.adapter.dto; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import lombok.Data; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoAiDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoAiDto.java new file mode 100644 index 00000000..b7e99f57 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoAiDto.java @@ -0,0 +1,25 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class GenerateInfoAiDto implements Serializable { + + @JsonPropertyDescription("项目名称,简洁有意义,不超过20个字符") + private String name; + + @JsonPropertyDescription("项目描述,一句话概括功能和用途,不超过100个字符") + private String description; + + @JsonPropertyDescription("SVG图标内容,根元素必须为 ;第一个元素必须是铺满画布的背景 rect(x=0,y=0,width=100,height=100);主图形需占满大部分画布(约10-90坐标范围),不要留大空白边距;不使用外部资源") + private String svgIcon; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoReqDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoReqDto.java new file mode 100644 index 00000000..f8082e71 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoReqDto.java @@ -0,0 +1,22 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "AI生成项目信息请求") +public class GenerateInfoReqDto implements Serializable { + + @Schema(description = "用户需求描述", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + private String prompt; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoResultDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoResultDto.java new file mode 100644 index 00000000..af066480 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/GenerateInfoResultDto.java @@ -0,0 +1,26 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "AI生成项目信息结果") +public class GenerateInfoResultDto implements Serializable { + + @Schema(description = "项目名称") + private String name; + + @Schema(description = "项目描述") + private String description; + + @Schema(description = "图标文件访问URL") + private String iconUrl; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookEntryDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookEntryDto.java new file mode 100644 index 00000000..4da31364 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookEntryDto.java @@ -0,0 +1,25 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Hook规则条目") +public class HookEntryDto implements Serializable { + + // 过滤条件(如 "Bash" 仅针对 Bash 工具) + @Schema(description = "匹配器,如工具名或正则表达式") + private String matcher; + + @Schema(description = "Hook配置列表,每项为解析后的 JSON 对象") + private List hooks; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookItemDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookItemDto.java new file mode 100644 index 00000000..b88c06ba --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookItemDto.java @@ -0,0 +1,32 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "单个Hook脚本配置") +public class HookItemDto implements Serializable { + + @Schema(description = "Hook类型," + + "command - 运行 shell 脚本、" + + "http - 调用 HTTP 端点、" + + "mcp_tool - 调用 MCP 工具、" + + "prompt - 向 Claude 发送提示、" + + "agent - 生成 subagent") + private String type; + + @Schema(description = "要执行的命令") + private String command; + + @Schema(description = "命令参数列表") + private List args; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookScriptDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookScriptDto.java new file mode 100644 index 00000000..d552f49b --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/HookScriptDto.java @@ -0,0 +1,24 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Hook外挂脚本DTO") +public class HookScriptDto implements Serializable { + + // 比如:传的相对路径:hooks/my-script.sh,实际存放路径是 ${PROJECT_DIR}/.claude/hooks/my-script.sh + @Schema(description = "脚本相对路径,如 hooks/my-script.sh") + private String path; + + @Schema(description = "脚本文件内容") + private String content; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/InitProjectTemplateDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/InitProjectTemplateDto.java new file mode 100644 index 00000000..9900d6bf --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/InitProjectTemplateDto.java @@ -0,0 +1,89 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Arrays; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "初始化项目模板请求DTO") +public class InitProjectTemplateDto implements Serializable { + + @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) + private Long userId; + + @Schema(description = "会话ID", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty("cId") + private Long cId; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED) + private ProjectType projectType; + + @Schema(description = "编程语言(projectType=AGENT时必填)") + private ProgrammingLanguage programmingLanguage; + + public enum ProjectType { + AGENT("agent"), + SKILL("skill"); + + private final String value; + + ProjectType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static ProjectType fromValue(String value) { + if (value == null) { + return null; + } + return Arrays.stream(values()) + .filter(e -> e.value.equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown projectType: " + value)); + } + } + + public enum ProgrammingLanguage { + TYPESCRIPT("typescript"), + PYTHON("python"); + + private final String value; + + ProgrammingLanguage(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static ProgrammingLanguage fromValue(String value) { + if (value == null) { + return null; + } + return Arrays.stream(values()) + .filter(e -> e.value.equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown programmingLanguage: " + value)); + } + } + +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/InstallProjectDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/InstallProjectDto.java new file mode 100644 index 00000000..17af612d --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/InstallProjectDto.java @@ -0,0 +1,29 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "安装项目依赖请求DTO") +public class InstallProjectDto implements Serializable { + + @Schema(description = "用户ID", hidden = true) + private Long userId; + + @Schema(description = "会话ID", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty("cId") + private Long cId; + + @Schema(description = "编程语言", requiredMode = Schema.RequiredMode.REQUIRED) + private InitProjectTemplateDto.ProgrammingLanguage programmingLanguage; + +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/McpServerConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/McpServerConfigDto.java new file mode 100644 index 00000000..2159d651 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/McpServerConfigDto.java @@ -0,0 +1,34 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "MCP Server配置") +public class McpServerConfigDto implements Serializable { + + @Schema(description = "MCP Server类型,stdio、http、streamable-http、sse、ws") + private String type; + + @Schema(description = "远程HTTP端点URL") + private String url; + + @Schema(description = "启动命令,如 npx、python(type为stdio时使用)") + private String command; + + @Schema(description = "命令参数列表(type为stdio时使用)") + private List args; + + @Schema(description = "环境变量(type为stdio时使用)") + private Map env; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PackageAgentDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PackageAgentDto.java new file mode 100644 index 00000000..88f755a1 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PackageAgentDto.java @@ -0,0 +1,30 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PackageAgentDto implements Serializable { + + @NotNull + private Long userId; + + @NotNull + @JsonProperty("cId") + private Long cId; + + @NotNull + private Long agentId; + + @NotNull + private InitProjectTemplateDto.ProgrammingLanguage programmingLanguage; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageAppNewSqlDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageAppNewSqlDTO.java new file mode 100644 index 00000000..3943dc84 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageAppNewSqlDTO.java @@ -0,0 +1,38 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.xspaceagi.agent.core.adapter.dto.config.Arg; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +public class PageAppNewSqlDTO { + + @Schema(description = "空间ID,环境变量中读取 DEV_SPACE_ID", requiredMode = Schema.RequiredMode.REQUIRED, hidden = true) + private Long spaceId; + + @Schema(description = "项目ID,环境变量中读取 DEV_PROJECT_ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String projectId; + + @Schema(description = "给接口一个分组名称,便于管理,尽量将所有接口放在一个分组里") + private String groupName; + + @Schema(description = "分组描述") + private String groupDescription; + + @Schema(description = "参数,使用一级即可") + private List args; + + @Schema(description = "表ID") + private Long tableId; + + @Schema(description = "SQL:Variable placeholders do not need single quotes, e.g., SELECT * FROM custom_table WHERE agent_id = {{agent_id}};For LIKE fuzzy queries, use $+variable, e.g., SELECT * FROM custom_table WHERE agent_name LIKE '%${{agent_name}}%';", requiredMode = Schema.RequiredMode.REQUIRED) + private String sql; + + @Schema(description = "API名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String apiName; + + @Schema(description = "API描述", requiredMode = Schema.RequiredMode.REQUIRED) + private String description; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageAppUpdateSqlDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageAppUpdateSqlDTO.java new file mode 100644 index 00000000..9882ad82 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageAppUpdateSqlDTO.java @@ -0,0 +1,29 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.xspaceagi.agent.core.adapter.dto.config.Arg; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +public class PageAppUpdateSqlDTO { + + @Schema(description = "项目ID,环境变量中读取 DEV_PROJECT_ID", requiredMode = Schema.RequiredMode.REQUIRED) + private String projectId; + + @Schema(description = "API ID") + private Long apiId; + + @Schema(description = "参数,使用一级即可") + private List args; + + @Schema(description = "SQL:Variable placeholders do not need single quotes, e.g., SELECT * FROM custom_table WHERE agent_id = {{agent_id}};For LIKE fuzzy queries, use $+variable, e.g., SELECT * FROM custom_table WHERE agent_name LIKE '%${{agent_name}}%';", requiredMode = Schema.RequiredMode.REQUIRED) + private String sql; + + @Schema(description = "API名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String apiName; + + @Schema(description = "API描述", requiredMode = Schema.RequiredMode.REQUIRED) + private String description; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageSqlResultDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageSqlResultDTO.java new file mode 100644 index 00000000..545d4f35 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PageSqlResultDTO.java @@ -0,0 +1,14 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class PageSqlResultDTO { + + @Schema(description = "接口ID") + private Long apiId; + + @Schema(description = "接口Schema,可用于调试") + private String apiSchema; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PermissionsConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PermissionsConfigDto.java new file mode 100644 index 00000000..02ace7d7 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PermissionsConfigDto.java @@ -0,0 +1,27 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "工具权限配置") +public class PermissionsConfigDto implements Serializable { + + @Schema(description = "自动允许的工具规则列表,如 [\"Bash(npm run lint)\", \"Read(~/.zshrc)\"]") + private List allow; + + @Schema(description = "拒绝的工具规则列表,如 [\"Bash(curl *)\", \"Read(./.env)\"]") + private List deny; + + @Schema(description = "需要询问的工具规则列表,如 [\"Bash(git push *)\"]") + private List ask; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PluginExecuteRequestDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PluginExecuteRequestDto.java index fb8e6157..08e08800 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PluginExecuteRequestDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PluginExecuteRequestDto.java @@ -21,4 +21,6 @@ public class PluginExecuteRequestDto implements Serializable { private Map params; private boolean test; + + private String apiKey; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PluginUpdateDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PluginUpdateDto.java index 673177b8..9b20f1ce 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PluginUpdateDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PluginUpdateDto.java @@ -24,4 +24,7 @@ public class PluginUpdateDto implements Serializable { @Schema(description = "插件配置") private T config; + + @Schema(description = "开发智能体的会话ID") + private Long devAgentConversationId; } \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ProjectCreateDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ProjectCreateDTO.java new file mode 100644 index 00000000..8ffa6e5c --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ProjectCreateDTO.java @@ -0,0 +1,24 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class ProjectCreateDTO { + + @Schema(description = "项目所属空间ID,不传则默认放在个人空间,可选") + private Long spaceId; + + @Schema(description = "目标项目类型", requiredMode = Schema.RequiredMode.REQUIRED) + private Published.TargetType targetType; + + @Schema(description = "目标项目名称,可选") + private String name; + + @Schema(description = "目标项目创建者ID,可选", hidden = true) + private Long creatorId; + + @Schema(description = "编程语言选择") + private InitProjectTemplateDto.ProgrammingLanguage programmingLanguage; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ProjectCreateResultDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ProjectCreateResultDTO.java new file mode 100644 index 00000000..1c2de5cb --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ProjectCreateResultDTO.java @@ -0,0 +1,18 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class ProjectCreateResultDTO { + + @Schema(description = "目标项目类型") + private Published.TargetType targetType; + + @Schema(description = "目标项目ID") + private String targetId; + + @Schema(description = "开发智能体的关联的会话ID") + private Long conversationId; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PublishedDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PublishedDto.java index 42a55c44..f4d349c2 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PublishedDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PublishedDto.java @@ -1,18 +1,17 @@ package com.xspaceagi.agent.core.adapter.dto; -import java.io.Serializable; -import java.math.BigDecimal; -import java.util.Date; -import java.util.List; - import com.xspaceagi.agent.core.adapter.repository.entity.Published; import com.xspaceagi.agent.core.spec.enums.PluginTypeEnum; import com.xspaceagi.agent.core.spec.enums.UsageScenarioEnum; import com.xspaceagi.custompage.sdk.dto.SourceTypeEnum; - import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + @Data public class PublishedDto implements Serializable { @@ -23,6 +22,8 @@ public class PublishedDto implements Serializable { private Long spaceId; + private Long groupId; + @Schema(description = "目标对象(智能体、工作流、插件)ID") private Published.TargetType targetType; @@ -102,6 +103,9 @@ public class PublishedDto implements Serializable { @Schema(description = "价格") private BigDecimal price; + @Schema(description = "价格类型, ONE_TIME 次;SECOND 秒;MILLION_TOKEN 百万Token") + private String pricingType; + @Schema(description = "是否已订阅,对智能体和技能有效") private boolean subscribed; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PublishedQueryDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PublishedQueryDto.java index 4e900998..b8de9a09 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PublishedQueryDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/PublishedQueryDto.java @@ -23,9 +23,15 @@ public class PublishedQueryDto implements Serializable { @Schema(description = "子类型") private Published.TargetSubType targetSubType; + @Schema(description = "智能体类型") + private List agentTypes; + @Schema(description = "页码") private Integer page; + @Schema(description = "上一页最后一条数据的时间戳,与page二选一") + private Long lastTimestamp; + @Schema(description = "每页数量") private Integer pageSize; @@ -65,12 +71,21 @@ public class PublishedQueryDto implements Serializable { @Schema(description = "查询的ID范围,比如只查看推荐、官方标识的组件等", hidden = true) private List targetIds; + @Schema(description = "推荐列表", hidden = true) + private List recommendIds; + + @Schema(description = "排除列表", hidden = true) + private List excludeIds; + @Schema(description = "是否只返回官方标识的内容") private Boolean official; @Schema(description = "适用场景筛选参数,如 [TaskAgent, PageApp]") private List usageScenarios; + @Schema(description = "是否返回配置信息", hidden = true) + private boolean returnConfig; + /** * 获取页码 * diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ResourceGroupDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ResourceGroupDto.java new file mode 100644 index 00000000..721dc8d0 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ResourceGroupDto.java @@ -0,0 +1,46 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +@Data +public class ResourceGroupDto implements Serializable { + + private Long id; + + private Long spaceId; + + private String name; + + private String description; + + private String icon; + + private Integer toolCount; + + private String type; + + @Schema(description = "目标对象(智能体、工作流、插件)类型,分组中第一个工具的类型") + private Published.TargetType targetType; + + @Schema(description = "目标对象(工作流、插件)ID,分组中第一个工具的ID") + private Long targetId; + + private Date created; + + private Date modified; + + @Schema(description = "是否需要付费") + private boolean paymentRequired; + + @Schema(description = "发布者信息") + private PublishUserDto publishUser; + + @Schema(description = "工具列表,选择使用") + private List tools; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ResourceGroupRelationDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ResourceGroupRelationDto.java new file mode 100644 index 00000000..be50d914 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/ResourceGroupRelationDto.java @@ -0,0 +1,6 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; + +public class ResourceGroupRelationDto extends ResourceGroupRelation { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SkillConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SkillConfigDto.java index e8d85da4..377bca4a 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SkillConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SkillConfigDto.java @@ -56,4 +56,7 @@ public class SkillConfigDto implements Serializable { @Schema(description = "技能发布zip代理下载地址") private String zipFileUrl; + + @Schema(description = "技能开发智能体的关联的会话ID") + private Long devAgentConversationId; } \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SkillPublishedConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SkillPublishedConfigDto.java index 438e0f13..667b37da 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SkillPublishedConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SkillPublishedConfigDto.java @@ -23,4 +23,6 @@ public class SkillPublishedConfigDto { @Schema(description = "技能发布zip代理下载地址") private String zipFileUrl; + + private Long devAgentConversationId; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SortUpdateDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SortUpdateDTO.java new file mode 100644 index 00000000..e7a9e7ad --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/SortUpdateDTO.java @@ -0,0 +1,10 @@ +package com.xspaceagi.agent.core.adapter.dto; + +import lombok.Data; + +@Data +public class SortUpdateDTO { + + private Long id; + private Integer sort; +} \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/TopicGenDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/TopicGenDto.java index fade46e0..4219e396 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/TopicGenDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/TopicGenDto.java @@ -17,4 +17,6 @@ public class TopicGenDto implements Serializable { @JsonPropertyDescription("Contextually appropriate titles, ≤50 characters. Do not generate titles unrelated to the topic. For meaningless contexts, such as user input errors or arbitrary content, no summary is needed; return empty. Output language based on the ") private String topic; + @JsonPropertyDescription("SVG icon, viewBox='0 0 100 100'. Do not generate icons unrelated to the topic. For meaningless contexts, such as user input errors or arbitrary content, no icon is needed; return empty. Output language based on the . \nSVG requirements: \n- Use simple geometric shapes and clean lines \n- Use a modern color palette (gradient or solid colors) \n- Must be a valid, self-contained SVG with no external references \n- Keep it simple and recognizable at small sizes \n- The SVG should be visually appealing and professional") + private String svgIcon; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/TryReqDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/TryReqDto.java index 5d81489d..34f92639 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/TryReqDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/TryReqDto.java @@ -48,6 +48,9 @@ public class TryReqDto implements Serializable { @Schema(description = "请求ID", hidden = true) private String requestId; + @Schema(description = "代理模式") + private String agentMode; + @Data public static class SelectedComponentDto { @Schema(description = "组件ID") diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/WorkflowExecuteRequestDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/WorkflowExecuteRequestDto.java index a827febf..25df5b9c 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/WorkflowExecuteRequestDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/WorkflowExecuteRequestDto.java @@ -33,6 +33,8 @@ public class WorkflowExecuteRequestDto implements Serializable { private boolean test; + private String apiKey; + public Map getParams() { return params == null ? params = new HashMap<>() : params; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/AgentConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/AgentConfigDto.java index b6d9cee9..fb74d938 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/AgentConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/AgentConfigDto.java @@ -2,10 +2,7 @@ package com.xspaceagi.agent.core.adapter.dto.config; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; -import com.xspaceagi.agent.core.adapter.dto.CreatorDto; -import com.xspaceagi.agent.core.adapter.dto.GuidQuestionDto; -import com.xspaceagi.agent.core.adapter.dto.PublishUserDto; -import com.xspaceagi.agent.core.adapter.dto.StatisticsDto; +import com.xspaceagi.agent.core.adapter.dto.*; import com.xspaceagi.agent.core.adapter.dto.config.bind.*; import com.xspaceagi.agent.core.adapter.repository.entity.AgentComponentConfig; import com.xspaceagi.agent.core.adapter.repository.entity.AgentConfig; @@ -36,6 +33,9 @@ public class AgentConfigDto implements Serializable { @Schema(description = "类型,ChatBot 对话智能体;PageApp 网页应用智能体, TaskAgent 任务型智能体") private String type; + @Schema(description = "子类型,ChatBot->ChatBot、PageApp->PageApp, TaskAgent -> General、Custom、Flow、Group") + private String subType; + @Schema(description = "商户ID") private Long tenantId; // 商户ID @@ -111,9 +111,12 @@ public class AgentConfigDto implements Serializable { @Schema(description = "是否收藏", hidden = true) private boolean isCollected; - @Schema(description = "开发会话ID") + @Schema(description = "debug调试会话ID") private Long devConversationId; + @Schema(description = "开发Agent的会话ID") + private Long devAgentConversationId; + @Schema(description = "发布时间,如果不为空,与当前modified时间做对比,如果发布时间小于modified,则前端显示:有更新未发布") private Date publishDate; @@ -162,6 +165,23 @@ public class AgentConfigDto implements Serializable { @Schema(description = "是否允许用户在对话框中选择自己的电脑, 1 允许,其他不允许") private Integer allowPrivateSandbox; + @Schema(description = "是否允许用户在对话框中选择模式, 1 允许,其他不允许") + private Integer allowChooseMode; + + @Schema(description = "是否开启询问用户, 1 允许,其他不允许") + private Integer enableAskQuestion; + + @Schema(description = "是否开启版本控制, 1 允许,其他不允许") + private Integer enableVersionControl; + + @Schema(description = "发布版本列表") + private List publishVersion; + + @Schema(description = "已发布的空间ID", hidden = true) + private List publishedSpaceIds; + + private List customPageMenus; + public String getPageHomeIndex() { //设置默认页面首页 if (getAgentComponentConfigList() != null) { @@ -249,6 +269,10 @@ public class AgentConfigDto implements Serializable { VariableConfigDto variableConfigDto = JSON.parseObject(componentConfig.getBindConfig().toString(), VariableConfigDto.class); componentConfig.setBindConfig(variableConfigDto); } + case Hook -> { + HookConfigDto hookConfigDto = JSON.parseObject(componentConfig.getBindConfig().toString(), HookConfigDto.class); + componentConfig.setBindConfig(hookConfigDto); + } case Mcp -> { McpBindConfigDto mcpBindConfigDto = JSON.parseObject(componentConfig.getBindConfig().toString(), McpBindConfigDto.class); componentConfig.setBindConfig(mcpBindConfigDto); @@ -308,6 +332,11 @@ public class AgentConfigDto implements Serializable { componentConfig.setBindConfig(new VariableConfigDto()); } } + case Hook -> { + if (!(componentConfig.getBindConfig() instanceof HookConfigDto)) { + componentConfig.setBindConfig(new HookConfigDto()); + } + } case Mcp -> { if (!(componentConfig.getBindConfig() instanceof McpBindConfigDto)) { componentConfig.setBindConfig(new McpBindConfigDto()); @@ -350,4 +379,12 @@ public class AgentConfigDto implements Serializable { @Schema(description = "是否选中") private boolean selected; } + + public enum AgentType { + ChatBot, PageApp, TaskAgent + } + + public enum AgentSubType { + ChatBot, PageApp, General, Custom, Flow, Group + } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/Arg.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/Arg.java index 99ab2b44..ab362b88 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/Arg.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/Arg.java @@ -70,6 +70,9 @@ public class Arg implements Serializable { private Long loopId; + @Schema(description = "参数更新策略") + private UpdateStrategy updateStrategy; + public void setSubArgs(List subArgs) { if (this.subArgs != null) { return; @@ -121,7 +124,14 @@ public class Arg implements Serializable { Number, //自动识别 AutoRecognition, + File, + Radio, + FixedValue, + } + public enum UpdateStrategy { + REPLACE, // Overwrite the existing value entirely + APPEND // Append to the existing value (string concatenation, list append, or numeric addition) } public static List updateBindConfigArgs(String startKey, List bindConfigArgs, List configInputArgs) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/HookConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/HookConfigDto.java new file mode 100644 index 00000000..dbf2356b --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/HookConfigDto.java @@ -0,0 +1,30 @@ +package com.xspaceagi.agent.core.adapter.dto.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +@Data +public class HookConfigDto implements Serializable { + + @Schema(description = "Hook列表") + private List hooks; + + @Data + public static class Hook { + @Schema(description = "Hook名称") + private String name; + @Schema(description = "Hook事件") + private String event; + @Schema(description = "Hook匹配规则") + private String matcher; + @Schema(description = "Hook类型") + private String type; + @Schema(description = "Hook配置") + private String config; + @Schema(description = "Hook状态,1 启用;0 停用") + private Integer status; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/McpBindConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/McpBindConfigDto.java index 9e225a9a..c8c0b0c9 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/McpBindConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/McpBindConfigDto.java @@ -34,6 +34,9 @@ public class McpBindConfigDto implements Serializable { @Schema(description = "是否默认选中,0-否,1-是") private Integer defaultSelected; + @Schema(description = "是否需要审批,0-否,1-是") + private Integer callApproval; + public enum McpInvokeTypeEnum { //自动调用、按需调用、手动选择、手动选择和按需调用 AUTO, ON_DEMAND, MANUAL, MANUAL_ON_DEMAND diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/PluginBindConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/PluginBindConfigDto.java index 2b73e7aa..dcd0dd9c 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/PluginBindConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/PluginBindConfigDto.java @@ -31,6 +31,9 @@ public class PluginBindConfigDto implements Serializable { @Schema(description = "是否默认选中,0-否,1-是") private Integer defaultSelected; + @Schema(description = "是否需要审批,0-否,1-是") + private Integer callApproval; + public enum PluginInvokeTypeEnum { //自动调用、按需调用、手动选择、手动选择和按需调用 AUTO, ON_DEMAND, MANUAL, MANUAL_ON_DEMAND diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/WorkflowBindConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/WorkflowBindConfigDto.java index f643e693..c419109f 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/WorkflowBindConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/bind/WorkflowBindConfigDto.java @@ -37,6 +37,9 @@ public class WorkflowBindConfigDto implements Serializable { // 相同requestId时是否使用缓存的结果,问答场景 private boolean useResultCache; + @Schema(description = "是否需要审批,0-否,1-是") + private Integer callApproval; + public enum WorkflowInvokeTypeEnum { //自动调用、按需调用 AUTO, ON_DEMAND, MANUAL, MANUAL_ON_DEMAND diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/plugin/PluginDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/plugin/PluginDto.java index 792289eb..73fcda1b 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/plugin/PluginDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/plugin/PluginDto.java @@ -72,6 +72,11 @@ public class PluginDto implements Serializable { @Schema(description = "已发布的空间ID", hidden = true) private List publishedSpaceIds; + @Schema(description = "开发时使用的会话ID") + private Long devAgentConversationId; + + private String toolId; + public static PluginDto convertToPluginDto(String config) { if (config == null) { return null; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/AgentNodeConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/AgentNodeConfigDto.java new file mode 100644 index 00000000..20495c2b --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/AgentNodeConfigDto.java @@ -0,0 +1,33 @@ +package com.xspaceagi.agent.core.adapter.dto.config.workflow; + +import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class AgentNodeConfigDto extends NodeConfigDto { + + @Schema(description = "智能体ID") + private Long agentId; + + @Schema(description = "节点名称") + private String name; + + @Schema(description = "节点描述") + private String description; + + @Schema(description = "节点图标") + private String icon; + + @Schema(description = "补充提示词") + private String extraPrompt; + + @Schema(description = "自身循环次数") + private int selfLoopTimes; + + @Schema(description = "循环提醒提示词") + private String reminderPrompt; + + @Schema(description = "智能体配置信息,执行阶段使用") + private AgentConfigDto agentConfigDto; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/IntentRecognitionNodeConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/IntentRecognitionNodeConfigDto.java index fa332b45..a79077e6 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/IntentRecognitionNodeConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/IntentRecognitionNodeConfigDto.java @@ -35,6 +35,9 @@ public class IntentRecognitionNodeConfigDto extends NodeConfigDto { @Schema(description = "token上限") private Integer maxTokens; // token上限 + @Schema(description = "系统提示词") + private String systemPrompt; + @Schema(description = "补充提示词") private String extraPrompt; @@ -55,12 +58,22 @@ public class IntentRecognitionNodeConfigDto extends NodeConfigDto { @Schema(description = "唯一标识") private String uuid; - @Schema(description = "意图描述") + @Schema(description = "意图名称") + private String name; + + @Schema(description = "意图描述,兜底匹配") private String intent; @Schema(description = "意图类型,NORMAL正常添加配置,OTHER其他意图") private IntentTypeEnum intentType; + //条件参数关系 + @Schema(description = "条件参数之间关系") + private ConditionNodeConfigDto.ConditionTypeEnum conditionType; + + //参数列表配置 + @Schema(description = "参数列表配置") + private List conditionArgs; @Schema(description = "关联下级节点id列表") private List nextNodeIds; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/KnowledgeInsertNodeConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/KnowledgeInsertNodeConfigDto.java new file mode 100644 index 00000000..4d33afd8 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/KnowledgeInsertNodeConfigDto.java @@ -0,0 +1,95 @@ +package com.xspaceagi.agent.core.adapter.dto.config.workflow; + +import com.xspaceagi.agent.core.adapter.dto.config.Arg; +import com.xspaceagi.agent.core.adapter.dto.config.InputArg; +import com.xspaceagi.agent.core.adapter.dto.config.OutputArg; +import com.xspaceagi.agent.core.spec.enums.DataTypeEnum; +import com.xspaceagi.system.spec.utils.I18nUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Data +public class KnowledgeInsertNodeConfigDto extends NodeConfigDto { + + @Schema(description = "知识库ID配置", requiredMode = Schema.RequiredMode.REQUIRED) + private Long knowledgeBaseId; + + @Schema(description = "节点名称") + private String name; + + @Schema(description = "节点描述") + private String description; + + @Schema(description = "节点图标") + private String icon; + + /** + * 公共节点配置转条件节点配置 + * + * @return 条件节点配置 + */ + public static KnowledgeInsertNodeConfigDto addFrom(Long knowledgeBaseId) { + KnowledgeInsertNodeConfigDto knowledgeInsertNodeConfigDto = new KnowledgeInsertNodeConfigDto(); + knowledgeInsertNodeConfigDto.setKnowledgeBaseId(knowledgeBaseId); + //设置默认输入参数和输出参数 + List args = new ArrayList<>(); + InputArg arg = new InputArg(); + arg.setKey(UUID.randomUUID().toString().replace("-", "")); + arg.setName("title"); + arg.setDescription(I18nUtil.systemMessage("Backend.WorkflowKbArgs.title.description")); + arg.setDataType(DataTypeEnum.String); + args.add(arg); + arg = new InputArg(); + arg.setKey(UUID.randomUUID().toString().replace("-", "")); + arg.setName("content"); + arg.setDescription(I18nUtil.systemMessage("Backend.WorkflowKbArgs.content.description")); + arg.setDataType(DataTypeEnum.String); + args.add(arg); + arg = new InputArg(); + arg.setKey(UUID.randomUUID().toString().replace("-", "")); + arg.setName("docUrl"); + arg.setDescription(I18nUtil.systemMessage("Backend.WorkflowKbArgs.docUrl.description")); + arg.setDataType(DataTypeEnum.String); + args.add(arg); + //segment + arg = new InputArg(); + arg.setKey(UUID.randomUUID().toString().replace("-", "")); + arg.setName("segment"); + arg.setDescription(I18nUtil.systemMessage("Backend.WorkflowKbArgs.segment.description")); + arg.setDataType(DataTypeEnum.String); + arg.setBindValue("WORDS"); + args.add(arg); + arg = new InputArg(); + arg.setKey(UUID.randomUUID().toString().replace("-", "")); + arg.setName("words"); + arg.setDescription(I18nUtil.systemMessage("Backend.WorkflowKbArgs.words.description")); + arg.setDataType(DataTypeEnum.Integer); + arg.setBindValue("800"); + args.add(arg); + + knowledgeInsertNodeConfigDto.setInputArgs(args); + + List outputArgs = new ArrayList<>(); + Arg outArg = new OutputArg(); + outArg.setName("success"); + outArg.setDataType(DataTypeEnum.Boolean); + outArg.setDescription(I18nUtil.systemMessage("Backend.WorkflowKbArgs.success.description")); + outputArgs.add(outArg); + outArg = new OutputArg(); + outArg.setName("message"); + outArg.setDataType(DataTypeEnum.String); + outArg.setDescription(I18nUtil.systemMessage("Backend.WorkflowKbArgs.message.description")); + outputArgs.add(outArg); + outArg = new OutputArg(); + outArg.setName("docId"); + outArg.setDataType(DataTypeEnum.Number); + outArg.setDescription(I18nUtil.systemMessage("Backend.WorkflowKbArgs.docId.description")); + outputArgs.add(outArg); + knowledgeInsertNodeConfigDto.setOutputArgs(outputArgs); + return knowledgeInsertNodeConfigDto; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/NodeConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/NodeConfigDto.java index e6089bfc..74e80fd1 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/NodeConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/NodeConfigDto.java @@ -11,6 +11,8 @@ import java.util.Map; @Data public class NodeConfigDto implements Serializable { + private ContextPassingType contextPassingType; + @Schema(description = "扩展字段,用于前端存储画布位置等相关配置") private Map extension; @@ -22,4 +24,9 @@ public class NodeConfigDto implements Serializable { @Schema(description = "异常处理配置") private ExceptionHandleConfigDto exceptionHandleConfig; + + public enum ContextPassingType { + Auto, + Manual + } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/QaNodeConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/QaNodeConfigDto.java index c715d007..e1158e29 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/QaNodeConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/QaNodeConfigDto.java @@ -1,5 +1,6 @@ package com.xspaceagi.agent.core.adapter.dto.config.workflow; +import com.xspaceagi.agent.core.adapter.dto.config.Arg; import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.bind.ModelBindConfigDto; import io.swagger.v3.oas.annotations.media.Schema; @@ -35,6 +36,9 @@ public class QaNodeConfigDto extends NodeConfigDto { @NotNull(message = "Answer type is required") private AnswerTypeEnum answerType; + @Schema(description = "表单参数") + private List formArgs; + //从回复中提取字段 @Schema(description = "从回复中提取字段") private Boolean extractField; @@ -53,7 +57,8 @@ public class QaNodeConfigDto extends NodeConfigDto { public enum AnswerTypeEnum { //直接回答、选项回答 TEXT, - SELECT + SELECT, + FORM } @Data diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/WorkflowConfigDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/WorkflowConfigDto.java index 45d3b3db..a019b245 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/WorkflowConfigDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/config/workflow/WorkflowConfigDto.java @@ -44,6 +44,12 @@ public class WorkflowConfigDto { @Schema(description = "图标地址") private String icon; + @Schema(description = "工作流类型: Workflow、AgentFlow") + private String type; + + @Schema(description = "AgentFlow时所属AgentID") + private Long agentId; + @Schema(description = "开始节点", hidden = true) private WorkflowNodeDto startNode; @@ -120,6 +126,12 @@ public class WorkflowConfigDto { case Knowledge: nodeConfigDto = JSON.parseObject(config, KnowledgeNodeConfigDto.class); break; + case KnowledgeInsert: + nodeConfigDto = JSON.parseObject(config, KnowledgeInsertNodeConfigDto.class); + break; + case Agent: + nodeConfigDto = JSON.parseObject(config, AgentNodeConfigDto.class); + break; case Loop: nodeConfigDto = JSON.parseObject(config, LoopNodeConfigDto.class); break; @@ -216,4 +228,9 @@ public class WorkflowConfigDto { } return null; } + + public enum Type { + Workflow, + AgentFlow + } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/RecommendHomeResponse.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/RecommendHomeResponse.java new file mode 100644 index 00000000..43b6bf4a --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/RecommendHomeResponse.java @@ -0,0 +1,20 @@ +package com.xspaceagi.agent.core.adapter.dto.recommend; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RecommendHomeResponse { + + private Map> recHome; + + private Map> recChatBoxNav; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendPageRequest.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendPageRequest.java new file mode 100644 index 00000000..ff028ef3 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendPageRequest.java @@ -0,0 +1,15 @@ +package com.xspaceagi.agent.core.adapter.dto.recommend; + +import lombok.Data; + +@Data +public class TargetRecommendPageRequest { + + private Integer pageNo = 1; + + private Integer pageSize = 10; + + private String recType; + + private String targetType; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendResponse.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendResponse.java new file mode 100644 index 00000000..442bff43 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendResponse.java @@ -0,0 +1,28 @@ +package com.xspaceagi.agent.core.adapter.dto.recommend; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TargetRecommendResponse { + + private Long id; + private Long tenantId; + private String targetType; + private Long targetId; + private String recType; + private String functionType; + private String label; + private String icon; + private String placeholder; + private Integer sort; + private Date modified; + private Date created; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendSaveRequest.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendSaveRequest.java new file mode 100644 index 00000000..0703e434 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/recommend/TargetRecommendSaveRequest.java @@ -0,0 +1,30 @@ +package com.xspaceagi.agent.core.adapter.dto.recommend; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class TargetRecommendSaveRequest { + + private Long id; + + @NotNull(message = "targetType不能为空") + private String targetType; + + @NotNull(message = "targetId不能为空") + private Long targetId; + + @NotNull(message = "recType不能为空") + private String recType; + + @NotNull(message = "functionType不能为空") + private String functionType; + + private String label; + + private String icon; + + private String placeholder; + + private Integer sort; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/ResourceGroupRelationRepository.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/ResourceGroupRelationRepository.java new file mode 100644 index 00000000..0fb8566a --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/ResourceGroupRelationRepository.java @@ -0,0 +1,7 @@ +package com.xspaceagi.agent.core.adapter.repository; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; + +public interface ResourceGroupRelationRepository extends IService { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/ResourceGroupRepository.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/ResourceGroupRepository.java new file mode 100644 index 00000000..9fefa391 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/ResourceGroupRepository.java @@ -0,0 +1,7 @@ +package com.xspaceagi.agent.core.adapter.repository; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroup; + +public interface ResourceGroupRepository extends IService { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/TargetRecommendRepository.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/TargetRecommendRepository.java new file mode 100644 index 00000000..4b08ce81 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/TargetRecommendRepository.java @@ -0,0 +1,7 @@ +package com.xspaceagi.agent.core.adapter.repository; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; + +public interface TargetRecommendRepository extends IService { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/AgentComponentConfig.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/AgentComponentConfig.java index 9271d2e0..f2816b1a 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/AgentComponentConfig.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/AgentComponentConfig.java @@ -49,6 +49,6 @@ public class AgentComponentConfig { private Date created; // 创建时间 public enum Type { - Plugin, Workflow, Trigger, Knowledge, Variable, Database, Model, Agent, Table, Mcp, Page, Event, Skill, SubAgent + Plugin, Workflow, Trigger, Knowledge, Variable, Database, Model, Agent, Table, Mcp, Page, Event, Skill, SubAgent, Hook } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/AgentConfig.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/AgentConfig.java index f55f60b0..ebae90fd 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/AgentConfig.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/AgentConfig.java @@ -4,14 +4,17 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.xspaceagi.agent.core.adapter.dto.AgentPublishVersionDto; +import com.xspaceagi.agent.core.adapter.typehandler.AgentPublishVersionListTypeHandler; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; +import java.util.List; -@TableName("agent_config") +@TableName(value = "agent_config", autoResultMap = true) @Data @Builder @AllArgsConstructor @@ -25,6 +28,8 @@ public class AgentConfig { private String type; + private String subType; + @TableField(value = "_tenant_id") private Long tenantId; // 商户ID @@ -62,6 +67,8 @@ public class AgentConfig { private Long devConversationId; // 开发模式对应的会话ID + private Long devAgentConversationId; // 开发Agent的会话ID + private AgentConfig.OpenStatus openScheduledTask; private Integer expandPageArea;//是否默认展开扩展页面区域, 1 展开;0 不展开 @@ -80,6 +87,15 @@ public class AgentConfig { private Integer allowPrivateSandbox; + private Integer allowChooseMode; + + private Integer enableAskQuestion; + + private Integer enableVersionControl; + + @TableField(value = "publish_version", typeHandler = AgentPublishVersionListTypeHandler.class) + private List publishVersion; + public enum OpenStatus { Open, Close; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/Conversation.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/Conversation.java index 97802aa3..26b0b065 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/Conversation.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/Conversation.java @@ -35,6 +35,8 @@ public class Conversation { private String summary; + private String icon; + @TableField(value = "variables", typeHandler = JsonTypeHandler.class) private Map variables; @@ -53,6 +55,9 @@ public class Conversation { private String sandboxServerId; private String sandboxSessionId; + private Long devSpaceId; + private String devTargetType; + private String devTargetId; private Date modified; @@ -62,7 +67,9 @@ public class Conversation { Chat, TempChat, TASK, - TaskCenter + TaskCenter, + Development, + DevDebug } public enum ConversationTaskStatus { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ModelConfig.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ModelConfig.java index 8beb7345..f5d0556d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ModelConfig.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ModelConfig.java @@ -59,6 +59,8 @@ public class ModelConfig { private Integer enabled; // 是否启用,0-否,1-是 private Integer accessControl; private String usageScenario; + private Integer sort; + public enum ModelScopeEnum { Space, Tenant, diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/PluginConfig.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/PluginConfig.java index a2806351..36c8819b 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/PluginConfig.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/PluginConfig.java @@ -39,4 +39,6 @@ public class PluginConfig { private Date modified; private Date created; + + private Long devAgentConversationId; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/Published.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/Published.java index 1e13e913..38a88156 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/Published.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/Published.java @@ -28,6 +28,8 @@ public class Published { private Long spaceId; + private Long groupId; + @TableField("user_id") private Long userId; @@ -97,7 +99,9 @@ public class Published { Knowledge, Table, Skill, - Model + Model, + PageApp, + Mcp } public enum TargetSubType { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ResourceGroup.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ResourceGroup.java new file mode 100644 index 00000000..d9c4589c --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ResourceGroup.java @@ -0,0 +1,44 @@ +package com.xspaceagi.agent.core.adapter.repository.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@TableName(value = "resource_group") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ResourceGroup { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField(value = "_tenant_id") + private Long tenantId; + + private Long spaceId; + + private Long creatorId; + + private String name; + + private String description; + + private String icon; + + private Integer toolCount; + + private String type; + + private Date created; + + private Date modified; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ResourceGroupRelation.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ResourceGroupRelation.java new file mode 100644 index 00000000..c8bcf02a --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/ResourceGroupRelation.java @@ -0,0 +1,36 @@ +package com.xspaceagi.agent.core.adapter.repository.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@TableName(value = "resource_group_relation") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ResourceGroupRelation { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField(value = "_tenant_id") + private Long tenantId; + + private String targetType; + + private Long targetId; + + private Long groupId; + + private Date created; + + private Date modified; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/SkillConfig.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/SkillConfig.java index 7c3c3794..8b415f73 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/SkillConfig.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/SkillConfig.java @@ -57,4 +57,6 @@ public class SkillConfig { private String modifiedName; // 最后修改人 private Integer yn; // 逻辑标记,1:有效;-1:无效 + + private Long devAgentConversationId; } \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/TargetRecommend.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/TargetRecommend.java new file mode 100644 index 00000000..836b42ff --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/TargetRecommend.java @@ -0,0 +1,72 @@ +package com.xspaceagi.agent.core.adapter.repository.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@TableName("target_recommend") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TargetRecommend { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField("_tenant_id") + private Long tenantId; + + @TableField("target_type") + private TargetType targetType; + + @TableField("target_id") + private Long targetId; + + @TableField("rec_type") + private RecType recType; + + @TableField("function_type") + private FunctionType functionType; + + private String label; + + private String icon; + + private String placeholder; + + private Integer sort; + + private Date modified; + + private Date created; + + public enum TargetType { + Agent, + PageApp, + Skill, + Plugin, + Workflow + } + + public enum RecType { + Home, + Official, + ChatBoxNav + } + + public enum FunctionType { + AgentDev, + PageAppDev, + SkillDev, + PluginDev, + Chat + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/WorkflowConfig.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/WorkflowConfig.java index d40ed570..78f09d0c 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/WorkflowConfig.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/WorkflowConfig.java @@ -36,6 +36,10 @@ public class WorkflowConfig { private String icon; + private String type; + + private Long agentId; + @TableField("start_node_id") private Long startNodeId; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/WorkflowNodeConfig.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/WorkflowNodeConfig.java index 53c01931..0e6ff4dc 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/WorkflowNodeConfig.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/WorkflowNodeConfig.java @@ -52,6 +52,7 @@ public class WorkflowNodeConfig { LoopContinue("继续循环", "用于终止当前循环,执行下次循环"), // 继续循环节点 LoopBreak("终止循环", "用于立即终止当前所在的循环,跳出循环体"), // 终止循环节点 Knowledge("知识库", "在选定的知识中,根据输入变量召回最匹配的信息"), // 知识库节点 + KnowledgeInsert("知识库写入", "向选定的知识库中写入新的内容"), // 知识库写入节点 TableSQL("SQL自定义", "可支持对数据表放开读写控制,用户可读写其他用户提交的数据,由开发者控制"), TableDataAdd("数据新增", "对选定的数据表进行数据写入"), TableDataDelete("数据删除", "对选定的数据表根据指定条件进行数据删除"), @@ -66,6 +67,7 @@ public class WorkflowNodeConfig { DocumentExtraction("文档提取", "用于提取文档内容,支持的文件类型: txt、 markdown、pdf、 html、 xlsx、 xls、 docx、 csv、 md、 htm"), // 文档提取节点 HTTPRequest("HTTP 请求", "用于配置http请求调用已有的服务"), // HTTP 请求节点 Mcp("MCP服务", "用于调用MCP服务"), // MCP服务节点 + Agent("Agent", "用于调用Agent服务"), End("结束", "工作流的最终节点,用于返回工作流运行后的结果信息"); // 结束节点 private String name; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/typehandler/AgentPublishVersionListTypeHandler.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/typehandler/AgentPublishVersionListTypeHandler.java new file mode 100644 index 00000000..30cf0cbe --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/typehandler/AgentPublishVersionListTypeHandler.java @@ -0,0 +1,15 @@ +package com.xspaceagi.agent.core.adapter.typehandler; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.xspaceagi.agent.core.adapter.dto.AgentPublishVersionDto; +import com.xspaceagi.system.spec.common.ListJsonTypeHandler; + +import java.util.List; + +public class AgentPublishVersionListTypeHandler extends ListJsonTypeHandler { + + @Override + protected TypeReference> getTypeReference() { + return new TypeReference>() {}; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/api/AgentApiServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/api/AgentApiServiceImpl.java index a58cc928..272cf372 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/api/AgentApiServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/api/AgentApiServiceImpl.java @@ -620,7 +620,7 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm return ReqResult.error("Page data source not found"); } Optional first = dataSources.stream().filter(dataSourceDto -> dataSourceDto.getId() != null && dataSourceDto.getId().equals(targetId)).findFirst(); - if (!first.isPresent()) { + if (first.isEmpty()) { return ReqResult.error("Data source not found"); } String key = first.get().getKey(); @@ -638,14 +638,14 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm List inputArgs = pluginConfigDto.getInputArgs(); List outputArgs = List.of( - Arg.builder().require(true).name("success").description("Interface call status, success=true only represents successful interface call, business execution logic please pay attention to data definition").dataType(DataTypeEnum.Boolean).build(), + Arg.builder().require(true).name("success").description("Interface call status, business execution logic please pay attention to data definition").dataType(DataTypeEnum.Boolean).build(), Arg.builder().require(true).name("message").description("Error information when interface call fails, such as server internal error").dataType(DataTypeEnum.String).build(), Arg.builder().require(true).name("data").description("Returned specific business data").dataType(DataTypeEnum.Object).subArgs(pluginConfigDto.getOutputArgs()).build() ); Map outputSchema = ArgConverter.convertArgsToJsonSchema(outputArgs); Map inputSchema = ArgConverter.convertArgsToJsonSchema(inputArgs); apiSchema.put("paths", Map.of( - "/api/page/plugin/" + key + "/execute", + "/api/page/p/" + key, buildPath(pluginDto.getName(), pluginDto.getDescription(), "application/json", inputSchema, outputSchema) )); } @@ -654,28 +654,17 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm if (workflowConfigDto == null) { return ReqResult.error("Workflow not found"); } - String description = workflowConfigDto.getDescription() == null ? "" : workflowConfigDto.getDescription() + "(Note that this interface is a text/event-stream streaming interface, it ends when complete=true appears. When processing data, please note that SSE data starts with data:, and SSE streaming interface is not used by default unless the user explicitly requests to use streaming interface)"; List inputArgs = workflowConfigDto.getInputArgs(); Map inputSchema = ArgConverter.convertArgsToJsonSchema(inputArgs, true); List outputArgs = List.of( - Arg.builder().require(true).name("success").description("Interface call status, success=true only represents successful interface call, business execution logic please pay attention to data definition").dataType(DataTypeEnum.Boolean).build(), - Arg.builder().require(true).name("message").description("Error information when interface call fails, such as server internal error").dataType(DataTypeEnum.String).build(), - Arg.builder().require(true).name("data").description("Returned specific business data").dataType(DataTypeEnum.Object).subArgs(workflowConfigDto.getOutputArgs()).build(), - Arg.builder().require(true).name("complete").description("Whether workflow execution is completed, just need to pay attention to results when complete=true if no special requirements").dataType(DataTypeEnum.Boolean).build() - ); - Map streamOutputSchema = ArgConverter.convertArgsToJsonSchema(outputArgs); - - outputArgs = List.of( - Arg.builder().require(true).name("success").description("Interface call status, success=true only represents successful interface call, business execution logic please pay attention to data definition").dataType(DataTypeEnum.Boolean).build(), + Arg.builder().require(true).name("success").description("Interface call status, business execution logic please pay attention to data definition").dataType(DataTypeEnum.Boolean).build(), Arg.builder().require(true).name("message").description("Error information when interface call fails, such as server internal error").dataType(DataTypeEnum.String).build(), Arg.builder().require(true).name("data").description("Returned specific business data").dataType(DataTypeEnum.Object).subArgs(workflowConfigDto.getOutputArgs()).build() ); Map outputSchema = ArgConverter.convertArgsToJsonSchema(outputArgs); apiSchema.put("paths", Map.of( - "/api/page/workflow/" + key + "/streamExecute", - buildPath(workflowConfigDto.getName(), description, "text/event-stream", inputSchema, streamOutputSchema), - "/api/page/workflow/" + key + "/execute", + "/api/page/w/" + key, buildPath(workflowConfigDto.getName(), workflowConfigDto.getDescription() == null ? "" : workflowConfigDto.getDescription(), "application/json", inputSchema, outputSchema) )); } @@ -848,6 +837,21 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm }); } } + if (workflowNodeDto.getType() == WorkflowNodeConfig.NodeType.KnowledgeInsert) { + KnowledgeInsertNodeConfigDto knowledgeNodeConfigDto = (KnowledgeInsertNodeConfigDto) workflowNodeDto.getNodeConfig(); + if (knowledgeNodeConfigDto.getKnowledgeBaseId() != null) { + KnowledgeCreateRequestVo knowledgeCreateRequestVo = KnowledgeCreateRequestVo.builder() + .dataType(1) + .name(knowledgeNodeConfigDto.getName()) + .description(knowledgeNodeConfigDto.getDescription()) + .icon(knowledgeNodeConfigDto.getIcon()) + .userId(user.getId()) + .spaceId(spaceId) + .build(); + Long knowledgeConfigId = knowledgeRpcService.createKnowledgeConfig(knowledgeCreateRequestVo, knowledgeNodeConfigDto.getKnowledgeBaseId()); + knowledgeNodeConfigDto.setKnowledgeBaseId(knowledgeConfigId); + } + } if (workflowNodeDto.getType().name().startsWith("Table")) { TableNodeConfigDto tableNodeConfigDto = (TableNodeConfigDto) workflowNodeDto.getNodeConfig(); if (StringUtils.isNotBlank(tableNodeConfigDto.getOriginalTableConfig())) { @@ -1148,7 +1152,8 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm @Override public Mono executePlugin(PluginExecuteRequestDto pluginExecuteRequest) { - PluginDto pluginDto = (PluginDto) JsonSerializeUtil.parseObjectGeneric(pluginExecuteRequest.getConfig()); + PluginDto pluginDto = pluginExecuteRequest.getConfig() instanceof PluginDto ? + (PluginDto) pluginExecuteRequest.getConfig() : (PluginDto) JsonSerializeUtil.parseObjectGeneric(pluginExecuteRequest.getConfig().toString()); if (pluginDto == null) { return Mono.error(new IllegalArgumentException("Invalid plugin configuration")); } @@ -1165,11 +1170,16 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm //For common agent, when user uses workflow, plugin, set parameter reference (temporarily only support system variables, user custom variables in agent cannot be passed) if (pluginExecuteRequest.getBindConfig() != null) { - PluginBindConfigDto pluginBindConfigDto = (PluginBindConfigDto) JsonSerializeUtil.parseObjectGeneric(pluginExecuteRequest.getBindConfig()); + PluginBindConfigDto pluginBindConfigDto = pluginExecuteRequest.getBindConfig() instanceof PluginBindConfigDto ? + (PluginBindConfigDto) pluginExecuteRequest.getBindConfig() : (PluginBindConfigDto) JsonSerializeUtil.parseObjectGeneric(pluginExecuteRequest.getBindConfig().toString()); if (pluginBindConfigDto != null) { completeArgReferenceBindValue(agentContext, pluginBindConfigDto.getInputArgBindConfigs(), pluginExecuteRequest.getParams()); } } + if (pluginExecuteRequest.getTraceContext() != null && pluginExecuteRequest.getTraceContext().getConversationId() != null) { + completeAgentContextVariable(agentContext, pluginExecuteRequest.getTraceContext().getConversationId()); + } + PluginContext pluginContext = PluginContext.builder() .requestId(pluginExecuteRequest.getRequestId()) .pluginConfig((PluginConfigDto) pluginDto.getConfig()) @@ -1190,6 +1200,22 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm }).doOnError(emitter::error).subscribe()); } + private void completeAgentContextVariable(AgentContext agentContext, String conversationId) { + try { + ConversationDto conversation = conversationApplicationService.getConversationByCid(Long.parseLong(conversationId)); + if (conversation != null && conversation.getVariables() != null) { + conversation.getVariables().forEach((key, value) -> { + if (!agentContext.getVariableParams().containsKey(key)) { + agentContext.getVariableParams().put(key, value); + } + }); + } + log.debug("completeAgentContextVariable conversationId: {}, variables: {}", conversationId, agentContext.getVariableParams()); + } catch (Exception e) { + log.error("completeAgentContextVariable error", e); + } + } + private void completeArgReferenceBindValue(AgentContext agentContext, List inputArgs, Map params) { if (inputArgs == null || params == null) { return; @@ -1223,7 +1249,8 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm public Flux executeWorkflow(WorkflowExecuteRequestDto workflowExecuteRequest) { AtomicReference disposable = new AtomicReference<>(); Flux flux = Flux.create(emitter -> { - WorkflowConfigDto workflowConfigDto = (WorkflowConfigDto) JsonSerializeUtil.parseObjectGeneric(workflowExecuteRequest.getConfig()); + WorkflowConfigDto workflowConfigDto = (workflowExecuteRequest.getConfig() instanceof WorkflowConfigDto) ? + (WorkflowConfigDto) workflowExecuteRequest.getConfig() : (WorkflowConfigDto) JsonSerializeUtil.parseObjectGeneric(workflowExecuteRequest.getConfig().toString()); if (workflowConfigDto == null) { emitter.error(BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentWorkflowNotFoundSimple)); return; @@ -1246,9 +1273,14 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm emitter.next(workflowExecutingDto); }); + if (workflowExecuteRequest.getTraceContext() != null && workflowExecuteRequest.getTraceContext().getConversationId() != null) { + completeAgentContextVariable(agentContext, workflowExecuteRequest.getTraceContext().getConversationId()); + } + //For common agent, when user uses workflow, plugin, set parameter reference (temporarily only support system variables, user custom variables in agent cannot be passed) if (workflowExecuteRequest.getBindConfig() != null) { - WorkflowBindConfigDto workflowBindConfigDto = (WorkflowBindConfigDto) JsonSerializeUtil.parseObjectGeneric(workflowExecuteRequest.getBindConfig()); + WorkflowBindConfigDto workflowBindConfigDto = workflowExecuteRequest.getBindConfig() instanceof WorkflowBindConfigDto ? + (WorkflowBindConfigDto) workflowExecuteRequest.getBindConfig() : (WorkflowBindConfigDto) JsonSerializeUtil.parseObjectGeneric(workflowExecuteRequest.getBindConfig().toString()); if (workflowBindConfigDto != null) { completeArgReferenceBindValue(agentContext, workflowBindConfigDto.getInputArgBindConfigs(), workflowExecuteRequest.getParams()); } @@ -1360,7 +1392,8 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm agentConfigDto.setName(pageAppAgentCreateDto.getName()); agentConfigDto.setDescription(pageAppAgentCreateDto.getDescription()); agentConfigDto.setIcon(pageAppAgentCreateDto.getIcon()); - agentConfigDto.setType("PageApp"); + agentConfigDto.setType(AgentConfigDto.AgentType.PageApp.name()); + agentConfigDto.setSubType(AgentConfigDto.AgentSubType.PageApp.name()); Long agentId = agentApplicationService.add(agentConfigDto); AgentComponentConfigDto agentComponentConfigDto = new AgentComponentConfigDto(); agentComponentConfigDto.setAgentId(agentId); @@ -1456,7 +1489,8 @@ public class AgentApiServiceImpl implements IAgentRpcService, TemplateExportOrIm agentConfigDto.setSpaceId(spaceDto.getId()); agentConfigDto.setName(StringUtils.isNotBlank(name) ? "Agent@" + name : I18nUtil.systemMessage("Backend.Agent.PrivateAgent.DefaultName", sandboxId.toString())); agentConfigDto.setDescription(I18nUtil.systemMessage("Backend.Agent.PrivateAgent.Description")); - agentConfigDto.setType("TaskAgent"); + agentConfigDto.setType(AgentConfigDto.AgentType.TaskAgent.name()); + agentConfigDto.setSubType(AgentConfigDto.AgentSubType.General.name()); agentConfigDto.setOpenLongMemory(AgentConfig.OpenStatus.Open); agentConfigDto.setAllowOtherModel(YesOrNoEnum.Y.getKey()); Map extra = new HashMap<>(); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/api/SkillRpcServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/api/SkillRpcServiceImpl.java new file mode 100644 index 00000000..048fea90 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/api/SkillRpcServiceImpl.java @@ -0,0 +1,25 @@ +package com.xspaceagi.agent.core.application.api; + +import com.xspaceagi.agent.core.adapter.application.SkillApplicationService; +import com.xspaceagi.agent.core.sdk.ISkillRpcService; +import com.xspaceagi.agent.core.spec.enums.UsageScenarioEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Slf4j +@Service +public class SkillRpcServiceImpl implements ISkillRpcService { + + @Resource + private SkillApplicationService skillApplicationService; + + @Override + public Long importSkill(String url, MultipartFile file, Long targetSkillId, Long targetSpaceId, List usageScenarios) { + return skillApplicationService.importSkill(url, file, targetSkillId, targetSpaceId, usageScenarios); + } + +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/AgentApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/AgentApplicationServiceImpl.java index 5c081f37..b56957dd 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/AgentApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/AgentApplicationServiceImpl.java @@ -13,7 +13,7 @@ import com.xspaceagi.agent.core.adapter.dto.config.plugin.CodePluginConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.plugin.HttpPluginConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto; -import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.*; import com.xspaceagi.agent.core.adapter.repository.CopyIndexRecordRepository; import com.xspaceagi.agent.core.adapter.repository.entity.*; import com.xspaceagi.agent.core.domain.service.AgentDomainService; @@ -30,9 +30,13 @@ import com.xspaceagi.agent.core.infra.rpc.*; import com.xspaceagi.agent.core.infra.rpc.dto.PageDto; import com.xspaceagi.agent.core.spec.enums.*; import com.xspaceagi.agent.core.spec.utils.PlaceholderParser; +import com.xspaceagi.agent.core.spec.utils.SvgIconUtil; +import com.xspaceagi.agent.core.spec.utils.ZipStringUtils; import com.xspaceagi.compose.sdk.request.DorisTableDefineRequest; import com.xspaceagi.compose.sdk.service.IComposeDbTableRpcService; import com.xspaceagi.compose.sdk.vo.define.TableDefineVo; +import com.xspaceagi.file.application.service.FileManagementService; +import com.xspaceagi.file.domain.model.FileRecordDomain; import com.xspaceagi.knowledge.sdk.request.KnowledgeCreateRequestVo; import com.xspaceagi.knowledge.sdk.response.KnowledgeConfigVo; import com.xspaceagi.mcp.sdk.dto.McpComponentDto; @@ -67,6 +71,7 @@ import com.xspaceagi.system.spec.enums.PermissionSubjectTypeEnum; import com.xspaceagi.system.spec.enums.YesOrNoEnum; import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; +import com.xspaceagi.system.spec.file.InMemoryMultipartFile; import com.xspaceagi.system.spec.jackson.JsonSerializeUtil; import com.xspaceagi.system.spec.utils.I18nUtil; import com.xspaceagi.system.spec.utils.RedisUtil; @@ -75,9 +80,11 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Service; import org.springframework.util.Assert; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.*; import java.util.stream.Collectors; @@ -162,6 +169,9 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { @Resource private ResourcePricingRpcService resourcePricingRpcService; + @Resource + private FileManagementService fileManagementService; + @Override @DSTransactional public Long add(AgentConfigDto agent) { @@ -210,6 +220,23 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { .bindConfig(modelBindConfigDto).build(); agentDomainService.addAgentComponentConfig(agentComponentConfig); + //AgentFlow初始化工作流 + if (agent.getSubType() != null && agent.getSubType().equals(AgentConfigDto.AgentSubType.Flow.name())) { + WorkflowConfigDto workflowConfigDto = new WorkflowConfigDto(); + workflowConfigDto.setName(agent.getName()); + workflowConfigDto.setDescription(agent.getDescription()); + workflowConfigDto.setCreatorId(agentConfig.getCreatorId()); + workflowConfigDto.setSpaceId(agent.getSpaceId()); + workflowConfigDto.setType(WorkflowConfigDto.Type.AgentFlow.name()); + workflowConfigDto.setAgentId(agentConfig.getId()); + Long workflowId = workflowApplicationService.add(workflowConfigDto); + AgentComponentConfig workflowComponentConfig = AgentComponentConfig.builder().agentId(agentConfig.getId()) + .type(AgentComponentConfig.Type.Workflow).targetId(workflowId) + .name(workflowConfigDto.getName()).description(workflowConfigDto.getDescription()) + .bindConfig(new WorkflowBindConfigDto()).build(); + agentDomainService.addAgentComponentConfig(workflowComponentConfig); + } + // 添加智能体新增历史记录 addConfigHistory(agentConfig.getId(), ConfigHistory.Type.Add, I18nUtil.systemMessage("Agent.ConfigHistory.Add")); @@ -512,6 +539,21 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { if (agentConfig.getExtra() == null) { agentConfigDto.setExtra(new HashMap<>()); } + if (agentConfigDto.getSubType() == null) { + if (AgentConfigDto.AgentType.ChatBot.name().equals(agentConfigDto.getType())) { + agentConfigDto.setSubType(AgentConfigDto.AgentSubType.ChatBot.name()); + } + if (AgentConfigDto.AgentType.PageApp.name().equals(agentConfigDto.getType())) { + agentConfigDto.setSubType(AgentConfigDto.AgentSubType.PageApp.name()); + } + if (AgentConfigDto.AgentType.TaskAgent.name().equals(agentConfigDto.getType())) { + if (agentConfigDto.getDevAgentConversationId() != null) { + agentConfigDto.setSubType(AgentConfigDto.AgentSubType.Custom.name()); + } else { + agentConfigDto.setSubType(AgentConfigDto.AgentSubType.General.name()); + } + } + } return agentConfigDto; } @@ -566,6 +608,41 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { if (!modelComponentConfigList.isEmpty()) { agentConfigDto.setModelComponentConfig(modelComponentConfigList.get(0)); } + if (AgentConfigDto.AgentSubType.Flow.name().equals(agentConfigDto.getSubType())) { + AgentComponentConfigDto componentConfigDto = agentConfigDto.getAgentComponentConfigList().stream().filter(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.Workflow).findFirst().orElse(null); + if (componentConfigDto != null) { + WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryById(componentConfigDto.getTargetId()); + Assert.notNull(workflowConfigDto, "AgentFlow configuration is incomplete"); + List workflowNodes = forExecute ? workflowApplicationService.queryWorkflowNodeListForTestExecute(workflowConfigDto.getId()) : workflowApplicationService.queryWorkflowNodeList(workflowConfigDto.getId()); + workflowConfigDto.setNodes(workflowNodes); + componentConfigDto.setTargetConfig(workflowConfigDto); + } + } + completeCreatorAndSpaceInfo(List.of(agentConfigDto)); + PublishedDto published = publishApplicationService.queryPublished(Published.TargetType.Agent, agentId); + if (published != null) { + agentConfigDto.setPublishDate(published.getModified()); + agentConfigDto.setCategory(published.getCategory()); + } + return agentConfigDto; + } + + return null; + } + + private AgentConfigDto queryByIdForDetail(Long agentId) { + AgentConfig agentConfig = agentDomainService.queryById(agentId); + if (agentConfig != null) { + AgentConfigDto agentConfigDto = convertToDto(agentConfig); + agentConfigDto.setAgentComponentConfigList(queryComponentConfigList(agentId, false)); + // 从agentConfigDto.getAgentComponentConfigList()中获取类型为Model的组件列表 + List modelComponentConfigList = agentConfigDto.getAgentComponentConfigList() + .stream() + .filter(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.Model) + .toList(); + if (!modelComponentConfigList.isEmpty()) { + agentConfigDto.setModelComponentConfig(modelComponentConfigList.get(0)); + } completeCreatorAndSpaceInfo(List.of(agentConfigDto)); PublishedDto published = publishApplicationService.queryPublished(Published.TargetType.Agent, agentId); if (published != null) { @@ -749,6 +826,12 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { return queryComponentConfigList(agentId, false); } + @Override + public List queryComponentConfigListByAgentIdAndType(Long agentId, AgentComponentConfig.Type type) { + List agentComponentConfigs = agentDomainService.queryComponentConfigsByAgentIdAndType(agentId, type); + return convertComponentConfigList(agentId, agentComponentConfigs, false); + } + private List queryComponentConfigList(Long agentId, boolean forExecute) { List agentComponentConfigList = agentDomainService.queryAgentComponentConfigList(agentId); AgentComponentConfig varConfig = agentComponentConfigList.stream().filter(agentComponentConfig -> agentComponentConfig.getType() == AgentComponentConfig.Type.Variable).findFirst().orElse(null); @@ -763,6 +846,21 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { agentComponentConfigList.add(varConfig); } + AgentComponentConfig hookConfig = agentComponentConfigList.stream().filter(agentComponentConfig -> agentComponentConfig.getType() == AgentComponentConfig.Type.Hook).findFirst().orElse(null); + if (hookConfig == null) { + //初始化Hook配置 + HookConfigDto hookBindConfigDto = new HookConfigDto(); + hookBindConfigDto.setHooks(new ArrayList<>()); + AgentComponentConfig hookComponentConfig = AgentComponentConfig.builder().agentId(agentId) + .type(AgentComponentConfig.Type.Hook) + .targetId(-1L) + .name("Hook").description("Hook Binding") + .bindConfig(hookBindConfigDto).build(); + agentComponentConfigList.add(hookComponentConfig); + agentDomainService.addAgentComponentConfig(hookComponentConfig); + + } + AgentComponentConfig eventConfig = agentComponentConfigList.stream().filter(agentComponentConfig -> agentComponentConfig.getType() == AgentComponentConfig.Type.Event).findFirst().orElse(null); if (eventConfig == null) { //初始化事件配置 @@ -788,7 +886,10 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { .bindConfig(subAgentBindConfigDto).build(); agentDomainService.addAgentComponentConfig(subAgentConfig); } + return convertComponentConfigList(agentId, agentComponentConfigList, forExecute); + } + private List convertComponentConfigList(Long agentId, List agentComponentConfigList, boolean forExecute) { AgentConfig agentConfig = agentDomainService.queryById(agentId); List agentComponentConfigDtoList = agentComponentConfigList.stream().map(agentComponentConfig -> { AgentComponentConfigDto agentComponentConfigDto = AgentComponentConfigDto.builder().build(); @@ -808,6 +909,19 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { } public AgentConfigDto queryPublishedConfig(Long agentId, boolean execute) { + String cacheKey = "agent_config:" + (execute ? "execute_" : "") + agentId; + Object val = redisUtil.get(cacheKey); + try { + if (val != null) { + String config = ZipStringUtils.decompressFromBase64(val.toString()); + AgentConfigCacheStatusVo agentConfigCacheStatusVo = (AgentConfigCacheStatusVo) JsonSerializeUtil.parseObjectGeneric(config); + if (!checkIfCacheExpired(agentConfigCacheStatusVo)) { + return agentConfigCacheStatusVo.getAgentConfig(); + } + } + } catch (Exception e) { + log.warn("读取Agent缓存失败:{}", cacheKey, e); + } PublishedDto agentPublished = publishApplicationService.queryPublished(Published.TargetType.Agent, agentId); if (agentPublished == null) { return null; @@ -816,6 +930,7 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { AgentConfig agentConfig = new AgentConfig(); BeanUtils.copyProperties(agentConfigDto, agentConfig); convertComponentConfig(agentConfigDto.getModelComponentConfig()); + agentConfigDto.setPublishedSpaceIds(agentPublished.getPublishedSpaceIds()); agentConfigDto.getAgentComponentConfigList().forEach(componentConfigDto -> componentConfigDto.setSpaceId(agentConfigDto.getSpaceId())); agentConfigDto.setAgentComponentConfigList(completeAgentComponentConfig(agentConfig, agentConfigDto.getAgentComponentConfigList(), execute)); AgentComponentConfigDto modelComponentConfig = agentConfigDto.getAgentComponentConfigList() @@ -830,22 +945,184 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { if (tenantConfig.getDefaultAgentId() != null && agentId.longValue() == tenantConfig.getDefaultAgentId() && CollectionUtils.isNotEmpty(tenantConfig.getDefaultAgentIds())) { //排除自身 tenantConfig.getDefaultAgentIds().remove(agentId); - List publishedList = publishApplicationService.queryPublishedList(Published.TargetType.Agent, tenantConfig.getDefaultAgentIds()); - publishedList.forEach(publishedDto -> { + for (Long defaultAgentId : tenantConfig.getDefaultAgentIds()) { + AgentConfigDto defaultAgentConfigDto = queryPublishedConfig(defaultAgentId, execute); AgentComponentConfigDto agentComponentConfigDto = new AgentComponentConfigDto(); - agentComponentConfigDto.setAgentId(agentId); - agentComponentConfigDto.setTargetId(publishedDto.getTargetId()); - agentComponentConfigDto.setName(publishedDto.getName()); - agentComponentConfigDto.setDescription(publishedDto.getDescription()); - agentComponentConfigDto.setType(AgentComponentConfig.Type.Agent); agentConfigDto.getAgentComponentConfigList().add(agentComponentConfigDto); - }); + agentComponentConfigDto.setAgentId(agentId); + agentComponentConfigDto.setTargetId(defaultAgentId); + agentComponentConfigDto.setName(defaultAgentConfigDto.getName()); + agentComponentConfigDto.setDescription(defaultAgentConfigDto.getDescription()); + agentComponentConfigDto.setType(AgentComponentConfig.Type.Agent); + agentComponentConfigDto.setTargetConfig(defaultAgentConfigDto); + } } agentConfigDto.setPublishDate(agentPublished.getModified()); agentConfigDto.setAccessControl(agentPublished.getAccessControl()); + + // 缓存 + try { + AgentConfigCacheStatusVo agentConfigCacheStatusVo = new AgentConfigCacheStatusVo(); + agentConfigCacheStatusVo.setAgentConfig(agentConfigDto); + agentConfigCacheStatusVo.setComponents(new ArrayList<>()); + AgentConfigCacheStatusVo.AgentComponentCacheStatusVo agentComponentCacheStatusVo = new AgentConfigCacheStatusVo.AgentComponentCacheStatusVo(); + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Agent); + agentComponentCacheStatusVo.setTargetId(agentId); + agentComponentCacheStatusVo.setLastUpdated(agentPublished.getModified().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + writeAgentConfigToCache(agentConfigDto, agentConfigCacheStatusVo); + String jsonStringGeneric = JsonSerializeUtil.toJSONStringGeneric(agentConfigCacheStatusVo); + redisUtil.set(cacheKey, ZipStringUtils.compressToBase64(jsonStringGeneric)); + } catch (Exception e) { + // 忽略 + log.warn("cache agent_config error", e); + } return agentConfigDto; } + private boolean checkIfCacheExpired(AgentConfigCacheStatusVo agentConfigCacheStatusVo) { + //按照type分组 + List agentIds = agentConfigCacheStatusVo.getComponents().stream().filter(agentComponentCacheStatusVo -> agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Agent).map(AgentConfigCacheStatusVo.AgentComponentCacheStatusVo::getTargetId).toList(); + List publishedDtos = publishApplicationService.queryPublishedListWithoutConfig(Published.TargetType.Agent, agentIds, null); + Map agentMap = publishedDtos.stream().collect(Collectors.toMap(PublishedDto::getTargetId, publishedDto -> publishedDto, (p, p1) -> p)); + + List pluginIds = agentConfigCacheStatusVo.getComponents().stream().filter(agentComponentCacheStatusVo -> agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Plugin).map(AgentConfigCacheStatusVo.AgentComponentCacheStatusVo::getTargetId).toList(); + publishedDtos = publishApplicationService.queryPublishedListWithoutConfig(Published.TargetType.Plugin, pluginIds, null); + Map pluginMap = publishedDtos.stream().collect(Collectors.toMap(PublishedDto::getTargetId, publishedDto -> publishedDto, (p, p1) -> p)); + + List workflowIds = agentConfigCacheStatusVo.getComponents().stream().filter(agentComponentCacheStatusVo -> agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Workflow).map(AgentConfigCacheStatusVo.AgentComponentCacheStatusVo::getTargetId).toList(); + publishedDtos = publishApplicationService.queryPublishedListWithoutConfig(Published.TargetType.Workflow, workflowIds, null); + Map workflowMap = publishedDtos.stream().collect(Collectors.toMap(PublishedDto::getTargetId, publishedDto -> publishedDto, (p, p1) -> p)); + + List skillIds = agentConfigCacheStatusVo.getComponents().stream().filter(agentComponentCacheStatusVo -> agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Skill).map(AgentConfigCacheStatusVo.AgentComponentCacheStatusVo::getTargetId).toList(); + publishedDtos = publishApplicationService.queryPublishedListWithoutConfig(Published.TargetType.Skill, skillIds, null); + Map skillMap = publishedDtos.stream().collect(Collectors.toMap(PublishedDto::getTargetId, publishedDto -> publishedDto, (p, p1) -> p)); + + List modelIds = agentConfigCacheStatusVo.getComponents().stream().filter(agentComponentCacheStatusVo -> agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Model).map(AgentConfigCacheStatusVo.AgentComponentCacheStatusVo::getTargetId).toList(); + List modelConfigDtos = modelApplicationService.queryModelConfigListByIds(modelIds); + Map modelConfigDtoMap = modelConfigDtos.stream().collect(Collectors.toMap(ModelConfigDto::getId, modelConfigDto -> modelConfigDto, (modelConfigDto1, modelConfigDto2) -> modelConfigDto1)); + + for (AgentConfigCacheStatusVo.AgentComponentCacheStatusVo agentComponentCacheStatusVo : agentConfigCacheStatusVo.getComponents()) { + if (agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Agent && (!agentMap.containsKey(agentComponentCacheStatusVo.getTargetId()) || agentMap.get(agentComponentCacheStatusVo.getTargetId()).getModified().getTime() > agentComponentCacheStatusVo.getLastUpdated())) { + return true; + } + if (agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Plugin && (!pluginMap.containsKey(agentComponentCacheStatusVo.getTargetId()) || pluginMap.get(agentComponentCacheStatusVo.getTargetId()).getModified().getTime() > agentComponentCacheStatusVo.getLastUpdated())) { + return true; + } + if (agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Workflow && (!workflowMap.containsKey(agentComponentCacheStatusVo.getTargetId()) || workflowMap.get(agentComponentCacheStatusVo.getTargetId()).getModified().getTime() > agentComponentCacheStatusVo.getLastUpdated())) { + return true; + } + if (agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Skill && (!skillMap.containsKey(agentComponentCacheStatusVo.getTargetId()) || skillMap.get(agentComponentCacheStatusVo.getTargetId()).getModified().getTime() > agentComponentCacheStatusVo.getLastUpdated())) { + return true; + } + if (agentComponentCacheStatusVo.getTargetType() == Published.TargetType.Model && (!modelConfigDtoMap.containsKey(agentComponentCacheStatusVo.getTargetId()) || modelConfigDtoMap.get(agentComponentCacheStatusVo.getTargetId()).getModified().getTime() > agentComponentCacheStatusVo.getLastUpdated())) { + return true; + } + } + return false; + } + + private void writeAgentConfigToCache(AgentConfigDto agentConfigDto, AgentConfigCacheStatusVo agentConfigCacheStatusVo) { + agentConfigDto.getAgentComponentConfigList().forEach(agentComponentConfigDto -> { + AgentConfigCacheStatusVo.AgentComponentCacheStatusVo agentComponentCacheStatusVo = new AgentConfigCacheStatusVo.AgentComponentCacheStatusVo(); + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Agent); + agentComponentCacheStatusVo.setTargetId(agentComponentConfigDto.getTargetId()); + if (agentComponentConfigDto.getTargetConfig() instanceof AgentConfigDto agentConfigDto1 && agentConfigDto1.getPublishDate() != null) { + agentComponentCacheStatusVo.setLastUpdated(agentConfigDto1.getPublishDate().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + writeAgentConfigToCache(agentConfigDto1, agentConfigCacheStatusVo); + } + } + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Plugin) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Plugin); + agentComponentCacheStatusVo.setTargetId(agentComponentConfigDto.getTargetId()); + if (agentComponentConfigDto.getTargetConfig() instanceof PluginDto pluginDto && pluginDto.getPublishDate() != null) { + agentComponentCacheStatusVo.setLastUpdated(pluginDto.getPublishDate().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + } + } + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Workflow) { + if (agentComponentConfigDto.getTargetConfig() instanceof WorkflowConfigDto workflowConfigDto && workflowConfigDto.getPublishDate() != null) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Workflow); + agentComponentCacheStatusVo.setTargetId(agentComponentConfigDto.getTargetId()); + agentComponentCacheStatusVo.setLastUpdated(workflowConfigDto.getPublishDate().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + writeWorkflowConfigToCache(workflowConfigDto, agentConfigCacheStatusVo); + } + } + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Skill) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Skill); + agentComponentCacheStatusVo.setTargetId(agentComponentConfigDto.getTargetId()); + if (agentComponentConfigDto.getTargetConfig() instanceof SkillConfigDto skillDto && skillDto.getPublishDate() != null) { + agentComponentCacheStatusVo.setLastUpdated(skillDto.getPublishDate().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + } + } + + // 模型有可能配置发生变化 + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Model) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Model); + agentComponentCacheStatusVo.setTargetId(agentComponentConfigDto.getTargetId()); + if (agentComponentConfigDto.getTargetConfig() instanceof ModelConfigDto modelDto && modelDto.getModified() != null) { + agentComponentCacheStatusVo.setLastUpdated(modelDto.getModified().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + } + } + }); + } + + private void writeWorkflowConfigToCache(WorkflowConfigDto workflowConfigDto, AgentConfigCacheStatusVo agentConfigCacheStatusVo) { + if (CollectionUtils.isEmpty(workflowConfigDto.getNodes())) { + return; + } + workflowConfigDto.getNodes().forEach(workflowNodeDto -> { + AgentConfigCacheStatusVo.AgentComponentCacheStatusVo agentComponentCacheStatusVo = new AgentConfigCacheStatusVo.AgentComponentCacheStatusVo(); + if (workflowNodeDto.getType() == WorkflowNodeConfig.NodeType.Workflow && workflowNodeDto.getNodeConfig() instanceof WorkflowAsNodeConfigDto workflowAsNodeConfigDto) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Workflow); + agentComponentCacheStatusVo.setTargetId(workflowAsNodeConfigDto.getWorkflowId()); + if (workflowAsNodeConfigDto.getWorkflowConfig() != null && workflowAsNodeConfigDto.getWorkflowConfig().getPublishDate() != null) { + agentComponentCacheStatusVo.setLastUpdated(workflowAsNodeConfigDto.getWorkflowConfig().getPublishDate().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + writeWorkflowConfigToCache(workflowAsNodeConfigDto.getWorkflowConfig(), agentConfigCacheStatusVo); + } + } + if (workflowNodeDto.getType() == WorkflowNodeConfig.NodeType.Plugin && workflowNodeDto.getNodeConfig() instanceof PluginNodeConfigDto pluginNodeConfigDto) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Plugin); + agentComponentCacheStatusVo.setTargetId(pluginNodeConfigDto.getPluginId()); + if (pluginNodeConfigDto.getPluginConfig() != null && pluginNodeConfigDto.getPluginConfig().getPublishDate() != null) { + agentComponentCacheStatusVo.setLastUpdated(pluginNodeConfigDto.getPluginConfig().getPublishDate().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + } + } + if (workflowNodeDto.getType() == WorkflowNodeConfig.NodeType.Mcp && workflowNodeDto.getNodeConfig() instanceof McpNodeConfigDto mcpNodeConfigDto) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Mcp); + agentComponentCacheStatusVo.setTargetId(mcpNodeConfigDto.getMcpId()); + if (mcpNodeConfigDto.getMcp() != null && mcpNodeConfigDto.getMcp() instanceof McpDto mcpDto) { + agentComponentCacheStatusVo.setLastUpdated(mcpDto.getModified().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + } + } + if (workflowNodeDto.getType() == WorkflowNodeConfig.NodeType.Agent && workflowNodeDto.getNodeConfig() instanceof AgentNodeConfigDto agentNodeConfigDto) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Agent); + agentComponentCacheStatusVo.setTargetId(agentNodeConfigDto.getAgentId()); + if (agentNodeConfigDto.getAgentConfigDto() != null && agentNodeConfigDto.getAgentConfigDto().getPublishDate() != null) { + agentComponentCacheStatusVo.setLastUpdated(agentNodeConfigDto.getAgentConfigDto().getPublishDate().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + writeAgentConfigToCache(agentNodeConfigDto.getAgentConfigDto(), agentConfigCacheStatusVo); + } + } + if (workflowNodeDto.getType() == WorkflowNodeConfig.NodeType.LLM && workflowNodeDto.getNodeConfig() instanceof LLMNodeConfigDto llmNodeConfigDto) { + agentComponentCacheStatusVo.setTargetType(Published.TargetType.Model); + agentComponentCacheStatusVo.setTargetId(llmNodeConfigDto.getModelId()); + if (llmNodeConfigDto.getModelConfig() != null && llmNodeConfigDto.getModelConfig().getModified() != null) { + agentComponentCacheStatusVo.setLastUpdated(llmNodeConfigDto.getModelConfig().getModified().getTime()); + agentConfigCacheStatusVo.getComponents().add(agentComponentCacheStatusVo); + } + } + }); + } + private AgentConfigDto queryPublishedConfig(Long agentId) { PublishedDto agentPublished = publishApplicationService.queryPublished(Published.TargetType.Agent, agentId); if (agentPublished == null) { @@ -889,7 +1166,7 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { agentDetailDto.setAllowCopy(agentConfigDto.getAllowCopy()); } } else { - agentConfigDto = queryById(agentId); + agentConfigDto = queryByIdForDetail(agentId); } if (agentConfigDto == null) { return null; @@ -904,6 +1181,9 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { variables.addAll(bindConfig.getVariables().stream().filter(var -> !var.isSystemVariable() && var.getInputType() != null && var.getInputType() != Arg.InputTypeEnum.AutoRecognition).collect(Collectors.toList())); } } + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent) { + return true; + } if (agentComponentConfigDto.getBindConfig() != null) { if (agentComponentConfigDto.getBindConfig() instanceof PluginBindConfigDto pluginBindConfigDto) { return pluginBindConfigDto.getInvokeType() == PluginBindConfigDto.PluginInvokeTypeEnum.MANUAL || @@ -940,9 +1220,17 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { agentManualComponentDto.setName(skillBindConfigDto.getAlias()); } } + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent) { + agentManualComponentDto.setDefaultSelected(0); + } return agentManualComponentDto; }).collect(Collectors.toList()); + boolean isAgentGroup = agentConfigDto.getAgentComponentConfigList().stream().anyMatch(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent); + if (isAgentGroup) { + agentConfigDto.setType(AgentConfigDto.AgentType.TaskAgent.name()); + agentConfigDto.setSubType(AgentConfigDto.AgentSubType.Group.name()); + } if (agentConfigDto.getModelComponentConfig() != null) { ModelBindConfigDto bindConfig = (ModelBindConfigDto) agentConfigDto.getModelComponentConfig().getBindConfig(); if (bindConfig.getReasoningModelId() != null) { @@ -985,6 +1273,7 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { agentDetailDto.setPageHomeIndex(agentConfigDto.getPageHomeIndex()); agentDetailDto.setCustomPageMenus(agentConfigDto.getCustomPageMenus()); agentDetailDto.setType(agentConfigDto.getType()); + agentDetailDto.setSubType(agentConfigDto.getSubType()); Map variablesMap = new HashMap<>(); if (RequestContext.get() != null && RequestContext.get().getUser() != null) { UserDto userDto = (UserDto) RequestContext.get().getUser(); @@ -1002,10 +1291,7 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { Map pageComponentConfigMap = agentConfigDto.getAgentComponentConfigList().stream().filter(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.Page) .collect(Collectors.toMap(AgentComponentConfigDto::getTargetId, componentConfigDto -> componentConfigDto, (old, newConfig) -> newConfig)); if (CollectionUtils.isNotEmpty(agentConfigDto.getOpeningGuidQuestions())) { - agentDetailDto.setOpeningGuidQuestions(agentConfigDto.getOpeningGuidQuestions().stream().map(question -> { - String questionText = PlaceholderParser.resoleAndReplacePlaceholder(variablesMap, question); - return questionText; - }).collect(Collectors.toList())); + agentDetailDto.setOpeningGuidQuestions(agentConfigDto.getOpeningGuidQuestions().stream().map(question -> PlaceholderParser.resoleAndReplacePlaceholder(variablesMap, question)).collect(Collectors.toList())); List newOpeningGuidQuestions = new ArrayList<>(); agentDetailDto.setGuidQuestionDtos(agentDetailDto.getOpeningGuidQuestions().stream().map(question -> { GuidQuestionDto guidQuestionDto; @@ -1089,6 +1375,8 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { agentDetailDto.setAllowAtSkill(agentConfigDto.getAllowAtSkill() == null ? YesOrNoEnum.Y.getKey() : agentConfigDto.getAllowAtSkill()); agentDetailDto.setAllowOtherModel(agentConfigDto.getAllowOtherModel() == null ? YesOrNoEnum.Y.getKey() : agentConfigDto.getAllowOtherModel()); agentDetailDto.setAllowPrivateSandbox(agentConfigDto.getAllowPrivateSandbox() == null ? YesOrNoEnum.Y.getKey() : agentConfigDto.getAllowPrivateSandbox()); + agentDetailDto.setAllowChooseMode(agentConfigDto.getAllowChooseMode() == null ? YesOrNoEnum.N.getKey() : agentConfigDto.getAllowChooseMode()); + agentDetailDto.setEnableVersionControl(agentConfigDto.getEnableVersionControl() == null ? YesOrNoEnum.N.getKey() : agentConfigDto.getEnableVersionControl()); // 是否有权限 agentDetailDto.setHasPermission(true); @@ -1145,6 +1433,10 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { } } + if (agentConfigDto.getDevAgentConversationId() != null) { + agentDetailDto.setShowPublishBtn(true); + } + return agentDetailDto; } @@ -1316,6 +1608,7 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { agentComponentConfigDto.setDeleted(true); return; } + pluginDto.setPublishDate(publishedDto.getModified()); agentComponentConfigDto.setName(pluginDto.getName()); agentComponentConfigDto.setDescription(pluginDto.getDescription()); agentComponentConfigDto.setIcon(pluginDto.getIcon()); @@ -1491,6 +1784,14 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { } if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Workflow) { WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryPublishedWorkflowConfig(agentComponentConfigDto.getTargetId(), agentComponentConfigDto.getSpaceId(), forExecute); + if (workflowConfigDto == null && AgentConfigDto.AgentSubType.Flow.name().equals(agentConfig.getSubType())) { + workflowConfigDto = workflowApplicationService.queryByIdWithoutNodes(agentComponentConfigDto.getTargetId()); + Assert.notNull(workflowConfigDto, "AgentFlow configuration is incomplete"); + if (forExecute) { + List workflowNodes = workflowApplicationService.queryWorkflowNodeListForTestExecute(workflowConfigDto.getId()); + workflowConfigDto.setNodes(workflowNodes); + } + } agentComponentConfigDto.setDeleted(workflowConfigDto == null); if (workflowConfigDto != null) { agentComponentConfigDto.setName(workflowConfigDto.getName()); @@ -1659,6 +1960,19 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { if (agentComponentConfigDto.getType() != AgentComponentConfig.Type.Model && agentComponentConfigDto.getType() != AgentComponentConfig.Type.Variable) { agentComponentConfigDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(agentComponentConfigDto.getIcon(), agentComponentConfigDto.getName(), agentComponentConfigDto.getType().name())); } + + //Agent + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent) { + AgentConfigDto agentConfigDto = queryPublishedConfig(agentComponentConfigDto.getTargetId(), forExecute); + if (agentConfigDto != null) { + agentComponentConfigDto.setTargetConfig(agentConfigDto); + agentComponentConfigDto.setName(agentConfigDto.getName()); + agentComponentConfigDto.setDescription(agentConfigDto.getDescription()); + agentComponentConfigDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(agentConfigDto.getIcon(), agentConfigDto.getName(), agentComponentConfigDto.getType().name())); + } else { + agentComponentConfigDto.setDeleted(true); + } + } }); return agentComponentConfigList.stream().filter(agentComponentConfigDto -> !agentComponentConfigDto.isDeleted()).collect(Collectors.toList()); } @@ -2129,6 +2443,57 @@ public class AgentApplicationServiceImpl implements AgentApplicationService { return modelConfigDtos; } + @Override + public GenerateInfoResultDto generateInfo(GenerateInfoReqDto req) { + String sysPrompt = """ + You are a professional project branding assistant. Based on the user's requirement description, generate: + 1. A concise project name (no more than 20 characters) + 2. A one-line project description (no more than 100 characters) + 3. A minimal SVG icon that visually represents the project + + SVG requirements (must match the platform default logo size of 200x200 pixels): + - Root element must be: + - The icon must be full-bleed: the first element MUST be a background rect covering the entire canvas: + - The main icon shape (not background) must be large: roughly span x from 15 to 85 and y from 12 to 88 + - Do NOT add low-opacity decorative lines/bars that shrink the perceived icon area + - Main graphic elements must occupy most of the canvas; do NOT leave large empty margins + - Draw all shapes within the 0-100 coordinate space; do not use width/height/viewBox values other than above + - Use simple geometric shapes and clean lines + - Use a modern color palette (gradient or solid colors) + - Must be a valid, self-contained SVG with no external references + - Keep it simple and recognizable at small sizes + - The SVG should be visually appealing and professional + + Identify the language based on the user's description and respond in the same language for name and description. + """; + + GenerateInfoAiDto aiResult = modelApplicationService.call( + sysPrompt, req.getPrompt(), new ParameterizedTypeReference() { + }); + + String iconUrl = null; + if (StringUtils.isNotBlank(aiResult.getSvgIcon())) { + iconUrl = uploadSvgIcon(aiResult.getSvgIcon(), aiResult.getName() + ".svg"); + } + + return GenerateInfoResultDto.builder() + .name(aiResult.getName()) + .description(aiResult.getDescription()) + .iconUrl(iconUrl) + .build(); + } + + public String uploadSvgIcon(String svgIcon, String fileName) { + byte[] svgBytes = SvgIconUtil.normalize(svgIcon).getBytes(StandardCharsets.UTF_8); + InMemoryMultipartFile multipartFile = new InMemoryMultipartFile( + "file", fileName, "image/svg+xml", svgBytes); + Long tenantId = RequestContext.get().getTenantId(); + Long userId = RequestContext.get().getUserId(); + FileRecordDomain fileRecord = fileManagementService.uploadFile( + multipartFile, tenantId, userId, "icon", -1L, null, true); + return fileRecord.getFileUrl(); + } + private Map getAgentStatisticsMapByAgentIds(List agentIds) { List agentStatisticsList = publishDomainService .queryStatisticsCountList(Published.TargetType.Agent, agentIds); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/AgentWorkspaceApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/AgentWorkspaceApplicationServiceImpl.java index a6a71e3b..4461a124 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/AgentWorkspaceApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/AgentWorkspaceApplicationServiceImpl.java @@ -2,18 +2,33 @@ package com.xspaceagi.agent.core.application.service; import com.alibaba.fastjson2.JSON; import com.xspaceagi.agent.core.adapter.application.AgentWorkspaceApplicationService; +import com.xspaceagi.agent.core.adapter.application.AgentApplicationService; import com.xspaceagi.agent.core.adapter.application.PublishApplicationService; +import com.xspaceagi.agent.core.adapter.application.SkillApplicationService; import com.xspaceagi.agent.core.adapter.constant.SkillFileFormatConstants; import com.xspaceagi.agent.core.adapter.dto.*; +import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; +import com.xspaceagi.agent.core.adapter.repository.entity.AgentConfig; +import com.xspaceagi.agent.core.adapter.repository.entity.Conversation; import com.xspaceagi.agent.core.adapter.repository.entity.Published; import com.xspaceagi.agent.core.adapter.repository.entity.SkillConfig; +import com.xspaceagi.agent.core.domain.service.AgentDomainService; +import com.xspaceagi.agent.core.domain.service.ConversationDomainService; +import com.xspaceagi.agent.core.domain.service.PublishDomainService; import com.xspaceagi.agent.core.domain.service.SkillDomainService; -import com.xspaceagi.agent.core.infra.rpc.SkillFileClient; +import com.xspaceagi.agent.core.infra.rpc.ComputerFileClient; +import com.xspaceagi.agent.core.infra.rpc.GitRpcClient; +import com.xspaceagi.agent.core.infra.rpc.WorkspaceRpcClient; import com.xspaceagi.agent.core.adapter.util.SkillNameUtil; import com.xspaceagi.agent.core.spec.utils.FileTypeUtils; import com.xspaceagi.agent.core.spec.utils.MarkdownExtractUtil; +import com.xspaceagi.agent.core.spec.utils.UrlFile; +import com.xspaceagi.file.application.service.FileManagementService; +import com.xspaceagi.file.domain.model.FileRecordDomain; import com.xspaceagi.file.sdk.IFileAccessService; +import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.enums.ErrorCodeEnum; +import com.xspaceagi.system.spec.enums.YesOrNoEnum; import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import com.xspaceagi.system.spec.file.InMemoryMultipartFile; @@ -24,6 +39,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -34,17 +50,35 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; +import org.springframework.core.io.ClassPathResource; + @Slf4j @Service public class AgentWorkspaceApplicationServiceImpl implements AgentWorkspaceApplicationService { @Resource - private SkillFileClient skillFileClient; + private WorkspaceRpcClient workspaceRpcClient; @Resource private SkillDomainService skillDomainService; @Resource private PublishApplicationService publishApplicationService; @Resource + private PublishDomainService publishDomainService; + @Resource private IFileAccessService iFileAccessService; + @Resource + private ComputerFileClient computerFileClient; + @Resource + private FileManagementService fileManagementService; + @Resource + private AgentDomainService agentDomainService; + @Resource + private AgentApplicationService agentApplicationService; + @Resource + private ConversationDomainService conversationDomainService; + @Resource + private SkillApplicationService skillApplicationService; + @Resource + private GitRpcClient gitRpcClient; @Override public void createWorkspace(CreateWorkspaceDto createWorkspaceDto) { @@ -103,9 +137,18 @@ public class AgentWorkspaceApplicationServiceImpl implements AgentWorkspaceAppli zipFile = buildZip(toPushSkills, subagents); } + String mcpServersConfig = createWorkspaceDto.getMcpServersConfig() != null ? JSON.toJSONString(createWorkspaceDto.getMcpServersConfig()) : null; + String permissionsConfig = createWorkspaceDto.getPermissionsConfig() != null ? JSON.toJSONString(createWorkspaceDto.getPermissionsConfig()) : null; + String hooksConfig = createWorkspaceDto.getHooksConfig() != null ? JSON.toJSONString(createWorkspaceDto.getHooksConfig()) : null; + List hookScripts = createWorkspaceDto.getHookScripts(); + String hookScriptsJson = null; + if (CollectionUtils.isNotEmpty(hookScripts)) { + hookScriptsJson = JSON.toJSONString(hookScripts); + } + Map result = null; try { - result = skillFileClient.createWorkSpaceV2(userId, cId, zipFile, skillUrls); + result = workspaceRpcClient.createWorkSpaceV2(userId, cId, zipFile, skillUrls, mcpServersConfig, permissionsConfig, hooksConfig, hookScriptsJson); } catch (Exception e) { log.warn("[createWorkspace] userId={} cId={} 调用 createWorkSpaceV2 异常,准备回退到 createWorkSpace", userId, cId, e); } @@ -116,7 +159,7 @@ public class AgentWorkspaceApplicationServiceImpl implements AgentWorkspaceAppli log.warn("[createWorkspace] userId={} cId={} createWorkSpaceV2失败,准备回退。message={}", userId, cId, message); MultipartFile fallbackZipFile = buildZipWithSkillUrls(zipFile, skillUrls, false); - result = skillFileClient.createWorkSpace(userId, cId, fallbackZipFile); + result = workspaceRpcClient.createWorkSpace(userId, cId, fallbackZipFile); } if (result == null) { @@ -155,7 +198,7 @@ public class AgentWorkspaceApplicationServiceImpl implements AgentWorkspaceAppli Map result = null; try { - result = skillFileClient.pushSkillsToWorkspaceV2(userId, cId, baseZipFile, skillUrls); + result = workspaceRpcClient.pushSkillsToWorkspaceV2(userId, cId, baseZipFile, skillUrls); } catch (Exception e) { log.warn("[addSkills] userId={} cId={} 调用 pushSkillsToWorkspaceV2 异常,准备回退到 pushSkillsToWorkspace", userId, cId, e); } @@ -168,7 +211,7 @@ public class AgentWorkspaceApplicationServiceImpl implements AgentWorkspaceAppli if (zipFile == null) { throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentSkillZipPackFailed); } - result = skillFileClient.pushSkillsToWorkspace(userId, cId, zipFile); + result = workspaceRpcClient.pushSkillsToWorkspace(userId, cId, zipFile); } if (result == null) { @@ -185,6 +228,622 @@ public class AgentWorkspaceApplicationServiceImpl implements AgentWorkspaceAppli log.info("[addSkills] userId={} cId={} 动态增加技能完成", userId, cId); } + @Override + public void initProjectTemplate(InitProjectTemplateDto dto) { + Long userId = dto.getUserId(); + Long cId = dto.getCId(); + InitProjectTemplateDto.ProjectType projectType = dto.getProjectType(); + InitProjectTemplateDto.ProgrammingLanguage programmingLanguage = dto.getProgrammingLanguage(); + + log.info("[initProjectTemplate] userId={} cId={} projectType={} programmingLanguage={} 开始初始化项目模板", userId, cId, projectType, programmingLanguage); + + if (userId == null || cId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "userId/cId"); + } + if (projectType == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "projectType"); + } + if (projectType == InitProjectTemplateDto.ProjectType.AGENT && programmingLanguage == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "programmingLanguage"); + } + + MultipartFile templateFile = null; + + if (projectType == InitProjectTemplateDto.ProjectType.AGENT) { + String templateFileName; + switch (programmingLanguage) { + case PYTHON: + templateFileName = "deepagent-app-py.zip"; + break; + default: + templateFileName = "deepagents-flow-ts.zip"; + break; + } + + // 从 classpath 读取模板 zip 文件 + String classpathLocation = "templates/" + templateFileName; + byte[] zipBytes; + try { + ClassPathResource resource = new ClassPathResource(classpathLocation); + try (InputStream is = resource.getInputStream()) { + zipBytes = is.readAllBytes(); + } + } catch (IOException e) { + log.error("[initProjectTemplate] userId={} cId={} 读取模板文件失败, classpathLocation={}", userId, cId, classpathLocation, e); + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.validationFailedWithDetail, + "Template file not found: " + templateFileName); + } + templateFile = new InMemoryMultipartFile("file", templateFileName, "application/zip", zipBytes); + } else if (projectType == InitProjectTemplateDto.ProjectType.SKILL) { + templateFile = buildSkillTemplateZip(); + } + + boolean enableGit = resolveEnableGit(cId); + log.info("[initProjectTemplate] userId={} cId={} enableGit={}", userId, cId, enableGit); + + // 调用 file-server 接口 + Map result = workspaceRpcClient.initProjectTemplate(userId, cId, templateFile, enableGit); + + // 结果校验 + if (result == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentTemplateInitFailed, "result is null"); + } + Object successObj = result.get("success"); + if (successObj instanceof Boolean && !(Boolean) successObj) { + String message = result.getOrDefault("message", "初始化项目模板失败").toString(); + log.error("[initProjectTemplate] userId={} cId={} 初始化项目模板失败, message={}", userId, cId, message); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentTemplateInitFailed, message); + } + + log.info("[initProjectTemplate] userId={} cId={} 初始化项目模板成功", userId, cId); + } + + private boolean resolveEnableGit(Long cId) { + Conversation conversation = conversationDomainService.getConversation(cId); + if (conversation == null || conversation.getAgentId() == null) { + return false; + } + AgentConfigDto executingAgent; + if (YesOrNoEnum.Y.getKey().equals(conversation.getDevMode())) { + executingAgent = agentApplicationService.queryConfigForTestExecute(conversation.getAgentId()); + } else { + executingAgent = agentApplicationService.queryPublishedConfigForExecute(conversation.getAgentId()); + } + if (executingAgent == null) { + return false; + } + return YesOrNoEnum.Y.getKey().equals(executingAgent.getEnableVersionControl()); + } + + @Override + public void installProject(InstallProjectDto dto) { + Long userId = dto.getUserId(); + Long cId = dto.getCId(); + InitProjectTemplateDto.ProgrammingLanguage programmingLanguage = dto.getProgrammingLanguage(); + + log.info("[installProject] userId={} cId={} programmingLanguage={} 开始安装项目依赖", userId, cId, programmingLanguage); + + if (userId == null || cId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "userId/cId"); + } + if (programmingLanguage == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "programmingLanguage"); + } + + Map result = workspaceRpcClient.installProject(userId, cId, programmingLanguage.getValue()); + if (result == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.workspaceExecuteCommandFailed, "installProject result is null"); + } + Object successObj = result.get("success"); + if (successObj instanceof Boolean && !(Boolean) successObj) { + String message = String.valueOf(result.getOrDefault("message", "安装项目依赖失败")); + log.error("[installProject] userId={} cId={} 安装项目依赖失败, message={}", userId, cId, message); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.workspaceExecuteCommandFailed, message); + } + + log.info("[installProject] userId={} cId={} 安装项目依赖成功", userId, cId); + } + + @Override + public List packageAgent(PackageAgentDto dto) { + Long userId = dto.getUserId(); + Long cId = dto.getCId(); + Long agentId = dto.getAgentId(); + + log.info("[packageAgent] userId={} cId={} agentId={} programmingLanguage={} 开始打包Agent", userId, cId, agentId, dto.getProgrammingLanguage()); + + if (userId == null || cId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "userId/cId"); + } + if (agentId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "agentId"); + } + + // 1. 先 git add + commit 确保工作区改动已提交(仅版本控制开启时) + boolean enableGit = resolveEnableGit(cId); + if (enableGit) { + gitCommitAndPush(userId, cId); + } + + // 2. 获取 git commit hash + String gitCommit = enableGit ? getGitCommitHash(userId, cId) : null; + log.info("[packageAgent] userId={} cId={} agentId={} gitCommit={}", userId, cId, agentId, gitCommit); + + // 3. 查询 agent_config 中的已有版本记录,检查是否需要重新打包 + AgentConfig agentConfig = agentDomainService.queryById(agentId); + List existingVersions = agentConfig != null ? agentConfig.getPublishVersion() : null; + if (CollectionUtils.isNotEmpty(existingVersions)) { + AgentPublishVersionDto latestVersion = existingVersions.stream() + .filter(v -> Boolean.TRUE.equals(v.getLatest())) + .findFirst() + .orElse(null); + if (latestVersion != null && gitCommit != null && gitCommit.equals(latestVersion.getGitCommit())) { + // git commit 没有变化,不需要重新打包 + log.info("[packageAgent] userId={} cId={} agentId={} git commit未变化, 跳过打包, version={}", userId, cId, agentId, latestVersion.getVersion()); + return existingVersions; + } + } + + // 4. 生成递增版本号 + String version = getNextVersion(existingVersions); + log.info("[packageAgent] userId={} cId={} agentId={} newVersion={}", userId, cId, agentId, version); + + // 5. 调用 file-server 构建 agent 产物 + Map buildResult = workspaceRpcClient.buildAgentPackage(userId, cId, agentId, version); + if (buildResult == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.workspaceExecuteCommandFailed, "buildAgentPackage result is null"); + } + Object buildSuccessObj = buildResult.get("success"); + if (buildSuccessObj instanceof Boolean && !(Boolean) buildSuccessObj) { + String message = String.valueOf(buildResult.getOrDefault("message", "buildAgentPackage failed")); + log.error("[packageAgent] userId={} cId={} 构建产物失败, message={}", userId, cId, message); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.workspaceExecuteCommandFailed, message); + } + + // 6. 遍历产物列表,下载并上传 + Object artifactsObj = buildResult.get("artifacts"); + List packages = new ArrayList<>(); + + if (artifactsObj instanceof List) { + for (Object artifactObj : (List) artifactsObj) { + if (!(artifactObj instanceof Map)) { + continue; + } + @SuppressWarnings("unchecked") + Map artifact = (Map) artifactObj; + + String artifactPath = String.valueOf(artifact.getOrDefault("path", "")); + String fileName = String.valueOf(artifact.getOrDefault("fileName", "")); + String platform = String.valueOf(artifact.getOrDefault("platform", "")); + + if (StringUtils.isBlank(artifactPath) || StringUtils.isBlank(fileName)) { + continue; + } + if (StringUtils.isBlank(platform)) { + log.warn("[packageAgent] userId={} cId={} 无法从文件名提取platform, fileName={}", userId, cId, fileName); + continue; + } + + try { + String staticPrefix = "computer/static/" + userId + "/" + cId; + byte[] fileBytes = computerFileClient.getStaticFileAsBytes(cId, staticPrefix, artifactPath, "packageAgent:" + userId + ":" + cId); + + String lowerFileName = fileName.toLowerCase(); + String contentType = lowerFileName.endsWith(".zip") ? "application/zip" : "application/gzip"; + InMemoryMultipartFile multipartFile = new InMemoryMultipartFile("file", fileName, contentType, fileBytes); + Long tenantId = RequestContext.get() != null ? RequestContext.get().getTenantId() : null; + Long uploadUserId = RequestContext.get() != null ? RequestContext.get().getUserId() : null; + FileRecordDomain fileRecord = fileManagementService.uploadFile(multipartFile, tenantId, uploadUserId, "agent_package", agentId, artifactPath, true); + + String fileUrl = fileRecord.getFileUrl(); + packages.add(AgentPublishVersionDto.PackageArtifact.builder() + .platform(platform) + .url(fileUrl) + .build()); + + log.info("[packageAgent] userId={} cId={} 上传产物成功, fileName={}, platform={}, fileUrl={}", userId, cId, fileName, platform, fileUrl); + } catch (Exception e) { + log.error("[packageAgent] userId={} cId={} 处理产物失败, artifactPath={}", userId, cId, artifactPath, e); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentPackageFailed, "Failed to process artifact: " + artifactPath); + } + } + } + + if (packages.isEmpty()) { + log.error("[packageAgent] userId={} cId={} 打包产物处理失败,无有效产物", userId, cId); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentPackageFailed, "No valid package artifacts found"); + } + + // 7. 清理沙箱打包产物 + try { + workspaceRpcClient.cleanupBuildArtifacts(userId, cId); + } catch (Exception e) { + log.warn("[packageAgent] userId={} cId={} 清理打包产物目录失败, 不影响发布流程", userId, cId, e); + } + + // 8. 保存版本记录到 agent_config.publishVersion + List updatedVersions = saveVersionToAgentConfig(agentId, existingVersions, version, gitCommit, packages); + + log.info("[packageAgent] userId={} cId={} agentId={} 打包完成, version={}, gitCommit={}, packages={}", userId, cId, agentId, version, gitCommit, packages.size()); + return updatedVersions; + } + + @Override + public List snapshotSkillFilesFromSandbox(Long userId, Long cId, String targetType, Long skillId) { + if (userId == null || cId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "userId/cId"); + } + + // 1. 获取技能 + byte[] zipBytes = zipAndDownloadFromSandbox(userId, cId); + + // 2. 解析 zip,逐个文件上传到文件服务 + List results = new ArrayList<>(); + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String entryName = entry.getName(); + if (entry.isDirectory()) { + SkillFileDto dirDto = new SkillFileDto(); + dirDto.setName(entryName); + dirDto.setIsDir(true); + results.add(dirDto); + continue; + } + // 跳过隐藏文件和无关文件 + if (entryName.startsWith(".") || entryName.contains("/__pycache__/") || entryName.contains("/node_modules/")) { + continue; + } + + byte[] fileBytes = zis.readAllBytes(); + String fileName = entryName; + int lastSlash = entryName.lastIndexOf('/'); + if (lastSlash >= 0 && lastSlash < entryName.length() - 1) { + fileName = entryName.substring(lastSlash + 1); + } + String contentType = FileTypeUtils.getContentTypeByFileName(fileName).toString(); + InMemoryMultipartFile multipartFile = new InMemoryMultipartFile("file", fileName, contentType, fileBytes); + Long tenantId = RequestContext.get() != null ? RequestContext.get().getTenantId() : null; + Long uploadUserId = RequestContext.get() != null ? RequestContext.get().getUserId() : null; + FileRecordDomain fileRecord = fileManagementService.uploadFile(multipartFile, tenantId, uploadUserId, targetType, skillId, entryName, true); + + SkillFileDto fileDto = new SkillFileDto(); + fileDto.setName(entryName); + fileDto.setFileProxyUrl(fileRecord.getFileUrl()); + results.add(fileDto); + } + } catch (IOException e) { + log.error("[snapshotSkillFilesFromSandbox] 解析zip失败, userId={}, cId={}", userId, cId, e); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentSkillZipPackFailed, + "Failed to parse skill zip"); + } + + log.info("[snapshotSkillFilesFromSandbox] userId={} cId={} skillId={} 从沙箱取回技能文件完成, fileCount={}", userId, cId, skillId, results.size()); + return results; + } + + @Override + public void deleteWorkspace(Long userId, Long cId) { + if (userId == null || cId == null) { + return; + } + try { + Map result = workspaceRpcClient.deleteWorkspace(userId, cId); + if (result != null) { + Object successObj = result.get("success"); + if (successObj instanceof Boolean && !(Boolean) successObj) { + log.warn("[deleteWorkspace] userId={} cId={} 删除工作空间失败, result={}", userId, cId, result); + } else { + log.info("[deleteWorkspace] userId={} cId={} 删除工作空间成功", userId, cId); + } + } + } catch (Exception e) { + log.warn("[deleteWorkspace] userId={} cId={} 删除工作空间异常", userId, cId, e); + } + } + + @Override + public SkillExportResultDto exportSkillFromSandbox(Long userId, Long cId, String skillName) { + if (userId == null || cId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "userId/cId"); + } + + // 1. 获取技能 + byte[] zipBytes = zipAndDownloadFromSandbox(userId, cId); + + // 2. 构建返回结果 + String exportFileName = (StringUtils.isNotBlank(skillName) ? skillName : "skill") + ".zip"; + SkillExportResultDto resultDto = new SkillExportResultDto(); + resultDto.setFileName(exportFileName); + resultDto.setData(zipBytes); + resultDto.setContentType("application/zip"); + + log.info("[exportSkillFromSandbox] userId={} cId={} skillName={} 导出完成, zipSize={}", userId, cId, skillName, zipBytes.length); + return resultDto; + } + + @Override + public void copySandboxWorkspace(Long srcUserId, Long srcCId, Long destUserId, Long destCId) { + if (srcUserId == null || srcCId == null || destUserId == null || destCId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "srcUserId/srcCId/destUserId/destCId"); + } + + log.info("[copySandboxWorkspace] srcUserId={} srcCId={} destUserId={} destCId={} 开始复制工作空间", srcUserId, srcCId, destUserId, destCId); + + // 1. 打包下载 + byte[] zipBytes = zipAndDownloadFromSandbox(srcUserId, srcCId); + + // 2. 使用 initProjectTemplate 将 zip 推送到新工作空间,按版本开关决定是否初始化 git + InMemoryMultipartFile zipFile = new InMemoryMultipartFile("file", "skill-copy.zip", "application/zip", zipBytes); + boolean enableGit = resolveEnableGit(destCId); + Map initResult = workspaceRpcClient.initProjectTemplate(destUserId, destCId, zipFile, enableGit); + + if (initResult == null) { + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentTemplateInitFailed, "initProjectTemplate result is null"); + } + Object successObj = initResult.get("success"); + if (successObj instanceof Boolean && !(Boolean) successObj) { + String message = String.valueOf(initResult.getOrDefault("message", "initProjectTemplate failed")); + log.error("[copySandboxWorkspace] destUserId={} destCId={} initProjectTemplate失败, message={}", destUserId, destCId, message); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentTemplateInitFailed, message); + } + + log.info("[copySandboxWorkspace] srcUserId={} srcCId={} destUserId={} destCId={} 复制工作空间完成", srcUserId, srcCId, destUserId, destCId); + } + + @Override + public void uploadZipToWorkspace(Long userId, Long cId, String zipFileUrl) { + if (userId == null || cId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "userId/cId"); + } + if (StringUtils.isBlank(zipFileUrl)) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "zipFileUrl"); + } + + log.info("[uploadZipToWorkspace] userId={} cId={} 开始上传 zip 到工作空间", userId, cId); + + // 1. 下载 zip 字节(UrlFile 内部会生成签名下载地址,并兼容 S3 presigned URL) + byte[] zipBytes; + try { + zipBytes = UrlFile.urlToBytes(zipFileUrl); + } catch (Exception e) { + log.error("[uploadZipToWorkspace] userId={} cId={} 下载 zip 失败, zipFileUrl={}", userId, cId, zipFileUrl, e); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentTemplateInitFailed, "Failed to download zip: " + e.getMessage()); + } + if (zipBytes == null || zipBytes.length == 0) { + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentTemplateInitFailed, "Downloaded zip is empty"); + } + + // 2. 如果压缩包只有一个顶层目录,去掉顶层目录后再上传 + zipBytes = stripTopLevelDirIfNeeded(zipBytes); + + // 3. 包装为 MultipartFile 并推送到工作空间,按版本开关决定是否初始化 git + InMemoryMultipartFile zipFile = new InMemoryMultipartFile("file", "skill.zip", "application/zip", zipBytes); + boolean enableGit = resolveEnableGit(cId); + Map initResult = workspaceRpcClient.initProjectTemplate(userId, cId, zipFile, enableGit); + + if (initResult == null) { + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentTemplateInitFailed, "initProjectTemplate result is null"); + } + Object successObj = initResult.get("success"); + if (successObj instanceof Boolean && !(Boolean) successObj) { + String message = String.valueOf(initResult.getOrDefault("message", "initProjectTemplate failed")); + log.error("[uploadZipToWorkspace] userId={} cId={} initProjectTemplate失败, message={}", userId, cId, message); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentTemplateInitFailed, message); + } + + log.info("[uploadZipToWorkspace] userId={} cId={} 上传 zip 到工作空间完成", userId, cId); + } + + // + // ------------------ 以下是private方法 ----------------------- + // + + /** + * 调用 file-server 的 zip-workspace 接口打包工作空间文件并下载 zip 字节 + * + * @param userId 用户ID + * @param cId 会话ID + * @return zip 文件的字节数组 + */ + private byte[] zipAndDownloadFromSandbox(Long userId, Long cId) { + byte[] zipBytes = workspaceRpcClient.zipWorkspace(userId, cId, null); + if (zipBytes == null || zipBytes.length == 0) { + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.workspaceExecuteCommandFailed, + "Failed to zip files in sandbox: empty response"); + } + log.info("userId={} cId={} 沙箱打包完成, zipSize={}", userId, cId, zipBytes.length); + return zipBytes; + } + + /** + * 如果 zip 只有一个顶层目录,去掉该顶层目录前缀后重新打包;否则原样返回。 + * 例如 entry 为 "my-skill/SKILL.md" 时会去掉 "my-skill/" 前缀。 + */ + private byte[] stripTopLevelDirIfNeeded(byte[] zipBytes) { + // 第一次扫描:判断是否所有 entry 都在同一个顶层目录下 + String topLevelDir = null; + boolean singleTopLevel = true; + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes), StandardCharsets.UTF_8)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String name = normalizeZipPath(entry.getName()); + int slash = name.indexOf('/'); + if (slash < 0) { + // 根目录下存在文件,不存在单一顶层目录 + singleTopLevel = false; + break; + } + String top = name.substring(0, slash); + if (topLevelDir == null) { + topLevelDir = top; + } else if (!topLevelDir.equals(top)) { + singleTopLevel = false; + break; + } + zis.closeEntry(); + } + } catch (IOException e) { + log.warn("[stripTopLevelDir] 解析zip判断顶层目录失败, 原样返回", e); + return zipBytes; + } + + if (!singleTopLevel || topLevelDir == null) { + return zipBytes; + } + + final String prefix = topLevelDir + "/"; + // 第二次扫描:去掉顶层目录前缀后重新打包 + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) { + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes), StandardCharsets.UTF_8)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String name = normalizeZipPath(entry.getName()); + if (name.startsWith(prefix)) { + name = name.substring(prefix.length()); + } + if (StringUtils.isBlank(name)) { + // 去掉前缀后为空(即顶层目录 entry 本身),跳过 + zis.closeEntry(); + continue; + } + ZipEntry newEntry = new ZipEntry(name); + zos.putNextEntry(newEntry); + if (!entry.isDirectory()) { + zos.write(zis.readAllBytes()); + } + zos.closeEntry(); + zis.closeEntry(); + } + } + zos.finish(); + log.info("[stripTopLevelDir] 去掉顶层目录 '{}' 后重新打包完成", topLevelDir); + return baos.toByteArray(); + } catch (IOException e) { + log.warn("[stripTopLevelDir] 重新打包失败, 原样返回", e); + return zipBytes; + } + } + + /** + * 执行 git add + commit,确保所有改动已提交 + */ + private void gitCommitAndPush(Long userId, Long cId) { + try { + gitRpcClient.commitTaskAgent(cId, userId, "auto commit before package", null, null, null); + } catch (Exception e) { + log.warn("[packageAgent] git commit失败, userId={}, cId={}", userId, cId, e); + } + } + + /** + * 获取沙箱工作空间中的 git commit hash(HEAD) + */ + private String getGitCommitHash(Long userId, Long cId) { + try { + Map result = gitRpcClient.logTaskAgent(cId, userId, 1, 1, null, null); + if (result != null) { + Object successObj = result.get("success"); + if (successObj instanceof Boolean && (Boolean) successObj) { + Object commitsObj = result.get("commits"); + if (commitsObj instanceof List && !((List) commitsObj).isEmpty()) { + Object firstCommit = ((List) commitsObj).get(0); + if (firstCommit instanceof Map) { + String hash = String.valueOf(((Map) firstCommit).get("hash")); + if (hash.length() >= 7) { + return hash; + } + } + } + } + } + } catch (Exception e) { + log.warn("[packageAgent] 获取git commit hash失败, userId={}, cId={}", userId, cId, e); + } + return null; + } + + /** + * 根据 agent_config 中的已有版本记录获取下一个版本号 + */ + private String getNextVersion(List existingVersions) { + if (CollectionUtils.isNotEmpty(existingVersions)) { + String latest = existingVersions.stream() + .filter(v -> Boolean.TRUE.equals(v.getLatest())) + .findFirst() + .map(AgentPublishVersionDto::getVersion) + .orElse(null); + if (latest != null) { + return incrementVersion(latest); + } + // 没有 latest 标记,取最后一个版本 + String last = existingVersions.get(existingVersions.size() - 1).getVersion(); + if (last != null) { + return incrementVersion(last); + } + } + return "1.0.0"; + } + + /** + * 保存版本记录到 agent_config:将所有旧版本 latest=false,追加新版本 latest=true + * @return 最新的完整版本列表 + */ + private List saveVersionToAgentConfig(Long agentId, List existingVersions, + String version, String gitCommit, List packages) { + try { + List updatedVersions = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(existingVersions)) { + // 所有旧版本标记为 latest=false + existingVersions.forEach(v -> v.setLatest(false)); + updatedVersions.addAll(existingVersions); + } + // 追加新版本 + updatedVersions.add(AgentPublishVersionDto.builder() + .version(version) + .gitCommit(gitCommit) + .latest(true) + .packages(packages) + .build()); + + AgentConfig updateConfig = new AgentConfig(); + updateConfig.setId(agentId); + updateConfig.setPublishVersion(updatedVersions); + agentDomainService.update(updateConfig); + return updatedVersions; + } catch (Exception e) { + log.error("[packageAgent] 保存版本记录到agent_config失败, agentId={}, version={}", agentId, version, e); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentPackageFailed, "Failed to save version to agent config"); + } + } + + /** + * 十进制递增版本号:x.y.z 每位 0-9,满十进一 + * 1.0.9 → 1.1.0, 1.9.9 → 2.0.0 + */ + private String incrementVersion(String version) { + try { + String[] parts = version.split("\\."); + if (parts.length == 3) { + int major = Integer.parseInt(parts[0]); + int minor = Integer.parseInt(parts[1]); + int patch = Integer.parseInt(parts[2]) + 1; + if (patch >= 10) { + patch = 0; + minor++; + } + if (minor >= 10) { + minor = 0; + major++; + } + return major + "." + minor + "." + patch; + } + } catch (NumberFormatException ignored) { + } + return "1.0.0"; + } + /** * 构建动态增加技能的锁标志文件 * 写入技能文件夹根目录下,用于标识该技能为动态增加 @@ -219,9 +878,47 @@ public class AgentWorkspaceApplicationServiceImpl implements AgentWorkspaceAppli } } - // - // ------------------ 以下是private方法 ----------------------- - // + /** + * 构建技能模板 zip:从 skill-template.json 读取模板文件列表,打包成 zip + */ + private MultipartFile buildSkillTemplateZip() { + SkillConfigDto skillConfigDto = skillApplicationService.getSkillTemplate(); + List files = skillConfigDto.getFiles(); + if (CollectionUtils.isEmpty(files)) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentSkillTemplateReadFailed, + "skill template has no files"); + } + return buildSkillFilesZip(files, "skill-template.zip"); + } + + /** + * 将 SkillFileDto 列表打包成 zip 文件 + */ + private MultipartFile buildSkillFilesZip(List files, String zipFileName) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) { + for (SkillFileDto file : files) { + if (file == null || StringUtils.isBlank(file.getName())) { + continue; + } + if (Boolean.TRUE.equals(file.getIsDir())) { + String dirPath = file.getName().endsWith("/") ? file.getName() : file.getName() + "/"; + zos.putNextEntry(new ZipEntry(dirPath)); + zos.closeEntry(); + } else { + zos.putNextEntry(new ZipEntry(file.getName())); + if (file.getContents() != null) { + zos.write(file.getContents().getBytes(StandardCharsets.UTF_8)); + } + zos.closeEntry(); + } + } + zos.finish(); + return new InMemoryMultipartFile("file", zipFileName, "application/zip", baos.toByteArray()); + } catch (IOException e) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentSkillZipPackFailed, e.getMessage()); + } + } /** * 将skills和subagents打包成 zip diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ComponentApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ComponentApplicationServiceImpl.java index 45412547..e0bd82b6 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ComponentApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ComponentApplicationServiceImpl.java @@ -1,14 +1,13 @@ package com.xspaceagi.agent.core.application.service; -import com.xspaceagi.agent.core.adapter.application.ComponentApplicationService; -import com.xspaceagi.agent.core.adapter.application.ModelApplicationService; -import com.xspaceagi.agent.core.adapter.application.PluginApplicationService; -import com.xspaceagi.agent.core.adapter.application.WorkflowApplicationService; +import com.xspaceagi.agent.core.adapter.application.*; import com.xspaceagi.agent.core.adapter.dto.ComponentDto; import com.xspaceagi.agent.core.adapter.dto.CreatorDto; import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto; import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; import com.xspaceagi.agent.core.spec.enums.ComponentTypeEnum; import com.xspaceagi.compose.sdk.service.IComposeDbTableRpcService; import com.xspaceagi.compose.sdk.vo.define.TableDefineVo; @@ -47,92 +46,119 @@ public class ComponentApplicationServiceImpl implements ComponentApplicationServ @Resource private UserApplicationService userApplicationService; + @Resource + private ResourceGroupApplicationService resourceGroupApplicationService; + @Override - public List getComponentListBySpaceId(Long spaceId) { + public List getComponentListBySpaceId(Long spaceId, Long groupId, List types) { List componentDtos = new java.util.ArrayList<>(); + if (types == null || types.contains(Published.TargetType.Workflow)) { + // 查询工作流配置 + List workflowConfigDtos = workflowApplicationService.queryListBySpaceId(spaceId); + workflowConfigDtos.forEach(workflowConfigDto -> { + ComponentDto componentDto = new ComponentDto(); + BeanUtils.copyProperties(workflowConfigDto, componentDto); + componentDto.setType(ComponentTypeEnum.Workflow); + componentDtos.add(componentDto); + }); + } - // 查询工作流配置 - List workflowConfigDtos = workflowApplicationService.queryListBySpaceId(spaceId); - workflowConfigDtos.forEach(workflowConfigDto -> { - ComponentDto componentDto = new ComponentDto(); - BeanUtils.copyProperties(workflowConfigDto, componentDto); - componentDto.setType(ComponentTypeEnum.Workflow); - componentDtos.add(componentDto); - }); + if (types == null || types.contains(Published.TargetType.Model)) { + // 查询模型配置 + List modelConfigDtos = modelApplicationService.queryModelConfigLisBySpaceId(spaceId); + modelConfigDtos.forEach(modelConfigDto -> { + ComponentDto componentDto = new ComponentDto(); + BeanUtils.copyProperties(modelConfigDto, componentDto); + componentDto.setName(modelConfigDto.getName()); + componentDto.setType(ComponentTypeEnum.Model); + componentDtos.add(componentDto); + }); + } - // 查询模型配置 - List modelConfigDtos = modelApplicationService.queryModelConfigLisBySpaceId(spaceId); - modelConfigDtos.forEach(modelConfigDto -> { - ComponentDto componentDto = new ComponentDto(); - BeanUtils.copyProperties(modelConfigDto, componentDto); - componentDto.setName(modelConfigDto.getName()); - componentDto.setType(ComponentTypeEnum.Model); - componentDtos.add(componentDto); - }); + if (types == null || types.contains(Published.TargetType.Plugin)) { + List pluginDtos = pluginApplicationService.queryListBySpaceId(spaceId); + pluginDtos.forEach(pluginDto -> { + ComponentDto componentDto = new ComponentDto(); + BeanUtils.copyProperties(pluginDto, componentDto); + componentDto.setType(ComponentTypeEnum.Plugin); + componentDto.setExt(pluginDto.getType()); + componentDto.setDevAgentConversationId(pluginDto.getDevAgentConversationId()); + componentDtos.add(componentDto); + }); + } - List pluginDtos = pluginApplicationService.queryListBySpaceId(spaceId); - pluginDtos.forEach(pluginDto -> { - ComponentDto componentDto = new ComponentDto(); - BeanUtils.copyProperties(pluginDto, componentDto); - componentDto.setType(ComponentTypeEnum.Plugin); - componentDto.setExt(pluginDto.getType()); - componentDtos.add(componentDto); - }); + if (types == null || types.contains(Published.TargetType.Knowledge)) { + List knowledgeConfigModels = iKnowledgeConfigApplicationService.queryListBySpaceId(spaceId); + //给知识库补充用户信息 + List userDtoList = userApplicationService.queryUserListByIds(knowledgeConfigModels.stream().map(KnowledgeConfigModel::getCreatorId).distinct().collect(Collectors.toList())); + //userDtoList转map + Map userDtoMap = userDtoList.stream().collect(Collectors.toMap(UserDto::getId, userDto -> userDto)); + knowledgeConfigModels.forEach(knowledgeConfigModel -> { + ComponentDto componentDto = new ComponentDto(); + componentDto.setName(knowledgeConfigModel.getName()); + componentDto.setId(knowledgeConfigModel.getId()); + componentDto.setSpaceId(knowledgeConfigModel.getSpaceId()); + //LocalDateTime转Date + componentDto.setCreated(Date.from(knowledgeConfigModel.getCreated().atZone(ZoneId.systemDefault()).toInstant())); + componentDto.setModified(Date.from(knowledgeConfigModel.getModified().atZone(ZoneId.systemDefault()).toInstant())); + componentDto.setDescription(knowledgeConfigModel.getDescription()); + componentDto.setType(ComponentTypeEnum.Knowledge); + componentDto.setExt(knowledgeConfigModel.getDataType()); + componentDto.setIcon(knowledgeConfigModel.getIcon()); + componentDto.setCreatorId(knowledgeConfigModel.getCreatorId()); + UserDto userDto = userDtoMap.get(knowledgeConfigModel.getCreatorId()); + if (userDto != null) { + componentDto.setCreator(new CreatorDto(userDto.getId(), userDto.getUserName(), userDto.getNickName(), userDto.getAvatar())); + } + componentDtos.add(componentDto); + }); + } - List knowledgeConfigModels = iKnowledgeConfigApplicationService.queryListBySpaceId(spaceId); - //给知识库补充用户信息 - List userDtoList = userApplicationService.queryUserListByIds(knowledgeConfigModels.stream().map(KnowledgeConfigModel::getCreatorId).distinct().collect(Collectors.toList())); - //userDtoList转map - Map userDtoMap = userDtoList.stream().collect(Collectors.toMap(UserDto::getId, userDto -> userDto)); - knowledgeConfigModels.forEach(knowledgeConfigModel -> { - ComponentDto componentDto = new ComponentDto(); - componentDto.setName(knowledgeConfigModel.getName()); - componentDto.setId(knowledgeConfigModel.getId()); - componentDto.setSpaceId(knowledgeConfigModel.getSpaceId()); - //LocalDateTime转Date - componentDto.setCreated(Date.from(knowledgeConfigModel.getCreated().atZone(ZoneId.systemDefault()).toInstant())); - componentDto.setModified(Date.from(knowledgeConfigModel.getModified().atZone(ZoneId.systemDefault()).toInstant())); - componentDto.setDescription(knowledgeConfigModel.getDescription()); - componentDto.setType(ComponentTypeEnum.Knowledge); - componentDto.setExt(knowledgeConfigModel.getDataType()); - componentDto.setIcon(knowledgeConfigModel.getIcon()); - componentDto.setCreatorId(knowledgeConfigModel.getCreatorId()); - UserDto userDto = userDtoMap.get(knowledgeConfigModel.getCreatorId()); - if (userDto != null) { - componentDto.setCreator(new CreatorDto(userDto.getId(), userDto.getUserName(), userDto.getNickName(), userDto.getAvatar())); - } - componentDtos.add(componentDto); - }); + if (types == null || types.contains(Published.TargetType.Table)) { + //数据表 + List tableDefineVoDorisDataPage = iComposeDbTableRpcService.queryListBySpaceId(spaceId); + //给知识库补充用户信息 + List tableUserDtoList = userApplicationService.queryUserListByIds(tableDefineVoDorisDataPage.stream() + .map(TableDefineVo::getCreatorId) + .distinct() + .toList()); + Map tableUserDtoMap = tableUserDtoList.stream().collect(Collectors.toMap(UserDto::getId, userDto -> userDto)); - //数据表 - List tableDefineVoDorisDataPage = iComposeDbTableRpcService.queryListBySpaceId(spaceId); - //给知识库补充用户信息 - List tableUserDtoList = userApplicationService.queryUserListByIds(tableDefineVoDorisDataPage.stream() - .map(TableDefineVo::getCreatorId) - .distinct() - .toList()); - Map tableUserDtoMap = tableUserDtoList.stream().collect(Collectors.toMap(UserDto::getId, userDto -> userDto)); - - tableDefineVoDorisDataPage.forEach(tableDefineVo -> { - ComponentDto componentDto = new ComponentDto(); - componentDto.setName(tableDefineVo.getTableName()); - componentDto.setDescription(tableDefineVo.getTableDescription()); - componentDto.setIcon(tableDefineVo.getIcon()); - componentDto.setId(tableDefineVo.getId()); - componentDto.setSpaceId(tableDefineVo.getSpaceId()); - componentDto.setCreated(Date.from(tableDefineVo.getCreated().atZone(ZoneId.systemDefault()).toInstant())); - componentDto.setModified(Date.from(tableDefineVo.getModified().atZone(ZoneId.systemDefault()).toInstant())); - componentDto.setType(ComponentTypeEnum.Table); - componentDto.setExt(tableDefineVo.getDorisDatabase() + "." + tableDefineVo.getDorisTable()); - UserDto userDto = tableUserDtoMap.get(tableDefineVo.getCreatorId()); - if (userDto != null) { - componentDto.setCreator(new CreatorDto(userDto.getId(), userDto.getUserName(), userDto.getNickName(), userDto.getAvatar())); - } - componentDto.setCreatorId(tableDefineVo.getCreatorId()); - componentDtos.add(componentDto); - }); + tableDefineVoDorisDataPage.forEach(tableDefineVo -> { + ComponentDto componentDto = new ComponentDto(); + componentDto.setName(tableDefineVo.getTableName()); + componentDto.setDescription(tableDefineVo.getTableDescription()); + componentDto.setIcon(tableDefineVo.getIcon()); + componentDto.setId(tableDefineVo.getId()); + componentDto.setSpaceId(tableDefineVo.getSpaceId()); + componentDto.setCreated(Date.from(tableDefineVo.getCreated().atZone(ZoneId.systemDefault()).toInstant())); + componentDto.setModified(Date.from(tableDefineVo.getModified().atZone(ZoneId.systemDefault()).toInstant())); + componentDto.setType(ComponentTypeEnum.Table); + componentDto.setExt(tableDefineVo.getDorisDatabase() + "." + tableDefineVo.getDorisTable()); + UserDto userDto = tableUserDtoMap.get(tableDefineVo.getCreatorId()); + if (userDto != null) { + componentDto.setCreator(new CreatorDto(userDto.getId(), userDto.getUserName(), userDto.getNickName(), userDto.getAvatar())); + } + componentDto.setCreatorId(tableDefineVo.getCreatorId()); + componentDtos.add(componentDto); + }); + } //componentDtos按照编辑时间从大到小排序 componentDtos.sort((o1, o2) -> o2.getModified().compareTo(o1.getModified())); + + if (groupId != null) { + Map relationMap = resourceGroupApplicationService.queryGroupRelations(groupId).stream() + .collect(Collectors.toMap(resourceGroupRelation -> resourceGroupRelation.getTargetType() + "-" + resourceGroupRelation.getTargetId(), + resourceGroupRelation -> resourceGroupRelation, (a, b) -> a)); + componentDtos.removeIf(componentDto -> !relationMap.containsKey(componentDto.getType().name() + "-" + componentDto.getId())); + componentDtos.forEach(componentDto -> componentDto.setGroupId(groupId)); + } else { + List> list = componentDtos.stream().map(componentDto -> Map.of(componentDto.getType().name(), componentDto.getId())).collect(Collectors.toList()); + Map relationMap = resourceGroupApplicationService.queryGroupRelations(list).stream() + .collect(Collectors.toMap(resourceGroupRelation -> resourceGroupRelation.getTargetType() + "-" + resourceGroupRelation.getTargetId(), + resourceGroupRelation -> resourceGroupRelation, (a, b) -> a)); + componentDtos.forEach(componentDto -> componentDto.setGroupId(relationMap.get(componentDto.getType().name() + "-" + componentDto.getId()) != null ? relationMap.get(componentDto.getType().name() + "-" + componentDto.getId()).getGroupId() : null)); + } return componentDtos; } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ComputerFileApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ComputerFileApplicationServiceImpl.java index a1ddb5a7..cbce4d47 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ComputerFileApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ComputerFileApplicationServiceImpl.java @@ -10,6 +10,7 @@ import com.xspaceagi.agent.core.infra.rpc.dto.SandboxServerConfig; import com.xspaceagi.agent.core.spec.utils.FileTypeUtils; import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.service.AuthService; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; import com.xspaceagi.system.sdk.service.dto.UserShareDto; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.common.UserContext; @@ -59,9 +60,11 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat private UserShareRpcService userShareRpcService; @Resource private AuthService authService; + @Resource + private SpacePermissionService spacePermissionService; @Override - public Map getFileList(Long userId, Long cId, String proxyPath, UserContext userContext) { + public Map getFileList(Long userId, Long cId, String proxyPath, UserContext userContext, String customTargetDir) { if (userId == null || userId <= 0) { log.error("[Web] Invalid userId, userId={}", userId); return buildError("userId is invalid"); @@ -71,7 +74,7 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat return buildError("cId is invalid"); } try { - return computerFileDomainService.getFileList(userId, cId, proxyPath, userContext); + return computerFileDomainService.getFileList(userId, cId, proxyPath, userContext, customTargetDir); } catch (Exception e) { log.error("[Web] Exception querying file list, userId={}, cId={}", userId, cId, e); return buildError("Failed to query file list"); @@ -79,7 +82,26 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat } @Override - public Map filesUpdate(Long userId, Long cId, List files, UserContext userContext) { + public Map getLogs(Long userId, Long cId, int tailLines) { + if (userId == null || userId <= 0) { + log.error("[Web] Invalid userId, userId={}", userId); + return buildError("userId is invalid"); + } + if (cId == null || cId <= 0) { + log.error("[Web] Invalid cId, cId={}", cId); + return buildError("cId is invalid"); + } + int safeTailLines = Math.max(1, tailLines); + try { + return computerFileDomainService.getLogs(userId, cId, safeTailLines, null); + } catch (Exception e) { + log.error("[Web] Exception querying logs, userId={}, cId={}", userId, cId, e); + return buildError("Failed to query logs"); + } + } + + @Override + public Map filesUpdate(Long userId, Long cId, List files, UserContext userContext, String customTargetDir) { if (userId == null || userId <= 0) { log.error("[Web] Invalid userId, userId={}", userId); return buildError("userId is invalid"); @@ -93,7 +115,7 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat return buildError("files is invalid"); } try { - return computerFileDomainService.filesUpdate(userId, cId, files, userContext); + return computerFileDomainService.filesUpdate(userId, cId, files, userContext, customTargetDir); } catch (Exception e) { log.error("[Web] Exception updating user file list, userId={}, cId={}", userId, cId, e); return buildError("Failed to update user file list, please check if filename is duplicated"); @@ -101,7 +123,7 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat } @Override - public Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file, UserContext userContext) { + public Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file, UserContext userContext, String customTargetDir) { if (userId == null || userId <= 0) { log.error("[Web] Invalid userId, userId={}", userId); return buildError("userId is invalid"); @@ -119,7 +141,7 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat return buildError("file is invalid"); } try { - return computerFileDomainService.uploadFile(userId, cId, filePath, file, userContext); + return computerFileDomainService.uploadFile(userId, cId, filePath, file, userContext, customTargetDir); } catch (Exception e) { log.error("[Web] Exception uploading user file, userId={}, cId={}, filePath={}", userId, cId, filePath, e); return buildError("Failed to upload user file"); @@ -127,7 +149,7 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat } @Override - public Map uploadFiles(Long userId, Long cId, List filePaths, List files, UserContext userContext) { + public Map uploadFiles(Long userId, Long cId, List filePaths, List files, UserContext userContext, String customTargetDir) { if (userId == null || userId <= 0) { log.error("[Web] Invalid userId, userId={}", userId); return buildError("userId is invalid"); @@ -149,7 +171,7 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat return buildError("filePaths and files count mismatch"); } try { - return computerFileDomainService.uploadFiles(userId, cId, filePaths, files, userContext); + return computerFileDomainService.uploadFiles(userId, cId, filePaths, files, userContext, customTargetDir); } catch (Exception e) { log.error("[Web] Exception batch uploading user files, userId={}, cId={}", userId, cId, e); return buildError("Failed to batch upload user files"); @@ -157,7 +179,34 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat } @Override - public ResponseEntity downloadAllFiles(Long userId, Long cId, UserContext userContext) { + public Map importProject(Long userId, Long cId, MultipartFile file, UserContext userContext, String customTargetDir) { + if (userId == null || userId <= 0) { + log.error("[Web] Invalid userId, userId={}", userId); + return buildError("userId is invalid"); + } + if (cId == null || cId <= 0) { + log.error("[Web] Invalid cId, cId={}", cId); + return buildError("cId is invalid"); + } + if (file == null || file.isEmpty()) { + log.error("[Web] file is invalid, file is empty"); + return buildError("file is invalid"); + } + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".zip")) { + log.error("[Web] file is invalid, only zip is supported, fileName={}", originalFilename); + return buildError("Only zip files are supported"); + } + try { + return computerFileDomainService.importProject(userId, cId, file, userContext, customTargetDir); + } catch (Exception e) { + log.error("[Web] Exception importing project, userId={}, cId={}, fileName={}", userId, cId, originalFilename, e); + return buildError("Failed to import project"); + } + } + + @Override + public ResponseEntity downloadAllFiles(Long userId, Long cId, UserContext userContext, String customTargetDir) { if (userId == null || userId <= 0) { log.error("[Web] Invalid userId, userId={}", userId); return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); @@ -171,7 +220,7 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat log.info("[Web] Download all files, logId={}, userId={}, cId={}", logId, userId, cId); try { - Flux fileFlux = computerFileDomainService.downloadAllFiles(userId, cId, logId, userContext); + Flux fileFlux = computerFileDomainService.downloadAllFiles(userId, cId, logId, userContext, customTargetDir); // Check the first signal before starting to write to detect errors early // Use share() to share Flux, then use materialize() to check the first signal @@ -273,7 +322,7 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat } @Override - public ResponseEntity getStaticFile(Long cId, HttpServletRequest request) { + public ResponseEntity getStaticFile(Long cId, HttpServletRequest request, String customTargetDir) { ConversationDto currentConversation = getConversation(cId); AuthResult authResult = staticFileAuth(currentConversation, cId, request); if (authResult.getRedirectUrl() != null) { @@ -321,7 +370,7 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat log.info("[Web] Client range request, logId={}, range={}", logId, rangeHeader); } ResponseEntity> downstreamResponse = computerFileDomainService.getStaticFileResponse( - cId, targetPrefix, finalRelativePath, logId, rangeHeader + cId, targetPrefix, finalRelativePath, logId, rangeHeader, customTargetDir ); if (downstreamResponse == null || downstreamResponse.getBody() == null) { log.error("[Web] Downstream static file response is empty, logId={}", logId); @@ -545,8 +594,14 @@ public class ComputerFileApplicationServiceImpl implements IComputerFileApplicat } if (!currentUserId.equals(conversationUserId)) { - throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + Long devSpaceId = currentConversation.getDevSpaceId(); + if (devSpaceId != null) { + spacePermissionService.checkSpaceUserPermission(devSpaceId, RequestContext.get().getUserId()); + } else { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } } + return new AuthResult(conversationUserId, null, null, null); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ConversationApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ConversationApplicationServiceImpl.java index 6cbb491f..4d5da652 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ConversationApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ConversationApplicationServiceImpl.java @@ -2,29 +2,35 @@ package com.xspaceagi.agent.core.application.service; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; +import com.google.common.base.Joiner; import com.xspaceagi.agent.core.adapter.application.*; import com.xspaceagi.agent.core.adapter.dto.*; -import com.xspaceagi.agent.core.adapter.dto.config.AgentComponentConfigDto; -import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; -import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.*; import com.xspaceagi.agent.core.adapter.dto.config.bind.*; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.AgentNodeConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowNodeDto; import com.xspaceagi.agent.core.adapter.repository.entity.*; +import com.xspaceagi.agent.core.application.support.PlatformEnvHookSupport; import com.xspaceagi.agent.core.domain.service.AgentDomainService; import com.xspaceagi.agent.core.domain.service.ConversationDomainService; import com.xspaceagi.agent.core.infra.component.agent.AgentContext; import com.xspaceagi.agent.core.infra.component.agent.AgentExecutor; import com.xspaceagi.agent.core.infra.component.agent.SandboxAgentClient; import com.xspaceagi.agent.core.infra.component.agent.dto.AgentExecuteResult; +import com.xspaceagi.agent.core.infra.component.model.dto.ComponentExecuteResult; import com.xspaceagi.agent.core.infra.component.workflow.handler.QANodeHandler; +import com.xspaceagi.agent.core.infra.rpc.GitRpcClient; import com.xspaceagi.agent.core.infra.rpc.McpRpcService; import com.xspaceagi.agent.core.infra.rpc.MetricRpcService; import com.xspaceagi.agent.core.infra.rpc.ResourcePricingRpcService; +import com.xspaceagi.agent.core.infra.sub.ChatMessagePubSubService; import com.xspaceagi.agent.core.spec.constant.Prompts; import com.xspaceagi.agent.core.spec.enums.GlobalVariableEnum; import com.xspaceagi.agent.core.spec.enums.MessageTypeEnum; import com.xspaceagi.agent.core.spec.enums.TaskCron; -import com.xspaceagi.agent.core.spec.utils.TikTokensUtil; +import com.xspaceagi.custompage.sdk.ICustomPageRpcService; +import com.xspaceagi.custompage.sdk.dto.CustomPageDto; import com.xspaceagi.file.sdk.IFileAccessService; import com.xspaceagi.log.sdk.service.ILogRpcService; import com.xspaceagi.log.sdk.vo.LogDocument; @@ -44,6 +50,7 @@ import com.xspaceagi.system.application.dto.TenantConfigDto; import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.service.NotifyMessageApplicationService; import com.xspaceagi.system.application.service.UserApplicationService; +import com.xspaceagi.system.application.util.DefaultIconUrlUtil; import com.xspaceagi.system.infra.dao.entity.NotifyMessage; import com.xspaceagi.system.sdk.common.TraceContext; import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService; @@ -67,8 +74,8 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.springframework.ai.chat.messages.*; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.MessageType; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Service; import org.springframework.util.Assert; @@ -86,6 +93,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static com.xspaceagi.agent.core.adapter.dto.ChatMessageDto.removeSystemTagContent; import static com.xspaceagi.system.spec.cache.SimpleJvmHashCache.DEFAULT_EXPIRE_AFTER_SECONDS; @Slf4j @@ -164,14 +172,27 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi @Resource private AgentExecutor agentExecutor; + @Resource + private GitRpcClient gitRpcClient; + @Resource private SandboxAgentClient sandboxAgentClient; @Resource private SkillApplicationService skillApplicationService; - @Autowired + + @Resource private ResourcePricingRpcService resourcePricingRpcService; + @Resource + private ICustomPageRpcService iCustomPageRpcService; + + @Resource + private WorkflowApplicationService workflowApplicationService; + + @Resource + private ChatMessagePubSubService chatMessagePubSubService; + @PostConstruct private void init() { scheduleTaskApiService.start(ScheduleTaskDto.builder() @@ -233,6 +254,31 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi }); } + @Override + public ConversationDto createConversationForProjectDevelopment(Long tenantId, Long userId, Long devSpaceId, Long devAgentId, String targetType, Long targetId) { + Conversation conversation = new Conversation(); + conversation.setTenantId(tenantId); + conversation.setAgentId(devAgentId); + conversation.setDevSpaceId(devSpaceId); + conversation.setDevTargetType(targetType); + conversation.setDevTargetId(targetId.toString()); + conversation.setUserId(userId); + conversation.setUid(UUID.randomUUID().toString().replace("-", "")); + conversation.setTopic(""); + conversation.setDevMode(0); + conversation.setTenantId(RequestContext.get().getTenantId()); + conversation.setType(Conversation.ConversationType.Development); + conversation.setTopicUpdated(1); + Long sandboxId = iSandboxConfigRpcService.queryUserSelectedSandboxId(userId, devAgentId); + if (sandboxId != null && sandboxId > 0) { + conversation.setSandboxServerId(sandboxId.toString()); + } + return TenantFunctions.callWithIgnoreCheck(() -> { + conversationDomainService.createConversation(conversation); + return convertConversationWithAgent(conversation); + }); + } + @Override public ConversationDto createConversation(Long userId, Long agentId, boolean devMode, boolean tempChat, Map variables) { AgentConfigDto agentConfigDto = agentApplicationService.queryById(agentId); @@ -243,7 +289,6 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi //使用过的用户数量+1 publishApplicationService.incStatisticsCount(Published.TargetType.Agent, agentId, PublishedStatistics.Key.USER_COUNT.getKey(), 1L); } - Conversation conversation = new Conversation(); conversation.setAgentId(agentId); conversation.setUserId(userId); @@ -255,6 +300,13 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi if (tempChat) { conversation.setType(Conversation.ConversationType.TempChat); } + //createdFrom + boolean isCreatedFromVibeCoding = agentConfigDto.getExtra() != null && agentConfigDto.getExtra().get("createdFrom") != null && "VibeCoding".equals(agentConfigDto.getExtra().get("createdFrom")); + if (devMode && (agentConfigDto.getDevAgentConversationId() != null || isCreatedFromVibeCoding)) { + conversation.setType(Conversation.ConversationType.DevDebug); + ConversationDto conversationByCid = getConversationByCid(agentConfigDto.getDevAgentConversationId()); + conversation.setSandboxServerId(conversationByCid.getSandboxServerId() == null ? "-1" : conversationByCid.getSandboxServerId());//为空时为选择了云端电脑 + } conversationDomainService.createConversation(conversation); // 增加智能体统计会话数量 publishApplicationService.incStatisticsCount(Published.TargetType.Agent, agentId, PublishedStatistics.Key.CONV_COUNT.getKey(), 1L); @@ -345,6 +397,17 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi conversationDomainService.updateConversation(cid, Conversation.builder().taskStatus(status).build()); } + @Override + public void updateConversationVariables(Long id, Map variables) { + Map variables0 = new HashMap<>(); + if (variables != null) { + //去除GlobalVariableEnum.isSystemVariable + variables0.putAll(variables); + variables0.entrySet().removeIf(entry -> GlobalVariableEnum.isSystemVariable(entry.getKey()) || "SANDBOX_ACCESS_KEY".equals(entry.getKey())); + } + TenantFunctions.runWithIgnoreCheck(() -> conversationDomainService.updateConversation(id, Conversation.builder().variables(variables0).build())); + } + //Conversation转ConversationDto private ConversationDto convertConversation(Conversation conversation) { if (conversation == null) { @@ -369,6 +432,10 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi conversationDto.setTopicUpdated(conversation.getTopicUpdated()); conversationDto.setSandboxSessionId(conversation.getSandboxSessionId()); conversationDto.setSandboxServerId(conversation.getSandboxServerId()); + conversationDto.setDevTargetType(conversation.getDevTargetType()); + conversationDto.setDevTargetId(conversation.getDevTargetId()); + conversationDto.setDevSpaceId(conversation.getDevSpaceId()); + conversationDto.setIcon(conversation.getIcon()); return conversationDto; } @@ -377,7 +444,32 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi return null; } ConversationDto conversationDto = convertConversation(conversation); - conversationDto.setAgent(agentApplicationService.queryAgentDetail(conversation.getAgentId(), conversation.getDevMode() != 1)); + boolean publishedMode = conversation.getDevMode() != 1; + AgentDetailDto agentDetailDto = agentApplicationService.queryAgentDetail(conversation.getAgentId(), publishedMode); + if (publishedMode && isPrivateTaskAgent(agentDetailDto)) { + AgentDetailDto latestAgentDetailDto = agentApplicationService.queryAgentDetail(conversation.getAgentId(), false); + if (latestAgentDetailDto != null && isPrivateTaskAgent(latestAgentDetailDto)) { + log.debug("Private TaskAgent conversation detail uses latest config, agentId:{}, cid:{}", + conversation.getAgentId(), conversation.getId()); + agentDetailDto = latestAgentDetailDto; + } + } + if (agentDetailDto != null) { + conversationDto.setAgent(agentDetailDto); + List variables = agentDetailDto.getVariables(); + Set nameSet; + if (variables != null) { + nameSet = agentDetailDto.getVariables().stream().map(Arg::getName).collect(Collectors.toSet()); + } else { + nameSet = Collections.emptySet(); + } + if (conversationDto.getVariables() != null) { + conversationDto.getVariables().entrySet().removeIf(entry -> !nameSet.contains(entry.getKey())); + } + } else { + conversationDto.setVariables(null); + } + return conversationDto; } @@ -417,6 +509,15 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi conversationUpdateDto.setTopic(conversationUpdateDto.getFirstMessage().length() > 20 ? conversationUpdateDto.getFirstMessage().substring(0, 20) : conversationUpdateDto.getFirstMessage()); } else { conversationUpdateDto.setTopic(topicGenDto.getTopic()); + if (StringUtils.isNotBlank(topicGenDto.getSvgIcon())) { + try { + String fileUrl = agentApplicationService.uploadSvgIcon(topicGenDto.getSvgIcon(), conversationUpdateDto.getTopic() + ".svg"); + conversationUpdateDto.setIcon(fileUrl); + } catch (Exception e) { + // ignore + log.warn("uploadSvgIcon failed", e); + } + } } } Conversation conversation = new Conversation(); @@ -426,6 +527,9 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi if (conversation.getTopic() != null && conversation.getTopic().trim().startsWith("\"")) { conversation.setTopic(conversation.getTopic().replace("\"", "").replace("主题:", "").trim()); } + if (StringUtils.isNotBlank(conversationUpdateDto.getIcon())) { + conversation.setIcon(conversationUpdateDto.getIcon()); + } conversationDomainService.updateConversation(userId, id, conversation); } @@ -457,8 +561,18 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi public List queryConversationList(Long userId, Long agentId, Long lastId, Integer limit, String topic) { List conversations = conversationDomainService.queryConversationList(userId, agentId, lastId, limit == null ? 10 : limit, topic); List conversationDtos = conversations.stream().map(this::convertConversation).collect(Collectors.toList()); - List agentIds = conversations.stream().map(Conversation::getAgentId).toList(); + List agentIds = conversations.stream().map(Conversation::getAgentId).collect(Collectors.toList()); + List devAgentIds = conversations.stream().filter(conversation -> Published.TargetType.Agent.name().equals(conversation.getDevTargetType()) && conversation.getDevTargetId() != null).map(conversation -> Long.parseLong(conversation.getDevTargetId())).toList(); + List pageAppIds = conversations.stream().filter(conversation -> Published.TargetType.PageApp.name().equals(conversation.getDevTargetType()) && conversation.getDevTargetId() != null).map(conversation -> Long.parseLong(conversation.getDevTargetId())).toList(); + agentIds.addAll(devAgentIds); if (CollectionUtils.isNotEmpty(agentIds)) { + Map customPageDtoMap; + if (CollectionUtils.isNotEmpty(pageAppIds)) { + List customPages = iCustomPageRpcService.listByIds(pageAppIds, null); + customPageDtoMap = customPages.stream().collect(Collectors.toMap(CustomPageDto::getProjectId, customPage -> customPage, (v1, v2) -> v1)); + } else { + customPageDtoMap = new HashMap<>(); + } List agentConfigs = agentDomainService.queryListByIds(agentIds); // agentConfigs 转map Map agentConfigMap = agentConfigs.stream().map(agentConfig -> { @@ -466,9 +580,33 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi agentDetailDto.setAgentId(agentConfig.getId()); agentDetailDto.setName(agentConfig.getName()); agentDetailDto.setType(agentConfig.getType()); + agentDetailDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(agentConfig.getIcon(), agentConfig.getName(), "agent", true)); return agentDetailDto; }).collect(Collectors.toMap(AgentDetailDto::getAgentId, agentConfig -> agentConfig)); - conversationDtos.forEach(conversationDto -> conversationDto.setAgent(agentConfigMap.get(conversationDto.getAgentId()))); + conversationDtos = conversationDtos.stream().map(conversationDto -> { + if (Published.TargetType.Agent.name().equals(conversationDto.getDevTargetType()) && conversationDto.getDevTargetId() != null) { + AgentDetailDto agentDetailDto = agentConfigMap.get(Long.parseLong(conversationDto.getDevTargetId())); + if (agentDetailDto != null) { + conversationDto.setTopic(agentDetailDto.getName()); + conversationDto.setSummary(agentDetailDto.getDescription()); + conversationDto.setIcon(agentDetailDto.getIcon()); + } + } + if (Published.TargetType.PageApp.name().equals(conversationDto.getDevTargetType()) && conversationDto.getDevTargetId() != null) { + CustomPageDto customPageDto = customPageDtoMap.get(Long.parseLong(conversationDto.getDevTargetId())); + if (customPageDto != null) { + conversationDto.setTopic(customPageDto.getName()); + conversationDto.setSummary(customPageDto.getDescription()); + conversationDto.setIcon(customPageDto.getIcon()); + } else { + // 项目已经被删除,对应的会话也删除 + conversationDomainService.deleteConversation(userId, conversationDto.getId()); + return null; + } + } + conversationDto.setAgent(agentConfigMap.get(conversationDto.getAgentId())); + return conversationDto; + }).filter(Objects::nonNull).collect(Collectors.toList()); } return conversationDtos; } @@ -568,7 +706,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi @Override public String queryMemory(Long tenantId, Long userId, Long agentId, String inputMessage, String context, boolean justKeywordSearch, boolean filterSensitive) { - return iMemoryRpcService.searchMemoriesMd(tenantId, userId, agentId, inputMessage, context, justKeywordSearch, filterSensitive); + return TenantFunctions.callWithIgnoreCheck(() -> iMemoryRpcService.searchMemoriesMd(tenantId, userId, agentId, inputMessage, context, justKeywordSearch, filterSensitive)); } @Override @@ -595,6 +733,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi chatMessageDto.setText(chatMessageDto.getText().replaceAll("[\\s\\S]*?", "").trim()); } chatMessageDto.setText(chatMessageDto.getText().replaceAll("[\\s\\S]*?", "").trim()); + chatMessageDto.setText(chatMessageDto.getText().replaceAll("[\\s\\S]*?", "").trim()); chatMessageDto.setText(chatMessageDto.getText().replaceAll("[\\s\\S]*?", "").trim()); if (chatMessageDto.getText().contains("") && chatMessageDto.getText().contains("")) { chatMessageDto.setText(chatMessageDto.getText().replace("", "").replace("", "").trim()); @@ -610,15 +749,6 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi }).collect(Collectors.toList()); } - private String removeSystemTagContent(String text) { - if (text == null) { - return null; - } - return text.replaceAll("[\\s\\S]*?", "").trim() - .replaceAll("```xml[\\s\\S]*?[\\s\\S]*?[\\s\\S]*?```", " ") - .replaceAll("[\\s\\S]*?", " "); - } - @Override public void addRoundMessage(String conversationId, Message message) { if (conversationId == null) { @@ -680,58 +810,31 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi if (conversationId.startsWith("agent:")) { conversationId = conversationId.replace("agent:", ""); } - List all = new ArrayList<>(); //获取最新的lastN条消息 List cachedMessageList0 = queryMessageList(Long.parseLong(conversationId), Long.MAX_VALUE, lastN); - - //cachedMessageList转Message - for (ChatMessageDto message : cachedMessageList0) { - if (message.getRole().name().equals(MessageType.USER.name())) { - all.add(new UserMessage(message.getText())); - } - if (message.getRole().name().equals(MessageType.ASSISTANT.name())) { - String text = removeSystemTagContent(message.getText()); - if (text != null) { - text = text.replaceAll("]*>.*?", ""); - } - all.add(new AssistantMessage(text)); - } - if (message.getRole().name().equals(MessageType.SYSTEM.name())) { - all.add(new SystemMessage(message.getText())); - } - } - - Collections.reverse(all); - Iterator iterator = all.iterator(); - int tokenCount = 0; - MessageType messageType = MessageType.ASSISTANT; - while (iterator.hasNext()) { - Message next = iterator.next(); - if (messageType != next.getMessageType()) { - iterator.remove(); - continue; - } - messageType = messageType == MessageType.USER ? MessageType.ASSISTANT : MessageType.USER; - if (tokenCount >= MAX_TOKEN_WINDOW_SIZE) { - iterator.remove(); - } else { - tokenCount += TikTokensUtil.tikTokensCount(next.getText()); - } - } - Collections.reverse(all); - removeIfFirstMessageIsAssistant(all); - return all; + return ChatMessageDto.toMessages(cachedMessageList0); } - //移除第一条消息不是User的内容,避免像deepseek第一条消息不是User消息时,导致无法正常工具调用 - private void removeIfFirstMessageIsAssistant(List all) { - if (CollectionUtils.isEmpty(all)) { - return; + public List getRoundMessages(String conversationId, Long minId) { + List messages = conversationDomainService.queryConversationMessageList(Long.parseLong(conversationId), minId).stream().map(message -> { + ChatMessageDto chatMessageDto = JSON.parseObject(message.getContent(), ChatMessageDto.class); + chatMessageDto.setTime(message.getCreated()); + chatMessageDto.setId(message.getMessageId()); + chatMessageDto.setIndex(message.getId()); + return chatMessageDto; + }).collect(Collectors.toList()); + Collections.reverse(messages); + while (!messages.isEmpty() && messages.get(0).getMessageType() == MessageType.ASSISTANT) { + messages.remove(0); } - if (all.get(0).getMessageType() == MessageType.ASSISTANT) { - all.remove(0); - removeIfFirstMessageIsAssistant(all); + for (ChatMessageDto message : messages) { + String text = removeSystemTagContent(message.getText()); + if (text != null) { + text = text.replaceAll("[\\s\\S]*?", "").replaceAll("]*>.*?", "").trim(); + message.setText(text); + } } + return messages; } private List queryMessageList(Long conversationId, Long index, int size) { @@ -771,11 +874,19 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi }).collect(Collectors.toList()); } - /** - * 生成conversation key - */ - public String generateConversationKey(String conversationId) { - return "conversation:" + conversationId; + @Override + public void setChatStopStatus(String conversationId) { + redisUtil.set("chat.stop." + conversationId, String.valueOf(System.currentTimeMillis()), 60); + } + + @Override + public boolean isChatStop(String conversationId) { + return redisUtil.get("chat.stop." + conversationId) != null; + } + + @Override + public void clearChatStopStatus(String conversationId) { + redisUtil.expire("chat.stop." + conversationId, 0); } @Override @@ -835,7 +946,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi public Flux chat(TryReqDto tryReqDto, Map headersFromRequest, boolean isTempChat, Boolean devMode) { log.info("ConversationApplicationServiceImpl.chat {}", tryReqDto); - ConversationDto conversationDto = getConversation(null, tryReqDto.getConversationId()); + ConversationDto conversationDto = getConversationByCid(tryReqDto.getConversationId()); AgentOutputDto errorOutput = new AgentOutputDto(); errorOutput.setEventType(AgentOutputDto.EventTypeEnum.FINAL_RESULT); AgentExecuteResult agentExecuteResult = new AgentExecuteResult(); @@ -851,6 +962,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi if (devMode != null) { conversationDto.setDevMode(devMode ? 1 : 0); } + devMode = Objects.equals(conversationDto.getDevMode(), YesOrNoEnum.Y.getKey()); AgentConfigDto agentConfigDto; if (Objects.equals(conversationDto.getDevMode(), YesOrNoEnum.Y.getKey())) { agentConfigDto = agentApplicationService.queryConfigForTestExecute(conversationDto.getAgentId()); @@ -888,6 +1000,16 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi agentExecuteResult.setError(errorMsg); return errorOutput(errorOutput, conversationDto); } + + setFileAkForCustomAgent(agentConfigDto); + if (isPrivateTaskAgent(agentConfigDto)) { + AgentConfigDto latestAgentConfigDto = agentApplicationService.queryConfigForTestExecute(conversationDto.getAgentId()); + if (latestAgentConfigDto != null && isPrivateTaskAgent(latestAgentConfigDto)) { + log.debug("Private TaskAgent chat uses latest config, agentId:{}, cid:{}", + conversationDto.getAgentId(), conversationDto.getId()); + agentConfigDto = latestAgentConfigDto; + } + } } } if (agentConfigDto == null) { @@ -994,10 +1116,12 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi } } - Conversation conversationUpdate = null; + Conversation conversationUpdate = Conversation.builder().build(); // 变量记录 if (tryReqDto.getVariableParams() != null && !tryReqDto.getVariableParams().isEmpty() && (conversationDto.getVariables() == null || conversationDto.getVariables().isEmpty())) { - conversationUpdate = Conversation.builder().variables(tryReqDto.getVariableParams()).build(); + conversationUpdate.setVariables(tryReqDto.getVariableParams()); + } else if (tryReqDto.getVariableParams() != null && !tryReqDto.getVariableParams().isEmpty()) { + conversationDto.getVariables().putAll(tryReqDto.getVariableParams()); } if (!Objects.equals(conversationDto.getDevMode(), YesOrNoEnum.Y.getKey()) && (StringUtils.isBlank(tryReqDto.getFrom()) || tryReqDto.getFrom().equals("chat"))) { @@ -1009,12 +1133,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi } if (conversationDto.getTopicUpdated() == -1) { - if (conversationUpdate == null) { - conversationUpdate = Conversation.builder().topicUpdated(0).build(); - } else { - conversationUpdate.setTopicUpdated(0); - } - + conversationUpdate.setTopicUpdated(0); long usedCount = conversationDomainService.agentUserCount(conversationDto.getUserId(), conversationDto.getAgentId()); if (usedCount == 0) { //使用过的用户数量+1 @@ -1024,9 +1143,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi publishApplicationService.incStatisticsCount(Published.TargetType.Agent, conversationDto.getAgentId(), PublishedStatistics.Key.CONV_COUNT.getKey(), 1L); } } - if (conversationUpdate == null) { - conversationUpdate = Conversation.builder().build(); - } + conversationUpdate.setTaskStatus(Conversation.ConversationTaskStatus.EXECUTING); conversationDto.setTaskStatus(Conversation.ConversationTaskStatus.EXECUTING); if (agentConfigDto.getExtra() != null && agentConfigDto.getExtra().get("sandboxId") != null) { @@ -1125,6 +1242,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi agentContext.setAttachments(tryReqDto.getAttachments()); agentContext.setUserDataPermission(userDataPermission); agentContext.setDefaultModelChanged(!isDefaultModel); + agentContext.setAgentMode(tryReqDto.getAgentMode()); if (tryReqDto.getFilterSensitive() == null) { // 自己开发的智能体不过滤敏感信息 agentContext.setFilterSensitive(!agentContext.getUserId().equals(agentConfigDto.getCreatorId())); @@ -1159,6 +1277,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi //tryReqDto.getSelectedComponents()移除Model tryReqDto.getSelectedComponents().removeIf(selectedComponentDto -> selectedComponentDto.getType() == AgentComponentConfig.Type.Model); List selectedIds = tryReqDto.getSelectedComponents().stream().map(TryReqDto.SelectedComponentDto::getId).toList(); + Set selectedIdSet = new HashSet<>(selectedIds); agentConfigDto.getAgentComponentConfigList().removeIf(agentComponentConfigDto -> { //根据前端传递要自动调用的组件,把对应组件改成自动调用 if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Workflow) { @@ -1199,22 +1318,33 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi } if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Knowledge) { KnowledgeBaseBindConfigDto bindConfig = (KnowledgeBaseBindConfigDto) agentComponentConfigDto.getBindConfig(); + boolean selected = selectedIdSet.contains(agentComponentConfigDto.getId()) || + (agentComponentConfigDto.getTargetId() != null && selectedIdSet.contains(agentComponentConfigDto.getTargetId())); + boolean defaultSelected = Objects.equals(bindConfig.getDefaultSelected(), YesOrNoEnum.Y.getKey()); + boolean shouldUseKnowledge = selected || defaultSelected; + if (defaultSelected && !selected) { + log.debug("Knowledge component uses default selected fallback, agentId:{}, componentId:{}, targetId:{}", + agentComponentConfigDto.getAgentId(), agentComponentConfigDto.getId(), agentComponentConfigDto.getTargetId()); + } if (bindConfig.getInvokeType() == KnowledgeBaseBindConfigDto.InvokeTypeEnum.MANUAL) { //手动调用未选择则不调用 - if (!selectedIds.contains(agentComponentConfigDto.getId())) { + if (!shouldUseKnowledge) { return true; } bindConfig.setInvokeType(KnowledgeBaseBindConfigDto.InvokeTypeEnum.AUTO); } if (bindConfig.getInvokeType() == KnowledgeBaseBindConfigDto.InvokeTypeEnum.MANUAL_ON_DEMAND) { //未选择则不调用 - if (!selectedIds.contains(agentComponentConfigDto.getId())) { + if (!shouldUseKnowledge) { return true; } //选择后调用改为按需调用 bindConfig.setInvokeType(KnowledgeBaseBindConfigDto.InvokeTypeEnum.ON_DEMAND); } } + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent) { + return !selectedIds.contains(agentComponentConfigDto.getId()); + } if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Skill) { SkillBindConfigDto bindConfig = (SkillBindConfigDto) agentComponentConfigDto.getBindConfig(); if (bindConfig.getInvokeType() == SkillBindConfigDto.SkillInvokeTypeEnum.MANUAL_ON_DEMAND && selectedIds.contains(agentComponentConfigDto.getId())) { @@ -1242,56 +1372,34 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi agentConfigDto.setUserPrompt(userPromptBuilder.toString()); } + boolean isMultiAgent = null != agentConfigDto.getAgentComponentConfigList().stream().filter(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent).findFirst().orElse(null); //任务型智能体MCP转换 //开发编排模式每次都需要刷新mcp配置 if ("TaskAgent".equals(agentConfigDto.getType())) { - List mcpToolComponentList = agentConfigDto.getAgentComponentConfigList().stream().filter(agentComponentConfig -> { - if (agentComponentConfig.getType() == AgentComponentConfig.Type.Mcp && agentComponentConfig.getTargetConfig() instanceof McpDto mcpDto) { - return mcpDto.getInstallType() == InstallTypeEnum.COMPONENT; - } - return agentComponentConfig.getType() == AgentComponentConfig.Type.Plugin || agentComponentConfig.getType() == AgentComponentConfig.Type.Knowledge - || agentComponentConfig.getType() == AgentComponentConfig.Type.Table || agentComponentConfig.getType() == AgentComponentConfig.Type.Workflow; - }).toList(); - if (!mcpToolComponentList.isEmpty()) { - boolean isDev = false; - if (conversationDto.getDevMode() != null && conversationDto.getDevMode().intValue() == YesOrNoEnum.Y.getKey()) { - agentApplicationService.buildProxyMcp(agentConfigDto, true); - isDev = true; - } - Map extra = agentConfigDto.getExtra(); - if (extra != null) { - try { - Long proxyMcpId = Long.parseLong(agentConfigDto.getExtra().get(isDev ? "devProxyMcpId" : "prodProxyMcpId").toString()); - UserAccessKeyDto.UserAccessKeyConfig userAccessKeyConfig = UserAccessKeyDto.UserAccessKeyConfig.builder() - .isDevMode(isDev ? 1 : 0) - .enabled(true) - .conversationId(tryReqDto.getConversationId().toString()) - .requestId(RequestContext.get().getRequestId()) - .traceContext(traceContext) - .build(); - - String exportMcpServerConfig = mcpRpcService.getExportMcpServerConfig(userDto.getId(), proxyMcpId, userAccessKeyConfig); - agentConfigDto.setProxyMcpServerConfig(exportMcpServerConfig); - } catch (NumberFormatException e) { - // ignore + boolean isDev = conversationDto.getDevMode() != null && conversationDto.getDevMode().intValue() == YesOrNoEnum.Y.getKey(); + updateProxyMcp(agentConfigDto, conversationDto.getId().toString(), traceContext, isDev); + //AgentFlow和AgentGroup补全群内的Mcp + if (isMultiAgent) { + agentConfigDto.getAgentComponentConfigList().forEach(agentComponentConfigDto -> { + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent && agentComponentConfigDto.getTargetConfig() instanceof AgentConfigDto agentConfigDto1) { + updateProxyMcp(agentConfigDto1, conversationDto.getId().toString(), traceContext, false); } + }); + } + if (AgentConfigDto.AgentSubType.Flow.name().equals(agentConfigDto.getSubType())) { + AgentComponentConfigDto componentConfigDto = agentConfigDto.getAgentComponentConfigList().stream().filter(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.Workflow).findFirst().orElse(null); + if (componentConfigDto != null && componentConfigDto.getTargetConfig() instanceof WorkflowConfigDto workflowConfigDto) { + workflowConfigDto.getNodes().forEach(node -> { + if (node.getNodeConfig() instanceof AgentNodeConfigDto agentNodeConfigDto && agentNodeConfigDto.getAgentConfigDto() != null) { + agentNodeConfigDto.getAgentConfigDto().setIcon(DefaultIconUrlUtil.setDefaultIconUrl(agentNodeConfigDto.getAgentConfigDto().getIcon(), agentNodeConfigDto.getAgentConfigDto().getName(), "Agent")); + setFileAkForCustomAgent(agentNodeConfigDto.getAgentConfigDto()); + updateProxyMcp(agentNodeConfigDto.getAgentConfigDto(), conversationDto.getId().toString(), traceContext, false); + } + }); } } } - boolean isMultiAgent = null != agentConfigDto.getAgentComponentConfigList().stream().filter(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent).findFirst().orElse(null); - if (isMultiAgent) { - agentContext.setAgentContextFunction(id -> { - AgentConfigDto agentConfig = agentApplicationService.queryPublishedConfigForExecute(id); - if (agentConfig == null) { - return null; - } - AgentContext context = new AgentContext(); - context.setAgentConfig(agentConfig); - return context; - }); - } - //附件追加AK if (tryReqDto.getAttachments() != null) { tryReqDto.getAttachments().forEach(attachmentDto -> attachmentDto.setFileUrl(iFileAccessService.getFileUrlWithAk(attachmentDto.getFileUrl(), true))); @@ -1327,33 +1435,111 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi agentStopped = true; } else if (modelChanged) { sandboxAgentClient.agentStop(agentContext.getConversationId()); - } - - List skillIds = agentContext.getAgentConfig().getAgentComponentConfigList().stream().filter(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.Skill).map(AgentComponentConfigDto::getTargetId).collect(Collectors.toList()); - List updatedSkills = agentContext.getAgentConfig().getAgentComponentConfigList().stream().filter(agentComponentConfigDto -> - // 技能发布时间大于会话更新时间 - agentComponentConfigDto.getType() == AgentComponentConfig.Type.Skill && ((SkillConfigDto) agentComponentConfigDto.getTargetConfig()).getPublishDate().getTime() > conversationDto.getModified().getTime() - ).toList(); - if (agentContext.getConversation().getSandboxSessionId() == null || !updatedSkills.isEmpty() || agentStopped) { - log.info("Start creating workspace and pushing skills, agentId {}, conversationId {}, skillIds {}", agentContext.getAgentConfig().getId(), agentContext.getConversationId(), skillIds); - try { - List subagents = null; - AgentComponentConfigDto componentConfigDto = agentContext.getAgentConfig().getAgentComponentConfigList().stream().filter(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.SubAgent).findFirst().orElse(null); - if (componentConfigDto != null && componentConfigDto.getBindConfig() instanceof SubAgentBindConfigDto subAgentBindConfigDto) { - if (CollectionUtils.isNotEmpty(subAgentBindConfigDto.getSubAgents())) { - subagents = subAgentBindConfigDto.getSubAgents().stream().map(subAgentDto -> { - SubagentDto subagentDto = new SubagentDto(); - subagentDto.setName(subAgentDto.getName()); - subagentDto.setContent(subAgentDto.getPrompt()); - return subagentDto; - }).toList(); + } else { + //AgentGroup + for (AgentComponentConfigDto agentComponentConfigDto : agentContext.getAgentConfig().getAgentComponentConfigList()) { + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent) { + if (agentComponentConfigDto.getTargetConfig() instanceof AgentConfigDto agentConfigDto0) { + if (agentConfigDto0.getModified().getTime() > agentContext.getConversation().getModified().getTime()) { + sandboxAgentClient.agentStop(agentContext.getConversationId()); + agentStopped = true; + break; + } } } + } + //AgentFlow + if (AgentConfigDto.AgentSubType.Flow.name().equals(agentConfigDto.getSubType())) { + for (AgentComponentConfigDto agentComponentConfigDto : agentContext.getAgentConfig().getAgentComponentConfigList()) { + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Workflow && agentComponentConfigDto.getTargetConfig() instanceof WorkflowConfigDto workflowConfigDto) { + if (devMode) { + List workflowNodeCheckDtos = workflowApplicationService.validWorkflow(workflowConfigDto.getId()); + if (CollectionUtils.isNotEmpty(workflowNodeCheckDtos)) { + Map nodeDtoMap = workflowConfigDto.getNodes().stream().collect(Collectors.toMap(WorkflowNodeDto::getId, node -> node, (o1, o2) -> o1)); + List errorMessages = workflowNodeCheckDtos.stream().filter(workflowNodeCheckDto -> !workflowNodeCheckDto.isSuccess()).map(workflowNodeCheckDto -> { + WorkflowNodeDto workflowNodeDto = nodeDtoMap.get(workflowNodeCheckDto.getNodeId()); + if (workflowNodeDto != null) { + return workflowNodeCheckDto.getMessages().stream().map(message -> "[" + workflowNodeDto.getName() + "]" + message).toList(); + } + return workflowNodeCheckDto.getMessages(); + }).flatMap(List::stream).collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(errorMessages)) { + String join = Joiner.on("\n\n").join(errorMessages); + errorOutput.setError(join); + agentExecuteResult.setError(errorOutput.getError()); + return errorOutput(errorOutput, conversationDto); + } + } + } + for (WorkflowNodeDto workflowNodeDto : workflowConfigDto.getNodes()) { + if (workflowNodeDto.getType() == WorkflowNodeConfig.NodeType.Agent && workflowNodeDto.getNodeConfig() instanceof AgentNodeConfigDto agentNodeConfigDto) { + if (agentNodeConfigDto.getAgentConfigDto() == null) { + errorOutput.setError("Agent configuration is incomplete. Please contact the developer for inspection."); + agentExecuteResult.setError(errorOutput.getError()); + return errorOutput(errorOutput, conversationDto); + } + if (agentNodeConfigDto.getAgentConfigDto().getModified().getTime() > agentContext.getConversation().getModified().getTime()) { + sandboxAgentClient.agentStop(agentContext.getConversationId()); + agentStopped = true; + break; + } + } + } + } + } + } + } + + boolean[] updateSkills = {false}; + List skillIds = new ArrayList<>(); + List subagents = new ArrayList<>(); + Map> hooksConfig = new HashMap<>(); + List hookScripts = new ArrayList<>(); + Map hookArgs = new HashMap<>(); + agentContext.getVariableParams().forEach((key, value) -> { + if (!GlobalVariableEnum.isSystemVariable(key) && !"SANDBOX_ACCESS_KEY".equals(key)) { + hookArgs.put(key, value); + } + }); + //MainAgent + checkIfSkillsUpdated(agentConfigDto, conversationDto, skillIds, subagents, hooksConfig, updateSkills); + //AgentGroup + List agentComponentConfigs = agentContext.getAgentConfig().getAgentComponentConfigList().stream().filter(agentComponentConfigDto -> agentComponentConfigDto.getType() == AgentComponentConfig.Type.Agent).toList(); + agentComponentConfigs.forEach(agentComponentConfigDto -> { + if (agentComponentConfigDto.getTargetConfig() instanceof AgentConfigDto groupAgentConfigDto) { + groupAgentConfigDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(groupAgentConfigDto.getIcon(), groupAgentConfigDto.getName(), "agent")); + setFileAkForCustomAgent(groupAgentConfigDto); + checkIfSkillsUpdated(groupAgentConfigDto, conversationDto, skillIds, subagents, hooksConfig, updateSkills); + } + }); + //AgentFlow + if (AgentConfigDto.AgentSubType.Flow.name().equals(agentConfigDto.getSubType())) { + for (AgentComponentConfigDto agentComponentConfigDto : agentContext.getAgentConfig().getAgentComponentConfigList()) { + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Workflow && agentComponentConfigDto.getTargetConfig() instanceof WorkflowConfigDto workflowConfigDto) { + for (WorkflowNodeDto workflowNodeDto : workflowConfigDto.getNodes()) { + if (workflowNodeDto.getType() == WorkflowNodeConfig.NodeType.Agent && workflowNodeDto.getNodeConfig() instanceof AgentNodeConfigDto agentNodeConfigDto) { + if (agentNodeConfigDto.getAgentConfigDto() == null) { + throw new BizException("Agent configuration is incomplete. Please contact the developer for inspection."); + } + checkIfSkillsUpdated(agentNodeConfigDto.getAgentConfigDto(), conversationDto, skillIds, subagents, hooksConfig, updateSkills); + } + } + } + } + } + + PlatformEnvHookSupport.apply(hookArgs, hooksConfig, hookScripts); + + if (agentContext.getConversation().getSandboxSessionId() == null || updateSkills[0] || agentStopped) { + log.info("Start creating workspace and pushing skills, agentId {}, conversationId {}, skillIds {}", agentContext.getAgentConfig().getId(), agentContext.getConversationId(), skillIds); + try { CreateWorkspaceDto createWorkspaceDto = CreateWorkspaceDto.builder() .userId(agentContext.getUserId()) .cId(conversationDto.getId()) .skillIds(skillIds) .subagents(subagents) + .hooksConfig(hooksConfig) + .hookScripts(hookScripts.isEmpty() ? null : hookScripts) .build(); agentWorkspaceApplicationService.createWorkspace(createWorkspaceDto); } catch (Exception e) { @@ -1383,6 +1569,14 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi if (userAccessKey == null) { userAccessKey = userAccessKeyApiService.newAccessKey(agentContext.getUserId(), UserAccessKeyDto.AKTargetType.Sandbox, agentConfigDto.getId().toString()); } + UserAccessKeyDto.UserAccessKeyConfig userAccessKeyConfig = UserAccessKeyDto.UserAccessKeyConfig.builder() + .isDevMode(devMode != null && devMode ? 1 : 0) + .enabled(true) + .conversationId(tryReqDto.getConversationId().toString()) + .requestId(RequestContext.get().getRequestId()) + .traceContext(traceContext) + .build(); + userAccessKeyApiService.updateUserAccessKeyConfig(userAccessKey.getId(), userAccessKeyConfig); agentContext.getVariableParams().put("SANDBOX_ACCESS_KEY", userAccessKey.getAccessKey()); } @@ -1409,23 +1603,37 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi SimpleJvmHashCache.putHash(agentContext.getRequestId(), "agentExecuteResult", agentContext.getAgentExecuteResult(), DEFAULT_EXPIRE_AFTER_SECONDS); final RequestContext requestContext = RequestContext.get(); + Flux flux; + + //Agent(ChatBot、Custom or General) + flux = agentExecutor.execute(agentContext); - Flux flux = agentExecutor.execute(agentContext); AtomicBoolean isComplete = agentContext.getFinished(); AtomicBoolean sseCanceled = new AtomicBoolean(false); AtomicBoolean userStopped = new AtomicBoolean(false); + AtomicBoolean gitAutoCommitted = new AtomicBoolean(false); AtomicLong lastActiveTime = new AtomicLong(System.currentTimeMillis()); + final boolean autoGitCommitEnabled = isVersionControlEnabled(agentConfigDto); //智能体执行输出 Object finalTargetConfig = targetConfig; Disposable disposable = flux.onErrorResume(Mono::error) + .doOnCancel(() -> { + AgentOutputDto agentOutputDto = AgentExecutor.buildFinalResultOutput(agentContext); + chatMessagePubSubService.publish(agentContext.getConversationId(), JSON.toJSONString(agentOutputDto)); + chatCompleted(requestContext, agentContext, tryReqDto, isComplete, sseCanceled, conversationDto, sink, finalTargetConfig, gitAutoCommitted, autoGitCommitEnabled); + }) .subscribe( msg -> { lastActiveTime.set(System.currentTimeMillis()); + if (autoGitCommitEnabled && isTerminalAgentOutput(msg)) { + tryAutoGitCommit(agentContext, gitAutoCommitted); + } Sinks.EmitResult emitResult = sink.tryEmitNext(msg); if (emitResult.isFailure()) { log.error("Agent output failed, cid {}, error {}", agentContext.getConversationId(), emitResult); } + chatMessagePubSubService.publish(agentContext.getConversationId(), JSON.toJSONString(msg)); }, e -> { isComplete.set(true); @@ -1439,35 +1647,10 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi sink.tryEmitComplete(); pushLogQueue(agentContext, tryReqDto.getFrom(), e.getMessage()); }, - () -> { - log.info("Agent execution completed, cid {}", agentContext.getConversationId()); - // 只统计平台级别模型 - if ("TaskAgent".equals(agentConfigDto.getType()) && finalTargetConfig instanceof ModelConfigDto && ((ModelConfigDto) finalTargetConfig).getScope() == ModelConfig.ModelScopeEnum.Tenant) { - metricRpcService.incrementMetricAllPeriods(agentContext.getTenantConfig().getTenantId(), agentContext.getUserId(), BizType.GENERAL_AGENT_CHAT.getCode(), BigDecimal.ONE); - } - isComplete.set(true); - updateConversationStatus(agentContext, conversationDto, Conversation.ConversationTaskStatus.COMPLETE); - sink.tryEmitComplete(); - pushLogQueue(agentContext, tryReqDto.getFrom(), null); - try { - RequestContext.set(requestContext); - Conversation conversation = conversationDomainService.getConversation(conversationDto.getId()); - if (conversation.getTopicUpdated() == -1 && conversationDto.getDevMode() == 0 && conversationDto.getType() == Conversation.ConversationType.Chat) { - log.info("Update conversation topic, cid {}", agentContext.getConversationId()); - updateTopic(agentContext, conversationDto); - } - if (sseCanceled.get() && conversationDto.getDevMode() == 0 && conversationDto.getType() == Conversation.ConversationType.Chat) { - log.info("Conversation connection disconnected, task execution completed send notification, cid {}, topic {}", agentContext.getConversationId(), conversationDto.getTopic()); - sendNotification(agentContext, conversation); - } - } finally { - RequestContext.remove(); - } - }); + () -> chatCompleted(requestContext, agentContext, tryReqDto, isComplete, sseCanceled, conversationDto, sink, finalTargetConfig, gitAutoCommitted, autoGitCommitEnabled)); //心跳 - Disposable disposableHeartbeat = Flux.interval(Duration.ofSeconds(10)) - .map(tick -> { + Flux.interval(Duration.ofSeconds(10)).map(tick -> { log.info("Heartbeat sent, cid {}, tick {}", agentContext.getConversationId(), tick); if (tick >= 360) {// 一个小时如果都还没有结束,打印详细日志,排查问题 log.warn("Agent execution time too long, cid {}", agentContext.getConversationId()); @@ -1482,6 +1665,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi AgentOutputDto agentOutputDto = new AgentOutputDto(); agentOutputDto.setRequestId(agentContext.getRequestId()); agentOutputDto.setEventType(AgentOutputDto.EventTypeEnum.HEART_BEAT); + chatMessagePubSubService.publish(agentContext.getConversationId(), JSON.toJSONString(agentOutputDto)); return agentOutputDto; }).takeWhile(outputDto -> !isComplete.get()) .doOnComplete(() -> log.info("Heartbeat ended, cid {}", agentContext.getConversationId())) @@ -1493,18 +1677,158 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi return sink.asFlux() .doOnComplete(() -> { log.info("Agent execution SSE channel completed, cid {}", agentContext.getConversationId()); - redisUtil.expire("chat.stop." + agentContext.getConversationId(), 0); + clearChatStopStatus(agentContext.getConversationId()); if (conversationDto.getTaskStatus() == null || conversationDto.getTaskStatus() == Conversation.ConversationTaskStatus.EXECUTING) { updateConversationStatus(agentContext, conversationDto, Conversation.ConversationTaskStatus.COMPLETE); } }) .doOnCancel(() -> { log.info("Agent execution SSE channel cancelled, cid {}", agentContext.getConversationId()); - disposableHeartbeat.dispose(); sseCanceled.set(true); }); } + private void chatCompleted(RequestContext requestContext, AgentContext agentContext, TryReqDto tryReqDto, AtomicBoolean isComplete, + AtomicBoolean sseCanceled, ConversationDto conversationDto, Sinks.Many sink, Object finalTargetConfig, + AtomicBoolean gitAutoCommitted, boolean autoGitCommitEnabled) { + log.info("Agent execution completed, cid {}", agentContext.getConversationId()); + AgentConfigDto agentConfigDto = agentContext.getAgentConfig(); + // 只统计平台级别模型 + if ("TaskAgent".equals(agentConfigDto.getType()) && finalTargetConfig instanceof ModelConfigDto && ((ModelConfigDto) finalTargetConfig).getScope() == ModelConfig.ModelScopeEnum.Tenant) { + metricRpcService.incrementMetricAllPeriods(agentContext.getTenantConfig().getTenantId(), agentContext.getUserId(), BizType.GENERAL_AGENT_CHAT.getCode(), BigDecimal.ONE); + } + isComplete.set(true); + if (autoGitCommitEnabled) { + tryAutoGitCommit(agentContext, gitAutoCommitted); + } + updateConversationStatus(agentContext, conversationDto, Conversation.ConversationTaskStatus.COMPLETE); + sink.tryEmitComplete(); + pushLogQueue(agentContext, tryReqDto.getFrom(), null); + try { + RequestContext.set(requestContext); + Conversation conversation = conversationDomainService.getConversation(conversationDto.getId()); + if (conversation.getTopicUpdated() == -1 && conversationDto.getDevMode() == 0 && conversationDto.getType() == Conversation.ConversationType.Chat) { + log.info("Update conversation topic, cid {}", agentContext.getConversationId()); + updateTopic(agentContext, conversationDto); + } + if (sseCanceled.get() && conversationDto.getDevMode() == 0 && conversationDto.getType() == Conversation.ConversationType.Chat) { + log.info("Conversation connection disconnected, task execution completed send notification, cid {}, topic {}", agentContext.getConversationId(), conversationDto.getTopic()); + sendNotification(agentContext, conversation); + } + } finally { + RequestContext.remove(); + } + } + + private boolean isPrivateTaskAgent(AgentDetailDto agentDetailDto) { + return agentDetailDto != null + && AgentConfigDto.AgentType.TaskAgent.name().equals(agentDetailDto.getType()) + && agentDetailDto.getSandboxId() != null; + } + + private boolean isPrivateTaskAgent(AgentConfigDto agentConfigDto) { + if (agentConfigDto == null + || !AgentConfigDto.AgentType.TaskAgent.name().equals(agentConfigDto.getType())) { + return false; + } + if (Objects.equals(agentConfigDto.getAllowPrivateSandbox(), YesOrNoEnum.Y.getKey())) { + return true; + } + Map extra = agentConfigDto.getExtra(); + return extra != null + && (Boolean.parseBoolean(String.valueOf(extra.get("private"))) + || extra.get("sandboxId") != null); + } + + private void setFileAkForCustomAgent(AgentConfigDto agentConfigDto) { + List publishVersion = agentConfigDto.getPublishVersion(); + if (publishVersion != null && !publishVersion.isEmpty()) { + publishVersion.forEach(publishVersionDto -> { + if (publishVersionDto.getPackages() != null) { + publishVersionDto.getPackages().forEach(packageArtifact -> packageArtifact.setUrl(iFileAccessService.getFileUrlWithAk(packageArtifact.getUrl(), true))); + } + }); + } + } + + private void updateProxyMcp(AgentConfigDto agentConfigDto, String conversationId, TraceContext traceContext, boolean isDev) { + if (!AgentConfigDto.AgentSubType.General.name().equals(agentConfigDto.getSubType()) && !AgentConfigDto.AgentSubType.Custom.name().equals(agentConfigDto.getSubType())) { + return; + } + List mcpToolComponentList = agentConfigDto.getAgentComponentConfigList().stream().filter(agentComponentConfig -> { + if (agentComponentConfig.getType() == AgentComponentConfig.Type.Mcp && agentComponentConfig.getTargetConfig() instanceof McpDto mcpDto) { + return mcpDto.getInstallType() == InstallTypeEnum.COMPONENT; + } + return agentComponentConfig.getType() == AgentComponentConfig.Type.Plugin || agentComponentConfig.getType() == AgentComponentConfig.Type.Knowledge + || agentComponentConfig.getType() == AgentComponentConfig.Type.Table || agentComponentConfig.getType() == AgentComponentConfig.Type.Workflow; + }).toList(); + if (mcpToolComponentList.isEmpty()) { + return; + } + if (isDev) { + agentApplicationService.buildProxyMcp(agentConfigDto, true); + } + Map extra = agentConfigDto.getExtra(); + if (extra != null && (extra.containsKey("devProxyMcpId") || extra.containsKey("prodProxyMcpId"))) { + try { + Long proxyMcpId = Long.parseLong(agentConfigDto.getExtra().get(isDev ? "devProxyMcpId" : "prodProxyMcpId").toString()); + UserAccessKeyDto.UserAccessKeyConfig userAccessKeyConfig = UserAccessKeyDto.UserAccessKeyConfig.builder() + .isDevMode(isDev ? 1 : 0) + .enabled(true) + .conversationId(conversationId) + .requestId(RequestContext.get().getRequestId()) + .traceContext(traceContext) + .build(); + + String exportMcpServerConfig = mcpRpcService.getExportMcpServerConfig(traceContext.getUserId(), proxyMcpId, userAccessKeyConfig); + agentConfigDto.setProxyMcpServerConfig(exportMcpServerConfig); + } catch (Exception e) { + // ignore + log.warn("Failed to get export mcp server config", e); + } + } + } + + private void checkIfSkillsUpdated(AgentConfigDto groupAgentConfigDto, ConversationDto conversationDto, List skillIds, List subagents, Map> hooksConfig, boolean[] updateSkills) { + groupAgentConfigDto.getAgentComponentConfigList().forEach(agentComponentConfigDto0 -> { + if (agentComponentConfigDto0.getType() == AgentComponentConfig.Type.Skill) { + if (!skillIds.contains(agentComponentConfigDto0.getTargetId())) { + skillIds.add(agentComponentConfigDto0.getTargetId()); + } + if (((SkillConfigDto) agentComponentConfigDto0.getTargetConfig()).getPublishDate().getTime() > conversationDto.getModified().getTime()) { + updateSkills[0] = true; + } + } + + if (agentComponentConfigDto0.getType() == AgentComponentConfig.Type.SubAgent) { + if (agentComponentConfigDto0.getBindConfig() instanceof SubAgentBindConfigDto subAgentBindConfigDto) { + if (CollectionUtils.isNotEmpty(subAgentBindConfigDto.getSubAgents())) { + subagents.addAll(subAgentBindConfigDto.getSubAgents().stream().map(subAgentDto -> { + SubagentDto subagentDto = new SubagentDto(); + subagentDto.setName(subAgentDto.getName()); + subagentDto.setContent(subAgentDto.getPrompt()); + return subagentDto; + }).toList()); + } + } + } + + if (agentComponentConfigDto0.getType() == AgentComponentConfig.Type.Hook) { + if (agentComponentConfigDto0.getBindConfig() instanceof HookConfigDto hookConfigDto) { + if (CollectionUtils.isNotEmpty(hookConfigDto.getHooks())) { + hookConfigDto.getHooks().forEach(hook -> { + List hookEntryDtos = hooksConfig.computeIfAbsent(hook.getEvent(), k -> new ArrayList<>()); + HookEntryDto hookEntryDto = new HookEntryDto(); + hookEntryDto.setMatcher(hook.getMatcher()); + hookEntryDto.setHooks(List.of(JSON.parse(hook.getConfig()))); + hookEntryDtos.add(hookEntryDto); + }); + } + } + } + }); + } + private PriceEstimate estimatePrice(AgentConfigDto agentConfigDto, TryReqDto tryReqDto, boolean devMode, boolean isDefaultModel) { TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig(); if (tenantConfigDto.getEnableSubscription() == null || tenantConfigDto.getEnableSubscription() == 0) { @@ -1550,25 +1874,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi estimateTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.MODEL).targetId(targetId.toString()).build()); } } - agentConfigDto.getAgentComponentConfigList().forEach(componentConfigDto -> { - if (componentConfigDto.getType() == AgentComponentConfig.Type.Skill) { - estimateSkillTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.SKILL).targetId(componentConfigDto.getTargetId().toString()).build()); - } - if (componentConfigDto.getType() == AgentComponentConfig.Type.Plugin) { - estimateTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.PLUGIN).targetId(componentConfigDto.getTargetId().toString()).build()); - } - if (componentConfigDto.getType() == AgentComponentConfig.Type.Mcp) { - estimateTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.MCP).targetId(componentConfigDto.getTargetId().toString()).build()); - } - if (componentConfigDto.getType() == AgentComponentConfig.Type.Workflow) { - estimateTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.WORKFLOW).targetId(componentConfigDto.getTargetId().toString()).build()); - Object targetConfig = componentConfigDto.getTargetConfig(); - if (targetConfig instanceof WorkflowConfigDto) { - resourcePricingRpcService.completeWorkflowEstimateTargets(estimateTargets, (WorkflowConfigDto) targetConfig, new HashSet<>()); - } - } - }); - + resourcePricingRpcService.completeAgentEstimate(agentConfigDto, estimateTargets, estimateSkillTargets, new HashSet<>()); PriceEstimate priceEstimate = resourcePricingRpcService.estimatePrice(RequestContext.get().getTenantId(), agentConfigDto.getCreatorId(), estimateSkillTargets); if (!priceEstimate.isPass()) { String message = I18nUtil.systemMessage("Backend.Chat.Error.SkillSubscriptionExpiredContactDeveloper", priceEstimate.getTargetId()); @@ -1611,7 +1917,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi ChatMessageDto chatMessageDto = ChatMessageDto.builder() .role(ChatMessageDto.Role.ASSISTANT) .type(MessageTypeEnum.CHAT) - .text("**" + errorOutput.getError() + "**") + .text(errorOutput.getError()) .finished(true) .finishReason("ERROR") .build(); @@ -1623,6 +1929,13 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi conversationDto.setTaskStatus(conversationTaskStatus); Conversation conversationUpdate = new Conversation(); conversationUpdate.setTaskStatus(conversationTaskStatus); + //从agentContext.getVariableParams()中获取非系统变量的内容 + conversationUpdate.setVariables(new HashMap<>()); + agentContext.getVariableParams().forEach((key, value) -> { + if (!GlobalVariableEnum.isSystemVariable(key) && !"SANDBOX_ACCESS_KEY".equals(key)) { + conversationUpdate.getVariables().put(key, value); + } + }); TenantFunctions.callWithIgnoreCheck(() -> conversationDomainService.updateConversation(conversationDto.getUserId(), conversationDto.getId(), conversationUpdate)); // 发送事件消息 @@ -1632,6 +1945,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi .event(Map.of("conversationId", conversationDto.getId().toString(), "requestId", agentContext.getRequestId(), "status", conversationTaskStatus)) .build(); notifyMessageApplicationService.publishEvent(agentContext.getUserId(), event); + chatMessagePubSubService.finishPublish(conversationDto.getId().toString()); } } @@ -1646,6 +1960,61 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi } } + private boolean isVersionControlEnabled(AgentConfigDto executingAgentConfig) { + if (executingAgentConfig == null || "ChatBot".equals(executingAgentConfig.getType())) { + return false; + } + return YesOrNoEnum.Y.getKey().equals(executingAgentConfig.getEnableVersionControl()); + } + + private static boolean isTerminalAgentOutput(AgentOutputDto msg) { + if (msg.getEventType() == AgentOutputDto.EventTypeEnum.FINAL_RESULT) { + return true; + } + if (msg.getEventType() == AgentOutputDto.EventTypeEnum.MESSAGE && msg.getData() instanceof ChatMessageDto chatMessage) { + return chatMessage.isFinished(); + } + return false; + } + + private void tryAutoGitCommit(AgentContext agentContext, AtomicBoolean gitCommitted) { + if (!gitCommitted.compareAndSet(false, true)) { + return; + } + autoGitCommit(agentContext); + } + + /** + * TaskAgent 执行完成后,检测是否有文件编辑操作,自动 git add + commit + */ + private void autoGitCommit(AgentContext agentContext) { + if (!isVersionControlEnabled(agentContext.getAgentConfig())) { + return; + } + try { + log.info("Auto git commit check file edits, cid={}", agentContext.getConversationId()); + List results = agentContext.getAgentExecuteResult().getComponentExecuteResults(); + boolean hasFileEdits = results.stream().anyMatch(r -> "edit".equals(r.getKind())); + if (!hasFileEdits) { + return; + } + Long cId = Long.valueOf(agentContext.getConversationId()); + Long userId = agentContext.getUserId(); + String authorName = agentContext.getUserName(); + String message = "auto commit: " + (agentContext.getOriginalMessage() != null + ? agentContext.getOriginalMessage().replaceAll("[\\r\\n]+", " ").trim() + : "Agent file changes"); + if (message.length() > 200) { + message = message.substring(0, 200); + } + log.info("Auto git commit start calling, cid={}", agentContext.getConversationId()); + gitRpcClient.commitTaskAgent(cId, userId, message, null, authorName, null); + log.info("Auto git commit success, cid={}", agentContext.getConversationId()); + } catch (Exception e) { + log.warn("Auto git commit failed, cid={}", agentContext.getConversationId(), e); + } + } + private void sendNotification(AgentContext agentContext, Conversation conversation) { if (StringUtils.isNotBlank(conversation.getTopic()) && conversation.getTopicUpdated() == 1) { String content = I18nUtil.systemMessage(agentContext.getUser().getLangMap(), "Backend.Chat.Notification.ConversationCompleted", @@ -1666,29 +2035,19 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi if (disposable.isDisposed() || isComplete.get()) { return; } - Object o = redisUtil.get("chat.stop." + agentContext.getConversationId()); - if (o != null) { - Long stopTime = null; - try { - stopTime = Long.parseLong(o.toString()); - } catch (NumberFormatException e) { - // - } + if (isChatStop(agentContext.getConversationId())) { userStopped.set(true); //5秒内没有停止就强行停止 - if (stopTime != null && System.currentTimeMillis() - stopTime > 1000 * 5) { - redisUtil.expire("chat.stop." + agentContext.getConversationId(), 0); - isComplete.set(true); - AgentOutputDto finalOutput = AgentExecutor.buildFinalResultOutput(agentContext); - boolean failure = sink.tryEmitNext(finalOutput).isFailure(); - if (failure) { - log.warn("final_result send failed"); - } - sink.tryEmitComplete(); - disposable.dispose(); - } else { - agentContext.setInterrupted(true); + clearChatStopStatus(agentContext.getConversationId()); + isComplete.set(true); + AgentOutputDto finalOutput = AgentExecutor.buildFinalResultOutput(agentContext); + boolean failure = sink.tryEmitNext(finalOutput).isFailure(); + if (failure) { + log.warn("final_result send failed"); } + sink.tryEmitComplete(); + agentContext.setInterrupted(true); + disposable.dispose(); } checkIfRequestStop(agentContext, sink, disposable, isComplete, userStopped); }, 1); @@ -1703,6 +2062,7 @@ public class ConversationApplicationServiceImpl extends AbstractTaskExecuteServi SimpleJvmHashCache.removeHashAll(agentContext.getRequestId()); if (agentExecuteResult == null) { agentExecuteResult = new AgentExecuteResult(); + agentExecuteResult.setSuccess(true); } LogDocument logDocument = LogDocument.builder() .input(hashAll.get("userMessage") != null ? hashAll.get("userMessage").toString() : null) diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ModelApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ModelApplicationServiceImpl.java index 4983c5a7..6719f3a3 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ModelApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ModelApplicationServiceImpl.java @@ -8,10 +8,12 @@ import com.xspaceagi.agent.core.adapter.application.ModelApplicationService; import com.xspaceagi.agent.core.adapter.dto.CodeCheckResultDto; import com.xspaceagi.agent.core.adapter.dto.CreatorDto; import com.xspaceagi.agent.core.adapter.dto.ModelQueryDto; +import com.xspaceagi.agent.core.adapter.dto.SortUpdateDTO; import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; import com.xspaceagi.agent.core.adapter.repository.ModelConfigRepository; import com.xspaceagi.agent.core.adapter.repository.entity.ModelConfig; import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.infra.component.model.FunctionToolCallback; import com.xspaceagi.agent.core.infra.component.model.ModelClientFactory; import com.xspaceagi.agent.core.infra.component.model.ModelInvoker; import com.xspaceagi.agent.core.infra.rpc.ResourcePricingRpcService; @@ -57,16 +59,15 @@ import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.converter.BeanOutputConverter; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.metadata.DefaultToolMetadata; import org.springframework.beans.BeanUtils; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @@ -240,7 +241,7 @@ public class ModelApplicationServiceImpl implements ModelApplicationService { @Override public List queryTenantModelConfigList(Integer accessControlStatus) { - return queryModelConfigList(new LambdaQueryWrapper().eq(ModelConfig::getScope, ModelConfig.ModelScopeEnum.Tenant).eq(accessControlStatus != null, ModelConfig::getAccessControl, accessControlStatus).orderByDesc(ModelConfig::getId)); + return queryModelConfigList(new LambdaQueryWrapper().eq(ModelConfig::getScope, ModelConfig.ModelScopeEnum.Tenant).eq(accessControlStatus != null, ModelConfig::getAccessControl, accessControlStatus).orderByAsc(ModelConfig::getSort).orderByDesc(ModelConfig::getCreated)); } @Override @@ -360,6 +361,7 @@ public class ModelApplicationServiceImpl implements ModelApplicationService { } private List queryModelConfigList(LambdaQueryWrapper queryWrapper) { + queryWrapper.orderByAsc(ModelConfig::getSort).orderByDesc(ModelConfig::getCreated); List modelDtos = new ArrayList<>(); modelRepository.list(queryWrapper).forEach(model1 -> modelDtos.add(convertToModelDto(model1))); // creatorIdList in modelDtos @@ -454,38 +456,53 @@ public class ModelApplicationServiceImpl implements ModelApplicationService { throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentModelJsonInvalid); } modelConfig.setIsReasonModel(0); - ChatClient.StreamResponseSpec stream = modelClientFactory.createChatClient(modelConfig) + ToolCallback jsonOutputToolCallback = FunctionToolCallback + .builder("json_output", (input, context) -> JSON.toJSONString(input)) + .description("Generate JSON based on the context. When this tool appears, it indicates that the user intends to invoke it to extract JSON content; please invoke it finally") + .inputSchema(jsonSchema) + .inputType(HashMap.class) + .toolMetadata(DefaultToolMetadata.builder().returnDirect(true).build()) + .toolCallResultConverter((res, returnType) -> (res instanceof String) ? (String) res : JSON.toJSONString(res)) + .build(); + ChatClient.StreamResponseSpec stream = modelClientFactory.createChatClient(modelConfig, List.of(jsonOutputToolCallback)) .prompt(new Prompt(new SystemMessage(sysPrompt), new UserMessage(prompt))).stream(); StringBuilder responseStr = new StringBuilder(); - Mono.create(sink -> stream.chatResponse().onErrorResume(throwable -> { - log.warn("call error", throwable); - if (throwable instanceof TimeoutException) { - return Mono.error(new TimeoutException("Large model execution timeout")); - } - return Mono.error(throwable); - }) - .doOnComplete(sink::success).doOnError(sink::error) - .subscribe(chatResponse -> { - Generation result = chatResponse.getResult(); - if (result == null || result.getOutput() == null) { - return; - } - AssistantMessage assistantMessage = result.getOutput(); - if (assistantMessage != null && assistantMessage.getText() != null) { - responseStr.append(assistantMessage.getText()); - } - if (assistantMessage != null && assistantMessage.getMetadata() != null) { - Object finishReason = assistantMessage.getMetadata().get("finishReason"); - if (finishReason != null && finishReason.toString().equals("STOP")) { - sink.success(); + try { + Mono.create(sink -> stream.chatResponse().onErrorResume(throwable -> { + log.warn("call error", throwable); + if (throwable instanceof TimeoutException) { + return Mono.error(new TimeoutException("Large model execution timeout")); } - } - })) - .block(); + return Mono.error(throwable); + }) + .doOnComplete(sink::success).doOnError(sink::error) + .subscribe(chatResponse -> { + Generation result = chatResponse.getResult(); + if (result == null || result.getOutput() == null) { + return; + } + AssistantMessage assistantMessage = result.getOutput(); + if (assistantMessage != null && assistantMessage.getText() != null) { + responseStr.append(assistantMessage.getText()); + } + if (assistantMessage != null && assistantMessage.getMetadata() != null) { + Object finishReason = assistantMessage.getMetadata().get("finishReason"); + if (finishReason != null && finishReason.toString().equals("STOP")) { + sink.success(); + } + } + })) + .block(); + } catch (Exception e) { + log.warn("Model call error", e); + return call(modelConfig, sysPrompt, responseStr.toString(), type, retry + 1); + } String jsonText = ModelInvoker.getJSONText(responseStr.toString()); if (!JSON.isValid(jsonText)) { + log.warn("Model returned JSON format is incorrect, please check prompt\nOriginal content {}\nReturned content:{}", prompt, responseStr); return call(modelConfig, sysPrompt, jsonText, type, retry + 1); } + log.debug("Model call Original content {}, jsonText: {}", prompt, jsonText); Object object = JSON.parseObject(jsonText, type.getType()); if (object == null) { log.warn("Model returned JSON format is incorrect, please check prompt\nOriginal content {}\nReturned content:{}", prompt, responseStr); @@ -501,7 +518,7 @@ public class ModelApplicationServiceImpl implements ModelApplicationService { public String call(String userPrompt) { ModelConfigDto modelConfig = queryDefaultModelConfig(); - return modelClientFactory.createChatClient(modelConfig).prompt() + return modelClientFactory.createChatClient(modelConfig, List.of()).prompt() .user(userPrompt) .call().content(); } @@ -596,7 +613,7 @@ public class ModelApplicationServiceImpl implements ModelApplicationService { return null; } else { // Non-vector model, use chat interface for testing - ChatClient chatClient = modelClientFactory.createChatClient(modelConfig); + ChatClient chatClient = modelClientFactory.createChatClient(modelConfig, List.of()); Flux chatResponseFlux = chatClient.prompt() .user(testPrompt) .stream().chatResponse(); @@ -656,4 +673,15 @@ public class ModelApplicationServiceImpl implements ModelApplicationService { }); return modelConfigs; } + + @Override + public void updateModelSort(List updateDTOS) { + List modelConfigs = updateDTOS.stream().map(updateDTO -> { + ModelConfig model = new ModelConfig(); + model.setId(updateDTO.getId()); + model.setSort(updateDTO.getSort()); + return model; + }).collect(Collectors.toList()); + modelRepository.updateBatchById(modelConfigs); + } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/PluginApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/PluginApplicationServiceImpl.java index d3cfb2f9..5a28940d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/PluginApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/PluginApplicationServiceImpl.java @@ -90,6 +90,7 @@ public class PluginApplicationServiceImpl implements PluginApplicationService { pluginConfig.setIcon(pluginUpdateDto.getIcon()); pluginConfig.setDescription(pluginUpdateDto.getDescription()); pluginConfig.setName(pluginUpdateDto.getName()); + pluginConfig.setDevAgentConversationId(pluginUpdateDto.getDevAgentConversationId()); if (pluginUpdateDto.getConfig() != null) { pluginConfig.setConfig(JSON.toJSONString(pluginUpdateDto.getConfig())); } @@ -380,6 +381,7 @@ public class PluginApplicationServiceImpl implements PluginApplicationService { .enableSubscription(tenantConfigDto.getEnableSubscription() != null && tenantConfigDto.getEnableSubscription() == 1) .billUserId(RequestContext.get().getUserId()) .devTest(pluginExecuteRequestDto.isTest()) + .apiKey(pluginExecuteRequestDto.getApiKey()) .traceTargets(Lists.newArrayList(TraceContext.TraceTarget.builder() .targetType(TraceContext.TraceTargetType.Plugin) .name(pluginDto.getName()) diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ProjectApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ProjectApplicationServiceImpl.java new file mode 100644 index 00000000..e942c8cf --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ProjectApplicationServiceImpl.java @@ -0,0 +1,178 @@ +package com.xspaceagi.agent.core.application.service; + +import com.xspaceagi.agent.core.adapter.application.*; +import com.xspaceagi.agent.core.adapter.dto.*; +import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; +import com.xspaceagi.agent.core.spec.enums.CodeLanguageEnum; +import com.xspaceagi.agent.core.spec.enums.PluginTypeEnum; +import com.xspaceagi.custompage.sdk.ICustomPageRpcService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class ProjectApplicationServiceImpl implements ProjectApplicationService { + + @Resource + private AgentApplicationService agentApplicationService; + + @Resource + private ConversationApplicationService conversationApplicationService; + + @Resource + private SkillApplicationService skillApplicationService; + + @Resource + private PluginApplicationService pluginApplicationService; + + @Resource + private ICustomPageRpcService iCustomPageRpcService; + + @Resource + private AgentWorkspaceApplicationService agentWorkspaceApplicationService; + + @Resource + private RecommendApplicationService recommendApplicationService; + + @Transactional + @Override + public ProjectCreateResultDTO createProject(ProjectCreateDTO projectCreateDTO) { + List targetRecommendResponses = recommendApplicationService.list(TargetRecommend.RecType.ChatBoxNav.name(), TargetRecommend.TargetType.Agent.name()); + Map recommendResponseMap = targetRecommendResponses.stream().collect(Collectors.toMap(TargetRecommendResponse::getFunctionType, targetRecommendResponse -> targetRecommendResponse, (old, v) -> v)); + if (projectCreateDTO.getTargetType() == Published.TargetType.Agent) { + return createProjectForAgent(projectCreateDTO, recommendResponseMap.get(TargetRecommend.FunctionType.AgentDev.name())); + } + + if (projectCreateDTO.getTargetType() == Published.TargetType.Skill) { + return createProjectForSkill(projectCreateDTO, recommendResponseMap.get(TargetRecommend.FunctionType.SkillDev.name())); + } + + if (projectCreateDTO.getTargetType() == Published.TargetType.Plugin) { + return createProjectForPlugin(projectCreateDTO, recommendResponseMap.get(TargetRecommend.FunctionType.PluginDev.name())); + } + + if (projectCreateDTO.getTargetType() == Published.TargetType.PageApp) { + return createProjectForCustomPage(projectCreateDTO, recommendResponseMap.get(TargetRecommend.FunctionType.PageAppDev.name())); + } + + throw new IllegalArgumentException("Invalid target type"); + } + + private ProjectCreateResultDTO createProjectForCustomPage(ProjectCreateDTO projectCreateDTO, TargetRecommendResponse targetRecommendResponse) { + Assert.notNull(targetRecommendResponse, "Develop agent not config"); + String projectId = iCustomPageRpcService.create(projectCreateDTO.getCreatorId(), projectCreateDTO.getSpaceId(), projectCreateDTO.getName()); + ProjectCreateResultDTO projectCreateResultDTO = new ProjectCreateResultDTO(); + projectCreateResultDTO.setTargetId(projectId); + projectCreateResultDTO.setTargetType(Published.TargetType.PageApp); + projectCreateResultDTO.setConversationId(null); + conversationApplicationService.createConversationForProjectDevelopment(targetRecommendResponse.getTenantId(), + projectCreateDTO.getCreatorId(), projectCreateDTO.getSpaceId(), targetRecommendResponse.getTargetId(), Published.TargetType.PageApp.name(), Long.parseLong(projectId)); + return projectCreateResultDTO; + } + + private ProjectCreateResultDTO createProjectForPlugin(ProjectCreateDTO projectCreateDTO, TargetRecommendResponse targetRecommendResponse) { + Assert.notNull(targetRecommendResponse, "Develop agent not config"); + PluginAddDto pluginConfigDto = new PluginAddDto(); + pluginConfigDto.setName(projectCreateDTO.getName()); + pluginConfigDto.setDescription(projectCreateDTO.getName()); + pluginConfigDto.setType(PluginTypeEnum.CODE); + pluginConfigDto.setCodeLang(CodeLanguageEnum.JavaScript); + pluginConfigDto.setCreatorId(projectCreateDTO.getCreatorId()); + pluginConfigDto.setSpaceId(projectCreateDTO.getSpaceId()); + Long pluginId = pluginApplicationService.add(pluginConfigDto); + Long devAgentConversationId = conversationApplicationService.createConversationForProjectDevelopment(targetRecommendResponse.getTenantId(), + projectCreateDTO.getCreatorId(), projectCreateDTO.getSpaceId(), targetRecommendResponse.getTargetId(), Published.TargetType.Plugin.name(), pluginId).getId(); + PluginUpdateDto pluginUpdateDto = new PluginUpdateDto<>(); + pluginUpdateDto.setId(pluginId); + pluginUpdateDto.setDevAgentConversationId(devAgentConversationId); + pluginApplicationService.update(pluginUpdateDto); + ProjectCreateResultDTO projectCreateResultDTO = new ProjectCreateResultDTO(); + projectCreateResultDTO.setTargetId(pluginId.toString()); + projectCreateResultDTO.setTargetType(Published.TargetType.Plugin); + projectCreateResultDTO.setConversationId(devAgentConversationId); + return projectCreateResultDTO; + } + + private ProjectCreateResultDTO createProjectForSkill(ProjectCreateDTO projectCreateDTO, TargetRecommendResponse targetRecommendResponse) { + Assert.notNull(targetRecommendResponse, "Develop agent not config"); + SkillConfigDto skillConfigDto = new SkillConfigDto(); + skillConfigDto.setName(projectCreateDTO.getName()); + skillConfigDto.setCreatorId(projectCreateDTO.getCreatorId()); + skillConfigDto.setSpaceId(projectCreateDTO.getSpaceId()); + Long skillId = skillApplicationService.add(skillConfigDto); + Long devSkillConversationId = conversationApplicationService.createConversationForProjectDevelopment(targetRecommendResponse.getTenantId(), + projectCreateDTO.getCreatorId(), projectCreateDTO.getSpaceId(), targetRecommendResponse.getTargetId(), Published.TargetType.Skill.name(), skillId).getId(); + + skillConfigDto = new SkillConfigDto(); + skillConfigDto.setId(skillId); + skillConfigDto.setDevAgentConversationId(devSkillConversationId); + skillApplicationService.update(skillConfigDto, false); + + ProjectCreateResultDTO projectCreateResultDTO = new ProjectCreateResultDTO(); + projectCreateResultDTO.setTargetId(skillId.toString()); + projectCreateResultDTO.setTargetType(Published.TargetType.Skill); + projectCreateResultDTO.setConversationId(devSkillConversationId); + try { + agentWorkspaceApplicationService.initProjectTemplate(InitProjectTemplateDto.builder() + .cId(devSkillConversationId) + .projectType(InitProjectTemplateDto.ProjectType.SKILL) + .userId(projectCreateDTO.getCreatorId()) + .build()); + } catch (Exception e) { + log.error("Init skill template error", e); + if (!log.isDebugEnabled()) { + throw e; + } + } + return projectCreateResultDTO; + } + + private ProjectCreateResultDTO createProjectForAgent(ProjectCreateDTO projectCreateDTO, TargetRecommendResponse targetRecommendResponse) { + Assert.notNull(targetRecommendResponse, "Develop agent not config"); + InitProjectTemplateDto.ProgrammingLanguage programmingLanguage = projectCreateDTO.getProgrammingLanguage() == null ? InitProjectTemplateDto.ProgrammingLanguage.TYPESCRIPT : projectCreateDTO.getProgrammingLanguage(); + AgentConfigDto agentConfigDto = new AgentConfigDto(); + agentConfigDto.setName(projectCreateDTO.getName()); + agentConfigDto.setType(AgentConfigDto.AgentType.TaskAgent.name()); + agentConfigDto.setSubType(AgentConfigDto.AgentSubType.Custom.name()); + agentConfigDto.setExtra(Map.of("createdFrom", "VibeCoding", "programmingLanguage", programmingLanguage)); + agentConfigDto.setSpaceId(projectCreateDTO.getSpaceId()); + agentConfigDto.setCreatorId(projectCreateDTO.getCreatorId()); + Long agentId = agentApplicationService.add(agentConfigDto); + ConversationDto conversationDto = conversationApplicationService.createConversationForProjectDevelopment(targetRecommendResponse.getTenantId(), + projectCreateDTO.getCreatorId(), projectCreateDTO.getSpaceId(), targetRecommendResponse.getTargetId(), Published.TargetType.Agent.name(), agentId); + Long devAgentConversationId = conversationDto.getId(); + agentConfigDto = new AgentConfigDto(); + agentConfigDto.setDevAgentConversationId(devAgentConversationId); + agentConfigDto.setId(agentId); + agentApplicationService.update(agentConfigDto); + conversationApplicationService.createConversation(projectCreateDTO.getCreatorId(), agentId, true); + ProjectCreateResultDTO projectCreateResultDTO = new ProjectCreateResultDTO(); + projectCreateResultDTO.setTargetId(agentId.toString()); + projectCreateResultDTO.setTargetType(Published.TargetType.Agent); + projectCreateResultDTO.setConversationId(devAgentConversationId); + try { + agentWorkspaceApplicationService.initProjectTemplate(InitProjectTemplateDto.builder() + .cId(devAgentConversationId) + .projectType(InitProjectTemplateDto.ProjectType.AGENT) + .programmingLanguage(programmingLanguage) + .userId(projectCreateDTO.getCreatorId()) + .build()); + } catch (Exception e) { + log.error("Init agent template error", e); + if (!log.isDebugEnabled()) { + throw e; + } + } + return projectCreateResultDTO; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/PublishApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/PublishApplicationServiceImpl.java index f7542a99..75fb782b 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/PublishApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/PublishApplicationServiceImpl.java @@ -10,6 +10,7 @@ import com.google.common.collect.Lists; import com.xspaceagi.agent.core.adapter.application.*; import com.xspaceagi.agent.core.adapter.constant.SkillFileFormatConstants; import com.xspaceagi.agent.core.adapter.dto.*; +import com.xspaceagi.agent.core.adapter.dto.config.AgentComponentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto; import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; @@ -22,18 +23,19 @@ import com.xspaceagi.agent.core.infra.rpc.ResourcePricingRpcService; import com.xspaceagi.agent.core.infra.rpc.dto.PageDto; import com.xspaceagi.agent.core.spec.enums.PluginTypeEnum; import com.xspaceagi.agent.core.spec.utils.CopyRelationCacheUtil; +import com.xspaceagi.agent.core.spec.utils.FileTypeUtils; +import com.xspaceagi.agent.core.spec.utils.MarkdownExtractUtil; import com.xspaceagi.file.application.service.FileManagementService; import com.xspaceagi.file.domain.model.FileRecordDomain; import com.xspaceagi.file.sdk.IFileAccessService; import com.xspaceagi.pricing.sdk.dto.PricingConfigDTO; -import com.xspaceagi.system.application.dto.SendNotifyMessageDto; -import com.xspaceagi.system.application.dto.SpaceDto; -import com.xspaceagi.system.application.dto.TenantConfigDto; -import com.xspaceagi.system.application.dto.UserDto; +import com.xspaceagi.system.application.dto.*; import com.xspaceagi.system.application.service.NotifyMessageApplicationService; import com.xspaceagi.system.application.service.SpaceApplicationService; +import com.xspaceagi.system.application.service.SysUserPermissionCacheService; import com.xspaceagi.system.application.service.UserApplicationService; import com.xspaceagi.system.infra.dao.entity.NotifyMessage; +import com.xspaceagi.system.infra.dao.entity.SpaceUser; import com.xspaceagi.system.infra.dao.entity.User; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.PageQueryVo; @@ -44,18 +46,21 @@ import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import com.xspaceagi.system.spec.file.InMemoryMultipartFile; import com.xspaceagi.system.spec.jackson.JsonSerializeUtil; import com.xspaceagi.system.spec.page.SuperPage; +import com.xspaceagi.system.spec.tenant.thread.TenantFunctions; import com.xspaceagi.system.spec.utils.I18nUtil; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -63,6 +68,7 @@ import java.util.zip.ZipOutputStream; import static com.xspaceagi.agent.core.adapter.repository.entity.Published.TargetSubType.ChatBot; import static com.xspaceagi.agent.core.adapter.repository.entity.Published.TargetSubType.PageApp; +import static com.xspaceagi.system.spec.enums.ResourceEnum.*; @Slf4j @Service @@ -115,25 +121,128 @@ public class PublishApplicationServiceImpl implements PublishApplicationService @Resource private PublishedRepository publishedRepository; + @Resource private FileManagementService fileManagementService; + @Resource private IFileAccessService iFileAccessService; + @Resource + private ResourceGroupApplicationService resourceGroupApplicationService; + private static final String TARGET_TYPE_SKILL_PUBLISH_APPLY = "skill_publish_apply"; private static final String TARGET_TYPE_SKILL_PUBLISHED = "skill_published"; @Resource private ResourcePricingRpcService resourcePricingRpcService; + @Resource + private SysUserPermissionCacheService sysUserPermissionCacheService; + + @Resource + private ModelApplicationService modelApplicationService; + + @Resource + private AgentWorkspaceApplicationService agentWorkspaceApplicationService; + @Override public SuperPage queryPublishedList(PublishedQueryDto publishedQueryDto) { SuperPage publishedList = publishDomainService.queryPublishedList(publishedQueryDto); - var dataList = convertPublishedList(publishedQueryDto.getTargetType(), publishedList.getRecords()); + var dataList = convertPublishedList(publishedQueryDto.getTargetType(), publishedList.getRecords(), publishedQueryDto.isReturnConfig()); return SuperPage.build(publishedList, dataList); } + public List queryGroupedPublishedList(PublishedQueryDto publishedQueryDto) { + int size = publishedQueryDto.obtainPageSize() < 1 ? 10 : publishedQueryDto.obtainPageSize(); + List resultList = List.of(); + int index = 2; + int groupCt = 0; + while (groupCt < size + 1 && index < 10) { + groupCt = 0; + publishedQueryDto.setPageSize(size * index); + SuperPage publishedList = publishDomainService.queryPublishedList(publishedQueryDto); + resultList = publishedList.getRecords(); + if (publishedList.getRecords().size() < size * index) { + break; + } + Set groupIdSet = new HashSet<>(); + for (Published published : resultList) { + if (published.getGroupId() == null) { + groupCt++; + } else if (!groupIdSet.contains(published.getGroupId())) { + groupIdSet.add(published.getGroupId()); + groupCt++; + } + } + index++; + } + List dataList = convertPublishedList(publishedQueryDto.getTargetType(), resultList, false); + //补齐资源分组信息 + List groupIdList = dataList.stream().map(PublishedDto::getGroupId).filter(Objects::nonNull).toList(); + List resourceGroups; + Map resourceGroupMap; + if (CollectionUtils.isNotEmpty(groupIdList)) { + resourceGroups = new ArrayList<>(resourceGroupApplicationService.queryList(groupIdList)); + resourceGroupMap = resourceGroups.stream().collect(Collectors.toMap(ResourceGroupDto::getId, resourceGroupDto -> resourceGroupDto, (k1, k2) -> k1)); + } else { + resourceGroupMap = new HashMap<>(); + resourceGroups = new ArrayList<>(); + } + + for (PublishedDto publishedDto : dataList) { + if (publishedDto.getGroupId() == null || resourceGroupMap.get(publishedDto.getGroupId()) == null) { + ResourceGroupDto resourceGroupDto = toResourceGroupDto(publishedDto); + resourceGroups.add(resourceGroupDto); + } else { + ResourceGroupDto resourceGroupDto = resourceGroupMap.get(publishedDto.getGroupId()); + if (resourceGroupDto.getTools() == null) { + resourceGroupDto.setTools(new ArrayList<>()); + } + resourceGroupDto.getTools().add(publishedDto); + resourceGroupDto.setToolCount(resourceGroupDto.getToolCount() == null ? 1 : resourceGroupDto.getToolCount() + 1); + resourceGroupDto.setPaymentRequired(resourceGroupDto.isPaymentRequired() || publishedDto.isPaymentRequired()); + resourceGroupDto.setModified(publishedDto.getModified()); + resourceGroupDto.setCreated(publishedDto.getModified()); + resourceGroupDto.setTargetType(publishedDto.getTargetType()); + resourceGroupDto.setTargetId(publishedDto.getTargetId()); + if (resourceGroupDto.getPublishUser() == null) { + resourceGroupDto.setPublishUser(publishedDto.getPublishUser()); + } + } + } + + //resourceGroups数量大于size时移除多余的内容 + if (resourceGroups.size() > size) { + resourceGroups = resourceGroups.subList(0, size); + } + + //resourceGroups 按照modified降序 + resourceGroups.sort(Comparator.comparing(ResourceGroupDto::getModified).reversed()); + + return resourceGroups; + } + + private ResourceGroupDto toResourceGroupDto(PublishedDto publishedDto) { + ResourceGroupDto resourceGroupDto = new ResourceGroupDto(); + resourceGroupDto.setId(-1L); + resourceGroupDto.setSpaceId(publishedDto.getSpaceId()); + resourceGroupDto.setName(publishedDto.getName()); + resourceGroupDto.setDescription(publishedDto.getDescription()); + resourceGroupDto.setIcon(publishedDto.getIcon()); + resourceGroupDto.setType(publishedDto.getTargetType().name()); + resourceGroupDto.setToolCount(1); + resourceGroupDto.setCreated(publishedDto.getCreated()); + resourceGroupDto.setModified(publishedDto.getModified()); + resourceGroupDto.setPaymentRequired(publishedDto.isPaymentRequired()); + resourceGroupDto.setTools(List.of(publishedDto)); + resourceGroupDto.setPublishUser(publishedDto.getPublishUser()); + resourceGroupDto.setTargetType(publishedDto.getTargetType()); + resourceGroupDto.setTargetId(publishedDto.getTargetId()); + return resourceGroupDto; + } + @Override public SuperPage queryPublishedListForAt(PublishedQueryDto publishedQueryDto) { SuperPage publishedPage = publishDomainService.queryPublishedList(publishedQueryDto); @@ -151,14 +260,14 @@ public class PublishApplicationServiceImpl implements PublishApplicationService if (pricingConfigDTO != null && YesOrNoEnum.Y.getKey().equals(pricingConfigDTO.getStatus())) { publishedDto.setPaymentRequired(true); publishedDto.setPrice(pricingConfigDTO.getPrice()); + publishedDto.setPricingType(pricingConfigDTO.getPricingType().getCode()); } return publishedDto; }).toList(); - return new SuperPage<>(publishedPage.getCurrent(), publishedPage.getSize(), publishedPage.getTotal(), dtoList); } - private List convertPublishedList(Published.TargetType targetType, List publishedList) { + private List convertPublishedList(Published.TargetType targetType, List publishedList, boolean returnConfig) { // 从agentPublishedList中获取agentIds List targetIds = publishedList.stream().map(Published::getTargetId).collect(Collectors.toList()); List statisticsList = publishDomainService.queryStatisticsCountList(targetType, targetIds); @@ -172,7 +281,7 @@ public class PublishApplicationServiceImpl implements PublishApplicationService // 补齐收藏状态 Map userTargetRelationMap; - if (RequestContext.get().getUserId() != null && targetType != null) { + if (RequestContext.get() != null && RequestContext.get().getUserId() != null && targetType != null) { List userTargetRelationList = userTargetRelationDomainService .queryUserTargetRelationByTargetIds(RequestContext.get().getUserId(), targetType, UserTargetRelation.OpType.Collect, targetIds); @@ -184,7 +293,7 @@ public class PublishApplicationServiceImpl implements PublishApplicationService } // 补齐封面图 - List agentIdList = publishedList.stream().filter(p -> p.getTargetType() == Published.TargetType.Agent && p.getTargetSubType() == Published.TargetSubType.PageApp) + List agentIdList = publishedList.stream().filter(p -> p.getTargetType() == Published.TargetType.Agent && p.getTargetSubType() == PageApp) .map(Published::getTargetId).toList(); List pageDtoList = customPageRpcService.queryPageListByAgentIds(agentIdList); @@ -218,7 +327,7 @@ public class PublishApplicationServiceImpl implements PublishApplicationService log.error("parse agent config error", e); } // 补齐封面图(仅针对PageApp类型) - if (published.getTargetSubType() == Published.TargetSubType.PageApp) { + if (published.getTargetSubType() == PageApp) { PageDto pageDto = pageDtoMap.get(published.getTargetId()); if (pageDto != null) { if (pageDto.getCoverImg() != null) { @@ -231,11 +340,14 @@ public class PublishApplicationServiceImpl implements PublishApplicationService } } // 不返回config - publishedDto.setConfig(null); + if (!returnConfig) { + publishedDto.setConfig(null); + } PricingConfigDTO pricingConfigDTO = pricingConfigMap.get(published.getTargetId().toString()); if (pricingConfigDTO != null && YesOrNoEnum.Y.getKey().equals(pricingConfigDTO.getStatus())) { publishedDto.setPaymentRequired(true); publishedDto.setPrice(pricingConfigDTO.getPrice()); + publishedDto.setPricingType(pricingConfigDTO.getPricingType().getCode()); } return publishedDto; }).collect(Collectors.toList()); @@ -273,17 +385,27 @@ public class PublishApplicationServiceImpl implements PublishApplicationService List publishedList = publishDomainService.queryPublishedList(targetType, targetIds, kw); //去重publishedList publishedList = publishedList.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Published::getTargetId))), ArrayList::new)); - return convertPublishedList(targetType, publishedList); + return convertPublishedList(targetType, publishedList, false); } finally { RequestContext.removeTenantIgnoreEntity(Published.class); } } + @Override + public List queryPublishedList(Published.TargetType targetType, List targetIds, String kw, boolean returnConfig) { + return TenantFunctions.callWithIgnoreCheck(() -> { + List publishedList = publishDomainService.queryPublishedList(targetType, targetIds, kw); + //去重publishedList + publishedList = publishedList.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Published::getTargetId))), ArrayList::new)); + return convertPublishedList(targetType, publishedList, returnConfig); + }); + } + @Override public IPage queryPublishedListForManage(PublishedQueryDto publishedQueryDto) { IPage publishedIPage = publishDomainService.queryPublishedListForManage(publishedQueryDto); List publishedList = publishedIPage.getRecords(); - List publishedDtos = convertPublishedList(publishedQueryDto.getTargetType(), publishedList); + List publishedDtos = convertPublishedList(publishedQueryDto.getTargetType(), publishedList, false); Map publishedMap = publishedList.stream().collect(Collectors.toMap(Published::getId, published -> published)); publishedDtos.forEach(publishedDto -> { if (publishedDto.getTargetType() == Published.TargetType.Plugin) { @@ -452,18 +574,28 @@ public class PublishApplicationServiceImpl implements PublishApplicationService } return true; }) - .collect(Collectors.toList()); - if (publishApplyList.size() > 0) { + .toList(); + if (!publishApplyList.isEmpty()) { publishApplyList.forEach(publishApply -> publishDomainService.deletePublishedApplyById(publishApply.getId())); } PublishApply publishApply = new PublishApply(); BeanUtils.copyProperties(publishApplyDto, publishApply); publishApply.setPublishStatus(Published.PublishStatus.Applying); + + SkillConfigDto skillConfigDto = null; // 启用 LargeObject 特性,支持序列化包含大文件内容的配置 if (publishApplyDto.getTargetType() == Published.TargetType.Skill) { - SkillConfigDto skillConfigDto = skillApplicationService.queryById(publishApplyDto.getTargetId(), true); - List snapshotFiles = snapshotSkillFiles(skillConfigDto == null ? List.of() : skillConfigDto.getFiles(), - TARGET_TYPE_SKILL_PUBLISH_APPLY, publishApplyDto.getTargetId()); + skillConfigDto = skillApplicationService.queryById(publishApplyDto.getTargetId(), true); + List snapshotFiles; + if (skillConfigDto != null && skillConfigDto.getDevAgentConversationId() != null) { + // 类型2:沙盒开发的技能,从沙箱取回文件 + snapshotFiles = agentWorkspaceApplicationService.snapshotSkillFilesFromSandbox( + skillConfigDto.getCreatorId(), skillConfigDto.getDevAgentConversationId(), TARGET_TYPE_SKILL_PUBLISH_APPLY, publishApplyDto.getTargetId()); + } else { + // 类型1:文件服务中的技能 + snapshotFiles = snapshotSkillFiles(skillConfigDto == null ? List.of() : skillConfigDto.getFiles(), + TARGET_TYPE_SKILL_PUBLISH_APPLY, publishApplyDto.getTargetId()); + } SkillPublishedConfigDto applyConfig = buildSkillConfigPayload(publishApplyDto, snapshotFiles, null); publishApply.setConfig(JSON.toJSONString(applyConfig, JSONWriter.Feature.LargeObject)); } else { @@ -478,8 +610,7 @@ public class PublishApplicationServiceImpl implements PublishApplicationService String hisConfig = publishApply.getConfig(); if (publishApply.getTargetType() == Published.TargetType.Skill) { - SkillConfigDto tempSkill = skillApplicationService.queryById(publishApply.getTargetId(), false); - hisConfig = JsonSerializeUtil.toJSONStringGeneric(tempSkill); + hisConfig = JsonSerializeUtil.toJSONStringGeneric(skillConfigDto); } ConfigHistory configHistory = ConfigHistory.builder() @@ -534,6 +665,10 @@ public class PublishApplicationServiceImpl implements PublishApplicationService @Override @DSTransactional public void publish(Published.TargetType targetType, Long targetId, Published.PublishScope scope, List publishApplies) { + publish(targetType, targetId, scope, publishApplies, true); + } + + private void publish(Published.TargetType targetType, Long targetId, Published.PublishScope scope, List publishApplies, boolean notify) { if (CollectionUtils.isEmpty(publishApplies)) { List publishedList = publishDomainService.queryPublishedList(targetType, List.of(targetId)); //根据scope过滤 @@ -545,6 +680,15 @@ public class PublishApplicationServiceImpl implements PublishApplicationService }); return; } + + Long groupId; + ResourceGroupRelation resourceGroupRelation = resourceGroupApplicationService.queryResourceGroupRelation(targetType.name(), targetId); + if (resourceGroupRelation != null) { + groupId = resourceGroupRelation.getGroupId(); + } else { + groupId = null; + } + Integer accessControlStatus; if (targetType == Published.TargetType.Agent) { AgentConfig agentConfig = agentDomainService.queryById(targetId); @@ -593,6 +737,7 @@ public class PublishApplicationServiceImpl implements PublishApplicationService published.setModified(now); published.setCreated(now); published.setAccessControl(accessControlStatus); + published.setGroupId(groupId); return published; }).collect(Collectors.toList()); PublishApply publishApply = new PublishApply(); @@ -627,12 +772,14 @@ public class PublishApplicationServiceImpl implements PublishApplicationService } } - notifyMessageApplicationService.sendNotifyMessage(SendNotifyMessageDto.builder() - .scope(NotifyMessage.MessageScope.System) - .content(message) - .senderId(RequestContext.get().getUserId()) - .userIds(Arrays.asList(publishApply.getApplyUserId())) - .build()); + if (notify) { + notifyMessageApplicationService.sendNotifyMessage(SendNotifyMessageDto.builder() + .scope(NotifyMessage.MessageScope.System) + .content(message) + .senderId(RequestContext.get().getUserId()) + .userIds(Arrays.asList(publishApply.getApplyUserId())) + .build()); + } } }); @@ -685,6 +832,229 @@ public class PublishApplicationServiceImpl implements PublishApplicationService } } + @Override + public String publishOrApply(PublishApplySubmitDto publishApplySubmitDto) { + //整体有两个地方做权限校验,一是校验有没有权限发布出去;而是校验有没有目标空间的发布权限 + Object targetConfig = checkPermissionAndReturnTargetConfig(publishApplySubmitDto.getTargetType(), publishApplySubmitDto.getTargetId()); + // 按目标类型校验资源权限 + checkPublishResourcePermission(publishApplySubmitDto.getTargetType(), targetConfig); + Assert.notNull(publishApplySubmitDto.getCategory(), "Please select a category"); + if (CollectionUtils.isEmpty(publishApplySubmitDto.getItems())) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentPublishScopeNotSelected); + } + if (targetConfig instanceof PluginDto || targetConfig instanceof WorkflowConfigDto) { + String name = targetConfig instanceof PluginDto + ? ((PluginDto) targetConfig).getName() + : ((WorkflowConfigDto) targetConfig).getName(); + name = name == null ? "" : name; + try { + //判断是否为英文 + name = name.trim().toLowerCase().replace(" ", "_").replace("__", "_"); + if (StringUtils.isNotBlank(name) && !name.matches("[a-zA-Z0-9_]+")) { + EnNameDto enNameDto = modelApplicationService.call(name, new ParameterizedTypeReference() { + }); + if (enNameDto != null && StringUtils.isNotBlank(enNameDto.getEnName())) { + if (targetConfig instanceof PluginDto) { + ((PluginDto) targetConfig).setFunctionName(enNameDto.getEnName()); + } else { + ((WorkflowConfigDto) targetConfig).setFunctionName(enNameDto.getEnName()); + } + } + } + } catch (Exception e) { + //忽略 + log.error("Exception when calling model conversion API", e); + } + } + if (targetConfig instanceof SkillConfigDto skillConfig) { + List files = skillConfig.getFiles(); + if (CollectionUtils.isNotEmpty(files)) { + List keyFiles = files.stream().filter(file -> "SKILL.MD".equalsIgnoreCase(file.getName()) && !Boolean.TRUE.equals(file.getIsDir())).toList(); + if (CollectionUtils.isNotEmpty(keyFiles)) { + for (SkillFileDto file : keyFiles) { + String name = MarkdownExtractUtil.extractFieldValue(file.getContents(), "name"); + if (StringUtils.isNotBlank(name)) { + skillConfig.setEnName(name); + break; + } + } + } + } + } + + TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig(); + String name = null; + String description = null; + String icon = null; + Integer publishAudit = YesOrNoEnum.Y.getKey(); + Long spaceId = null; + String agentType = null; + Object ext = null; + if (publishApplySubmitDto.getTargetType() == Published.TargetType.Agent) { + assert targetConfig instanceof AgentConfigDto; + AgentConfigDto agentConfigDto = (AgentConfigDto) targetConfig; + //私有电脑的agent不允许发布到广场 + if (agentConfigDto.getExtra() != null && agentConfigDto.getExtra().get("private") != null + && publishApplySubmitDto.getItems().stream().anyMatch(item -> item.getScope() == Published.PublishScope.Tenant)) { + throw new BizException(I18nUtil.systemMessage("Backend.Publish.AgentPrivateCannotPublishToSquare")); + } + Map ext0 = new HashMap<>(); + ext0.put("agentType", agentConfigDto.getSubType()); + ext = ext0; + // 构建代理MCP,id存储在agentConfigDto的extra字段中 + agentApplicationService.buildProxyMcp(agentConfigDto, false); + name = agentConfigDto.getName(); + description = agentConfigDto.getDescription(); + icon = agentConfigDto.getIcon(); + spaceId = agentConfigDto.getSpaceId(); + publishAudit = tenantConfigDto.getAgentPublishAudit(); + agentType = agentConfigDto.getType(); + if (AgentConfigDto.AgentSubType.Flow.name().equals(agentConfigDto.getSubType())) { + // AgentFlow发布时将对应的workflow发布 + AgentComponentConfigDto componentConfigDto = agentConfigDto.getAgentComponentConfigList().stream().filter(agentComponentConfig -> agentComponentConfig.getType().equals(AgentComponentConfig.Type.Workflow)).findFirst().orElse(null); + if (componentConfigDto == null) { + throw new BizException(I18nUtil.systemMessage("Backend.Publish.AgentFlowMustHaveWorkflow")); + } + PublishApplySubmitDto publishApplySubmit = new PublishApplySubmitDto(); + publishApplySubmit.setTargetType(Published.TargetType.Workflow); + publishApplySubmit.setTargetId(componentConfigDto.getTargetId()); + publishApplySubmit.setItems(new ArrayList<>()); + publishApplySubmit.setCategory("Other"); + PublishApplySubmitDto.PublishItem publishItem = new PublishApplySubmitDto.PublishItem(); + publishItem.setScope(Published.PublishScope.Space); + publishItem.setAllowCopy(0); + publishItem.setOnlyTemplate(0); + publishItem.setSpaceId(spaceId); + publishApplySubmit.getItems().add(publishItem); + publishOrApply(publishApplySubmit);// 发布workflow + } + } + if (publishApplySubmitDto.getTargetType() == Published.TargetType.Workflow) { + assert targetConfig instanceof WorkflowConfigDto; + WorkflowConfigDto workflowConfigDto = (WorkflowConfigDto) targetConfig; + name = workflowConfigDto.getName(); + description = workflowConfigDto.getDescription(); + icon = workflowConfigDto.getIcon(); + spaceId = workflowConfigDto.getSpaceId(); + publishAudit = tenantConfigDto.getWorkflowPublishAudit(); + Map ext0 = new HashMap<>(); + ext0.put("workflowType", workflowConfigDto.getType()); + ext = ext0; + } + if (publishApplySubmitDto.getTargetType() == Published.TargetType.Plugin) { + assert targetConfig instanceof PluginDto; + PluginDto pluginDto = (PluginDto) targetConfig; + name = pluginDto.getName(); + description = pluginDto.getDescription(); + icon = pluginDto.getIcon(); + spaceId = pluginDto.getSpaceId(); + publishAudit = tenantConfigDto.getPluginPublishAudit(); + } + if (publishApplySubmitDto.getTargetType() == Published.TargetType.Skill) { + assert targetConfig instanceof SkillConfigDto; + SkillConfigDto skillConfigDto = (SkillConfigDto) targetConfig; + name = skillConfigDto.getName(); + description = skillConfigDto.getDescription(); + icon = skillConfigDto.getIcon(); + ext = skillConfigDto.getExt(); + spaceId = skillConfigDto.getSpaceId(); + publishAudit = tenantConfigDto.getSkillPublishAudit(); + } + + List userSpaceIds = obtainAuthSpaceIds(); + //参数检查 + for (PublishApplySubmitDto.PublishItem publishItem : publishApplySubmitDto.getItems()) { + Assert.notNull(publishItem.getScope(), "Publish scope is required"); + if (publishItem.getScope() == Published.PublishScope.Space) { + Assert.notNull(publishItem.getSpaceId(), "Space ID is required"); + SpaceDto spaceDto = spaceApplicationService.queryById(publishItem.getSpaceId()); + if (spaceDto == null) { + throw new BizException(I18nUtil.systemMessage("Backend.Publish.SpaceNotFound", publishItem.getSpaceId().toString())); + } + if (!userSpaceIds.contains(publishItem.getSpaceId())) { + throw new BizException(I18nUtil.systemMessage("Backend.Publish.SpaceNoPermission", publishItem.getSpaceId().toString())); + } + if (!spaceDto.getId().equals(spaceId) && !Objects.equals(spaceDto.getReceivePublish(), YesOrNoEnum.Y.getKey())) { + throw new BizException(I18nUtil.systemMessage("Backend.Publish.SpaceReceivePublishDisabled", publishItem.getSpaceId().toString())); + } + } + } + + List tenantPublishApplyDtos = new ArrayList<>(); + List spacePublishApplyDtos = new ArrayList<>(); + String message = I18nUtil.systemMessage("Backend.Publish.Success"); + + // Agent 类型且有 devAgentConversationId 时,执行打包(版本记录保存在 agent_config 中) + if (publishApplySubmitDto.getTargetType() == Published.TargetType.Agent && targetConfig instanceof AgentConfigDto agentConfigDto) { + Long devAgentConversationId = agentConfigDto.getDevAgentConversationId(); + if (devAgentConversationId != null) { + try { + InitProjectTemplateDto.ProgrammingLanguage programmingLanguage = InitProjectTemplateDto.ProgrammingLanguage.TYPESCRIPT; + if (agentConfigDto.getExtra() != null && agentConfigDto.getExtra().get("programmingLanguage") != null) { + try { + programmingLanguage = InitProjectTemplateDto.ProgrammingLanguage.fromValue(String.valueOf(agentConfigDto.getExtra().get("programmingLanguage"))); + } catch (Exception ignored) { + } + } + PackageAgentDto packageDto = PackageAgentDto.builder() + .userId(RequestContext.get().getUserId()) + .cId(devAgentConversationId) + .agentId(publishApplySubmitDto.getTargetId()) + .programmingLanguage(programmingLanguage) + .build(); + List latestVersions = agentWorkspaceApplicationService.packageAgent(packageDto); + agentConfigDto.setPublishVersion(latestVersions); + + log.info("[publishOrApply] Agent打包完成, targetId={}", publishApplySubmitDto.getTargetId()); + } catch (Exception e) { + log.error("[publishOrApply] Agent打包失败, targetId={}", publishApplySubmitDto.getTargetId(), e); + throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.agentPackageFailed, "Agent package failed: " + e.getMessage()); + } + } + } + + for (PublishApplySubmitDto.PublishItem publishItem : publishApplySubmitDto.getItems()) { + PublishApplyDto publishApplyDto = new PublishApplyDto(); + publishApplyDto.setApplyUser((UserDto) RequestContext.get().getUser()); + publishApplyDto.setTargetType(publishApplySubmitDto.getTargetType()); + if (agentType != null) { + publishApplyDto.setTargetSubType(Published.TargetSubType.valueOf(agentType)); + } + publishApplyDto.setTargetId(publishApplySubmitDto.getTargetId()); + publishApplyDto.setChannels(List.of(Published.PublishChannel.System)); + publishApplyDto.setRemark(publishApplySubmitDto.getRemark()); + publishApplyDto.setName(name); + publishApplyDto.setDescription(description); + publishApplyDto.setIcon(icon); + publishApplyDto.setExt(ext); + publishApplyDto.setTargetConfig(targetConfig); + publishApplyDto.setSpaceId(spaceId); + publishApplyDto.setScope(publishItem.getScope()); + publishApplyDto.setCategory(publishApplySubmitDto.getCategory()); + publishApplyDto.setAllowCopy(publishItem.getAllowCopy()); + publishApplyDto.setOnlyTemplate(publishItem.getOnlyTemplate()); + if (publishItem.getScope() == Published.PublishScope.Space) { + publishApplyDto.setSpaceId(publishItem.getSpaceId()); + } + Long applyId = publishApply(publishApplyDto); + publishApplyDto.setId(applyId); + if (publishItem.getScope() == Published.PublishScope.Space) { + spacePublishApplyDtos.add(publishApplyDto); + } else { + tenantPublishApplyDtos.add(publishApplyDto); + } + } + + if (publishAudit == null || publishAudit.equals(YesOrNoEnum.N.getKey()) || CollectionUtils.isEmpty(tenantPublishApplyDtos)) { + publish(publishApplySubmitDto.getTargetType(), publishApplySubmitDto.getTargetId(), Published.PublishScope.Tenant, tenantPublishApplyDtos); + } else { + message = I18nUtil.systemMessage("Backend.Publish.AuditPending"); + } + boolean notify = !(targetConfig instanceof WorkflowConfigDto workflowConfigDto) || !"AgentFlow".equals(workflowConfigDto.getType()); + publish(publishApplySubmitDto.getTargetType(), publishApplySubmitDto.getTargetId(), Published.PublishScope.Space, spacePublishApplyDtos, notify); + return message; + } + @Override @DSTransactional public Long copyPublish(Long userId, Published.TargetType targetType, Long targetId, Long originalSpaceId, Long targetSpaceId) { @@ -1018,6 +1388,92 @@ public class PublishApplicationServiceImpl implements PublishApplicationService publishedRepository.update(published, updateWrapper); } + + /** + * 根据发布目标类型校验当前用户是否具备相应的发布资源权限 + */ + private void checkPublishResourcePermission(Published.TargetType targetType, Object targetConfig) { + Long userId = RequestContext.get().getUserId(); + if (userId == null || targetType == null || targetConfig == null) { + return; + } + + String resourceCode = null; + if (targetType == Published.TargetType.Agent) { + AgentConfigDto agentConfigDto = (AgentConfigDto) targetConfig; + String type = agentConfigDto.getType(); + if (Published.TargetSubType.PageApp.name().equals(type)) { + resourceCode = PAGE_APP_PUBLISH.getCode(); + } else { + resourceCode = AGENT_PUBLISH.getCode(); + } + } else if (targetType == Published.TargetType.Plugin || targetType == Published.TargetType.Workflow) { + resourceCode = COMPONENT_LIB_PUBLISH.getCode(); + } else if (targetType == Published.TargetType.Skill) { + resourceCode = SKILL_PUBLISH.getCode(); + } + + if (resourceCode != null) { + sysUserPermissionCacheService.checkResourcePermissionAny(userId, List.of(resourceCode)); + } + } + + public Object checkPermissionAndReturnTargetConfig(Published.TargetType targetType, Long targetId) { + Assert.notNull(targetType, "targetType is required"); + Assert.notNull(targetId, "targetId is required"); + Long spaceId = null; + Long creatorId = null; + Object targetConfig = null; + if (targetType == Published.TargetType.Agent) { + AgentConfigDto agentDto = agentApplicationService.queryById(targetId); + if (agentDto == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentNotFoundAlt); + } + spaceId = agentDto.getSpaceId(); + creatorId = agentDto.getCreatorId(); + targetConfig = agentDto; + } + if (targetType == Published.TargetType.Plugin) { + PluginDto pluginDto = pluginApplicationService.queryById(targetId); + if (pluginDto == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentOpenapiPluginNotFound); + } + spaceId = pluginDto.getSpaceId(); + creatorId = pluginDto.getCreatorId(); + targetConfig = pluginDto; + } + if (targetType == Published.TargetType.Workflow) { + WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryById(targetId); + if (workflowConfigDto == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentWorkflowNotFound); + } + spaceId = workflowConfigDto.getSpaceId(); + creatorId = workflowConfigDto.getCreatorId(); + targetConfig = workflowConfigDto; + } + if (targetType == Published.TargetType.Skill) { + SkillConfigDto skillConfigDto = skillApplicationService.queryById(targetId); + if (skillConfigDto == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentOpenapiSkillNotFound); + } + spaceId = skillConfigDto.getSpaceId(); + creatorId = skillConfigDto.getCreatorId(); + targetConfig = skillConfigDto; + } + Assert.notNull(spaceId, "spaceId is required"); + SpaceUserDto spaceUserDto = spaceApplicationService.querySpaceUser(spaceId, RequestContext.get().getUserId()); + if (spaceUserDto == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.permissionDenied); + } + if (creatorId.equals(RequestContext.get().getUserId())) { + return targetConfig; + } + if (spaceUserDto.getRole() == SpaceUser.Role.User) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.permissionDenied); + } + return targetConfig; + } + private SkillPublishedConfigDto buildPublishedSkillConfig(PublishApplyDto publishApplyDto, List applySnapshotFiles) { List publishedSnapshotFiles = snapshotSkillFiles(applySnapshotFiles, TARGET_TYPE_SKILL_PUBLISHED, publishApplyDto.getTargetId()); @@ -1027,6 +1483,14 @@ public class PublishApplicationServiceImpl implements PublishApplicationService } private SkillPublishedConfigDto buildSkillConfigPayload(PublishApplyDto publishApplyDto, List files, String zipFileUrl) { + SkillConfigDto skillConfigDto = null; + Object targetConfig = publishApplyDto.getTargetConfig(); + if (targetConfig instanceof SkillConfigDto) { + skillConfigDto = (SkillConfigDto) targetConfig; + } else { + skillConfigDto = JsonSerializeUtil.parseObject(String.valueOf(targetConfig), SkillConfigDto.class); + } + SkillPublishedConfigDto configDto = new SkillPublishedConfigDto(); configDto.setFormat(SkillFileFormatConstants.SKILL_FILES_V2); configDto.setId(publishApplyDto.getTargetId()); @@ -1035,6 +1499,7 @@ public class PublishApplicationServiceImpl implements PublishApplicationService configDto.setIcon(publishApplyDto.getIcon()); configDto.setFiles(files == null ? List.of() : files); configDto.setZipFileUrl(zipFileUrl); + configDto.setDevAgentConversationId(skillConfigDto.getDevAgentConversationId()); return configDto; } @@ -1128,7 +1593,7 @@ public class PublishApplicationServiceImpl implements PublishApplicationService } byte[] bytes; if (StringUtils.isNotBlank(sourceFile.getContents())) { - bytes = sourceFile.getContents().getBytes(java.nio.charset.StandardCharsets.UTF_8); + bytes = sourceFile.getContents().getBytes(StandardCharsets.UTF_8); } else { if (StringUtils.isBlank(sourceFile.getFileProxyUrl())) { log.warn("invalid skill file index, missing file source, targetType={}, skillId={}, fileName={}", @@ -1240,7 +1705,7 @@ public class PublishApplicationServiceImpl implements PublishApplicationService String folderName = StringUtils.isBlank(skillName) ? "skill" : skillName; String baseDir = folderName.endsWith("/") ? folderName : folderName + "/"; try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ZipOutputStream zos = new ZipOutputStream(baos, java.nio.charset.StandardCharsets.UTF_8)) { + ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) { Set addedEntries = new HashSet<>(); for (SkillFileDto file : files) { if (file == null || StringUtils.isBlank(file.getName())) { @@ -1301,7 +1766,7 @@ public class PublishApplicationServiceImpl implements PublishApplicationService if (idx >= 0 && idx < path.length() - 1) { fileName = path.substring(idx + 1); } - String contentType = fileName.endsWith(".zip") ? "application/zip" : "application/octet-stream"; + String contentType = FileTypeUtils.getContentTypeByFileName(fileName).toString(); InMemoryMultipartFile multipartFile = new InMemoryMultipartFile("file", fileName, contentType, bytes); Long tenantId = RequestContext.get() != null ? RequestContext.get().getTenantId() : null; Long userId = RequestContext.get() != null ? RequestContext.get().getUserId() : null; @@ -1372,4 +1837,13 @@ public class PublishApplicationServiceImpl implements PublishApplicationService } return key; } + + public List obtainAuthSpaceIds() { + var userId = RequestContext.get().getUserId(); + //查询用户有权限的空间,限制访问空间,比如工作流查询全部知识库,要限制用户有权限的空间下的知识库 + var spaceList = this.spaceApplicationService.queryListByUserId(userId); + return spaceList.stream(). + map(SpaceDto::getId) + .toList(); + } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/RecommendApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/RecommendApplicationServiceImpl.java new file mode 100644 index 00000000..477a15bf --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/RecommendApplicationServiceImpl.java @@ -0,0 +1,194 @@ +package com.xspaceagi.agent.core.application.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xspaceagi.agent.core.adapter.application.RecommendApplicationService; +import com.xspaceagi.agent.core.adapter.dto.recommend.RecommendHomeResponse; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendPageRequest; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendSaveRequest; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; +import com.xspaceagi.agent.core.infra.dao.mapper.TargetRecommendMapper; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.enums.ErrorCodeEnum; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class RecommendApplicationServiceImpl implements RecommendApplicationService { + + @Resource + private TargetRecommendMapper targetRecommendMapper; + + @Override + public void save(TargetRecommendSaveRequest request) { + Long tenantId = getTenantId(); + TargetRecommend entity = toEntity(request); + entity.setTenantId(tenantId); + if (entity.getSort() == null) { + entity.setSort(0); + } + targetRecommendMapper.insert(entity); + } + + @Override + public void update(TargetRecommendSaveRequest request) { + if (request.getId() == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGetTenantFailed); + } + Long tenantId = getTenantId(); + TargetRecommend existing = targetRecommendMapper.selectById(request.getId()); + if (existing == null || !existing.getTenantId().equals(tenantId)) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGetTenantFailed); + } + TargetRecommend entity = toEntity(request); + entity.setId(request.getId()); + entity.setTenantId(tenantId); + targetRecommendMapper.updateById(entity); + } + + @Override + public void delete(Long id) { + Long tenantId = getTenantId(); + TargetRecommend existing = targetRecommendMapper.selectById(id); + if (existing == null || !existing.getTenantId().equals(tenantId)) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGetTenantFailed); + } + targetRecommendMapper.deleteById(id); + } + + @Override + public TargetRecommendResponse getById(Long id) { + TargetRecommend entity = targetRecommendMapper.selectById(id); + if (entity == null) { + return null; + } + return toResponse(entity); + } + + @Override + public List page(TargetRecommendPageRequest request) { + Long tenantId = getTenantId(); + Page page = new Page<>(request.getPageNo(), request.getPageSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(TargetRecommend::getTenantId, tenantId) + .eq(StringUtils.isNotBlank(request.getRecType()), + TargetRecommend::getRecType, + request.getRecType() != null ? TargetRecommend.RecType.valueOf(request.getRecType()) : null) + .eq(StringUtils.isNotBlank(request.getTargetType()), + TargetRecommend::getTargetType, + request.getTargetType() != null ? TargetRecommend.TargetType.valueOf(request.getTargetType()) : null) + .orderByAsc(TargetRecommend::getSort) + .orderByDesc(TargetRecommend::getCreated); + IPage result = targetRecommendMapper.selectPage(page, wrapper); + return result.getRecords().stream().map(this::toResponse).collect(Collectors.toList()); + } + + @Override + public List list(String recType, String targetType) { + return targetRecommendMapper.selectList( + new LambdaQueryWrapper() + .eq(TargetRecommend::getTenantId, getTenantId()) + .eq(StringUtils.isNotBlank(recType), TargetRecommend::getRecType, recType != null ? TargetRecommend.RecType.valueOf(recType) : null) + .eq(StringUtils.isNotBlank(targetType), TargetRecommend::getTargetType, targetType != null ? TargetRecommend.TargetType.valueOf(targetType) : null) + .orderByAsc(TargetRecommend::getSort) + .orderByDesc(TargetRecommend::getCreated) + ).stream().map(this::toResponse).collect(Collectors.toList()); + } + + @Override + public void updateSort(Long id, Integer sort) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .eq(TargetRecommend::getId, id) + .set(TargetRecommend::getSort, sort); + targetRecommendMapper.update(null, wrapper); + } + + @Override + public RecommendHomeResponse getRecommendations() { + Long tenantId = getTenantId(); + List all = targetRecommendMapper.selectList( + new LambdaQueryWrapper() + .eq(TargetRecommend::getTenantId, tenantId) + .eq(TargetRecommend::getRecType, TargetRecommend.RecType.Home) + .orderByAsc(TargetRecommend::getSort) + ); + List chatBoxNavAll = targetRecommendMapper.selectList( + new LambdaQueryWrapper() + .eq(TargetRecommend::getTenantId, tenantId) + .eq(TargetRecommend::getRecType, TargetRecommend.RecType.ChatBoxNav) + .orderByAsc(TargetRecommend::getSort) + ); + + Map> recHome = all.stream() + .map(this::toResponse) + .collect(Collectors.groupingBy( + r -> r.getTargetType(), + LinkedHashMap::new, + Collectors.toList() + )); + + Map> recChatBoxNav = chatBoxNavAll.stream() + .map(this::toResponse) + .collect(Collectors.groupingBy( + r -> r.getTargetType(), + LinkedHashMap::new, + Collectors.toList() + )); + + return RecommendHomeResponse.builder() + .recHome(recHome) + .recChatBoxNav(recChatBoxNav) + .build(); + } + + private Long getTenantId() { + Long tenantId = RequestContext.get() != null ? RequestContext.get().getTenantId() : null; + if (tenantId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGetTenantFailed); + } + return tenantId; + } + + private TargetRecommend toEntity(TargetRecommendSaveRequest request) { + TargetRecommend entity = new TargetRecommend(); + entity.setTargetType(TargetRecommend.TargetType.valueOf(request.getTargetType())); + entity.setTargetId(request.getTargetId()); + entity.setRecType(TargetRecommend.RecType.valueOf(request.getRecType())); + entity.setFunctionType(TargetRecommend.FunctionType.valueOf(request.getFunctionType())); + entity.setLabel(request.getLabel()); + entity.setIcon(request.getIcon()); + entity.setPlaceholder(request.getPlaceholder()); + entity.setSort(request.getSort()); + return entity; + } + + private TargetRecommendResponse toResponse(TargetRecommend entity) { + return TargetRecommendResponse.builder() + .id(entity.getId()) + .tenantId(entity.getTenantId()) + .targetType(entity.getTargetType() != null ? entity.getTargetType().name() : null) + .targetId(entity.getTargetId()) + .recType(entity.getRecType() != null ? entity.getRecType().name() : null) + .functionType(entity.getFunctionType() != null ? entity.getFunctionType().name() : null) + .label(entity.getLabel()) + .icon(entity.getIcon()) + .placeholder(entity.getPlaceholder()) + .sort(entity.getSort()) + .modified(entity.getModified()) + .created(entity.getCreated()) + .build(); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ResourceGroupApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ResourceGroupApplicationServiceImpl.java new file mode 100644 index 00000000..9024b4c6 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/ResourceGroupApplicationServiceImpl.java @@ -0,0 +1,231 @@ +package com.xspaceagi.agent.core.application.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xspaceagi.agent.core.adapter.application.ResourceGroupApplicationService; +import com.xspaceagi.agent.core.adapter.dto.ResourceGroupDto; +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroup; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; +import com.xspaceagi.agent.core.domain.service.PublishDomainService; +import com.xspaceagi.agent.core.domain.service.ResourceGroupDomainService; +import com.xspaceagi.agent.core.domain.service.ResourceGroupRelationDomainService; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.enums.ErrorCodeEnum; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class ResourceGroupApplicationServiceImpl implements ResourceGroupApplicationService { + + @Resource + private ResourceGroupDomainService resourceGroupDomainService; + + @Resource + private ResourceGroupRelationDomainService resourceGroupRelationDomainService; + + @Resource + private PublishDomainService publishDomainService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long add(ResourceGroupDto dto) { + if (StringUtils.isBlank(dto.getName())) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "name"); + } + if (StringUtils.isBlank(dto.getType())) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "type"); + } + + ResourceGroup resourceGroup = new ResourceGroup(); + BeanUtils.copyProperties(dto, resourceGroup); + resourceGroup.setId(null); + resourceGroup.setCreated(null); + resourceGroup.setModified(null); + resourceGroup.setToolCount(0); + resourceGroup.setTenantId(RequestContext.get().getTenantId()); + resourceGroup.setCreatorId(RequestContext.get().getUserId()); + + return resourceGroupDomainService.add(resourceGroup); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(ResourceGroupDto dto) { + if (dto.getId() == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "id"); + } + if (StringUtils.isBlank(dto.getName())) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "name"); + } + + ResourceGroup exist = resourceGroupDomainService.queryById(dto.getId()); + if (exist == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.resourceDataNotFound); + } + + ResourceGroup resourceGroup = new ResourceGroup(); + resourceGroup.setId(dto.getId()); + resourceGroup.setName(dto.getName()); + resourceGroup.setDescription(dto.getDescription()); + resourceGroup.setIcon(dto.getIcon()); + resourceGroup.setType(dto.getType()); + resourceGroup.setSpaceId(dto.getSpaceId()); + resourceGroup.setModified(new Date()); + + resourceGroupDomainService.update(resourceGroup); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + if (id == null) { + throw new IllegalArgumentException("Resource group ID cannot be empty"); + } + resourceGroupDomainService.delete(id); + + List resourceGroupRelations = queryGroupRelations(id); + resourceGroupRelations.forEach(relation -> publishDomainService.updatePublishedGroupInfo(Published.TargetType.valueOf(relation.getTargetType()), relation.getTargetId(), null)); + resourceGroupRelations.forEach(relation -> resourceGroupRelationDomainService.delete(relation.getId())); + } + + @Override + public ResourceGroupDto queryById(Long id) { + ResourceGroup resourceGroup = resourceGroupDomainService.queryById(id); + if (resourceGroup == null) { + return null; + } + ResourceGroupDto dto = new ResourceGroupDto(); + BeanUtils.copyProperties(resourceGroup, dto); + return dto; + } + + @Override + public ResourceGroupDto queryByName(Long spaceId, String type, String name) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper + .eq(spaceId != null, ResourceGroup::getSpaceId, spaceId) + .eq(StringUtils.isNotBlank(type), ResourceGroup::getType, type) + .eq(StringUtils.isNotBlank(name), ResourceGroup::getName, name); + return resourceGroupDomainService.list(queryWrapper).stream().map(entity -> { + ResourceGroupDto dto = new ResourceGroupDto(); + BeanUtils.copyProperties(entity, dto); + return dto; + }).findFirst().orElse(null); + } + + @Override + public List queryList(String name, List types, Long spaceId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper + .eq(spaceId != null, ResourceGroup::getSpaceId, spaceId) + .in(!CollectionUtils.isEmpty(types), ResourceGroup::getType, types) + .like(StringUtils.isNotBlank(name), ResourceGroup::getName, name) + .orderByDesc(ResourceGroup::getId); + + List list = resourceGroupDomainService.list(queryWrapper); + return list.stream().map(entity -> { + ResourceGroupDto dto = new ResourceGroupDto(); + BeanUtils.copyProperties(entity, dto); + return dto; + }).collect(Collectors.toList()); + } + + @Override + public List queryList(List groupIds) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(ResourceGroup::getId, groupIds); + return resourceGroupDomainService.list(queryWrapper).stream().map(entity -> { + ResourceGroupDto dto = new ResourceGroupDto(); + BeanUtils.copyProperties(entity, dto); + return dto; + }).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addResourceToGroup(Long groupId, String targetType, Long targetId) { + ResourceGroup group = resourceGroupDomainService.queryById(groupId); + if (group == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.resourceDataNotFound); + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper + .eq(ResourceGroupRelation::getGroupId, groupId) + .eq(ResourceGroupRelation::getTargetType, targetType) + .eq(ResourceGroupRelation::getTargetId, targetId); + List existList = resourceGroupRelationDomainService.list(queryWrapper); + if (!existList.isEmpty()) { + return; + } + + ResourceGroupRelation relation = ResourceGroupRelation.builder() + .tenantId(RequestContext.get().getTenantId()) + .groupId(groupId) + .targetType(targetType) + .targetId(targetId) + .build(); + resourceGroupRelationDomainService.add(relation); + + ResourceGroup updateGroup = new ResourceGroup(); + updateGroup.setId(groupId); + updateGroup.setToolCount(resourceGroupRelationDomainService.countByGroupId(groupId)); + resourceGroupDomainService.update(updateGroup); + + publishDomainService.updatePublishedGroupInfo(Published.TargetType.valueOf(targetType), targetId, groupId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeResourceFromGroup(Long groupId, String targetType, Long targetId) { + resourceGroupRelationDomainService.deleteByGroupIdAndTarget(groupId, targetType, targetId); + + ResourceGroup updateGroup = new ResourceGroup(); + updateGroup.setId(groupId); + updateGroup.setToolCount(resourceGroupRelationDomainService.countByGroupId(groupId)); + resourceGroupDomainService.update(updateGroup); + + publishDomainService.updatePublishedGroupInfo(Published.TargetType.valueOf(targetType), targetId, null); + } + + @Override + public List queryGroupRelations(Long groupId) { + return resourceGroupRelationDomainService.listByGroupId(groupId); + } + + @Override + public List queryGroupRelations(List> targets) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + for (Map target : targets) { + for (String key : target.keySet()) { + queryWrapper.or(wrapper -> { + wrapper.eq(ResourceGroupRelation::getTargetType, key); + wrapper.eq(ResourceGroupRelation::getTargetId, target.get(key)); + }); + } + } + return resourceGroupRelationDomainService.list(queryWrapper); + } + + @Override + public ResourceGroupRelation queryResourceGroupRelation(String type, Long targetId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper + .eq(ResourceGroupRelation::getTargetType, type) + .eq(ResourceGroupRelation::getTargetId, targetId); + return resourceGroupRelationDomainService.queryOne(queryWrapper); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/SkillApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/SkillApplicationServiceImpl.java index 0ce758c7..60942e7c 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/SkillApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/SkillApplicationServiceImpl.java @@ -47,6 +47,10 @@ import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.net.URI; +import java.net.URLDecoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; @@ -78,8 +82,10 @@ public class SkillApplicationServiceImpl implements SkillApplicationService { @Resource private IFileAccessService iFileAccessService; + public static final Long MAX_SKILL_FILE_SIZE = 100L * 1024 * 1024; + // 单个文件最大大小100M - private final static long MAX_SINGLE_FILE_SIZE = 100 * 1024 * 1024L; + public final static Long MAX_SINGLE_FILE_SIZE = 100 * 1024 * 1024L; private static final String TARGET_TYPE_SKILL_DEV = "skill_dev"; private static final String TARGET_TYPE_SKILL_PUBLISH_APPLY = "skill_publish_apply"; private static final String TARGET_TYPE_SKILL_PUBLISHED = "skill_published"; @@ -613,6 +619,98 @@ public class SkillApplicationServiceImpl implements SkillApplicationService { return resultId; } + @Override + public Long importSkill(String url, MultipartFile file, Long targetSkillId, Long targetSpaceId, List usageScenarios) { + // file 和 url 二选一 + if ((file == null || file.isEmpty()) && (url == null || url.isBlank())) { + throw new IllegalArgumentException("Please provide either a file or a URL to import"); + } + if (file != null && !file.isEmpty() && url != null && !url.isBlank()) { + throw new IllegalArgumentException("Please provide either a file or a URL, not both"); + } + + // 如果提供了 url,下载文件 + if ((file == null || file.isEmpty()) && url != null && !url.isBlank()) { + file = downloadFromUrl(url); + } + + if (file.isEmpty()) { + throw new IllegalArgumentException("Please select a file to upload"); + } + + // 检查文件大小 + if (file.getSize() > MAX_SKILL_FILE_SIZE) { + throw new IllegalArgumentException("Import file must be smaller than 20 MB"); + } + + // 查询已有技能 + SkillConfigDto existSkill = null; + if (targetSkillId != null) { + existSkill = this.queryById(targetSkillId, true); + if (existSkill == null) { + throw new IllegalArgumentException("Skill does not exist"); + } + } else { + if (targetSpaceId == null) { + throw new IllegalArgumentException("Please select a target space"); + } + } + + // 转换 usageScenarios 为 SkillExtDto + SkillExtDto ext = convertUsageScenariosToExt(usageScenarios); + + return this.importSkill(file, existSkill, targetSpaceId, ext); + } + + /** + * 从 URL 下载技能文件,返回 MultipartFile + */ + private MultipartFile downloadFromUrl(String url) { + try { + String decodedUrl = URLDecoder.decode(url.trim(), StandardCharsets.UTF_8); + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(URI.create(decodedUrl)) + .GET() + .build(); + HttpClient httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); + if (response.statusCode() != 200) { + throw new IllegalArgumentException("Failed to download skill file from URL, HTTP status: " + response.statusCode()); + } + byte[] body = response.body(); + if (body == null || body.length == 0) { + throw new IllegalArgumentException("Downloaded file is empty"); + } + // 从 URL 路径中提取文件名 + String path = URI.create(decodedUrl).getPath(); + String fileName = path.substring(path.lastIndexOf('/') + 1); + if (fileName.isBlank()) { + fileName = "skill.zip"; + } + return new InMemoryMultipartFile("file", fileName, "application/octet-stream", body); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + log.error("Failed to download skill file from URL: {}", url, e); + throw new IllegalArgumentException("Failed to download skill file from URL: " + e.getMessage()); + } + } + + private SkillExtDto convertUsageScenariosToExt(List usageScenarios) { + if (usageScenarios == null || usageScenarios.isEmpty()) { + SkillExtDto ext = new SkillExtDto(); + ext.setSupportTaskAgent(1); + ext.setSupportPageApp(0); + return ext; + } + SkillExtDto ext = new SkillExtDto(); + ext.setSupportTaskAgent(usageScenarios.contains(UsageScenarioEnum.TaskAgent) ? 1 : 0); + ext.setSupportPageApp(usageScenarios.contains(UsageScenarioEnum.PageApp) ? 1 : 0); + return ext; + } + @Override @Transactional(rollbackFor = Exception.class) public Long copySkill(SkillConfigDto skillConfigDto, Long targetSpaceId) { @@ -651,6 +749,21 @@ public class SkillApplicationServiceImpl implements SkillApplicationService { } } + @Override + public SkillConfigDto getSkillTemplate() { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("templates/skill-template.json")) { + if (inputStream == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentSkillTemplateReadFailed, + "skill template not found"); + } + return getSkillTemplate(inputStream); + } catch (BizException e) { + throw e; + } catch (IOException e) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentSkillTemplateReadFailed, e.getMessage()); + } + } + @Override public void checkSpaceSkillPermission(Long spaceId, Long skillId) { SkillConfigDto skillConfigDto = queryById(skillId, false); @@ -896,7 +1009,7 @@ public class SkillApplicationServiceImpl implements SkillApplicationService { if (idx >= 0 && idx < path.length() - 1) { fileName = path.substring(idx + 1); } - String contentType = FileTypeUtils.isTextFile(path) ? "text/plain" : "application/octet-stream"; + String contentType = FileTypeUtils.getContentTypeByFileName(fileName).toString(); InMemoryMultipartFile multipartFile = new InMemoryMultipartFile("file", fileName, contentType, bytes); Long tenantId = RequestContext.get() != null ? RequestContext.get().getTenantId() : null; Long userId = RequestContext.get() != null ? RequestContext.get().getUserId() : null; @@ -1073,6 +1186,7 @@ public class SkillApplicationServiceImpl implements SkillApplicationService { } dto.setFiles(normalizeSkillFiles(publishedConfig.getFiles())); dto.setZipFileUrl(publishedConfig.getZipFileUrl()); + dto.setDevAgentConversationId(publishedConfig.getDevAgentConversationId()); return dto; } } catch (Exception e) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TableWorkflowForPageApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TableWorkflowForPageApplicationServiceImpl.java new file mode 100644 index 00000000..89629e5d --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TableWorkflowForPageApplicationServiceImpl.java @@ -0,0 +1,172 @@ +package com.xspaceagi.agent.core.application.service; + +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.xspaceagi.agent.core.adapter.application.PublishApplicationService; +import com.xspaceagi.agent.core.adapter.application.ResourceGroupApplicationService; +import com.xspaceagi.agent.core.adapter.application.TableWorkflowForPageApplicationService; +import com.xspaceagi.agent.core.adapter.application.WorkflowApplicationService; +import com.xspaceagi.agent.core.adapter.dto.*; +import com.xspaceagi.agent.core.adapter.dto.config.Arg; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.NodeConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.TableCustomSqlNodeConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowNodeDto; +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.WorkflowNodeConfig; +import com.xspaceagi.agent.core.infra.rpc.CustomPageRpcService; +import com.xspaceagi.agent.core.sdk.IAgentRpcService; +import com.xspaceagi.agent.core.sdk.enums.TargetTypeEnum; +import com.xspaceagi.agent.core.spec.enums.DataTypeEnum; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.exception.BizException; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class TableWorkflowForPageApplicationServiceImpl implements TableWorkflowForPageApplicationService { + + @Resource + private WorkflowApplicationService workflowApplicationService; + + @Resource + private PublishApplicationService publishApplicationService; + + @Resource + private CustomPageRpcService customPageRpcService; + + @Resource + private IAgentRpcService iAgentRpcService; + + @Resource + private ResourceGroupApplicationService resourceGroupApplicationService; + + @DSTransactional + @Override + public PageSqlResultDTO tableNewSql(PageAppNewSqlDTO pageAppNewSqlDTO) { + WorkflowConfigDto workflowConfigDto = new WorkflowConfigDto(); + workflowConfigDto.setName(pageAppNewSqlDTO.getApiName()); + workflowConfigDto.setDescription(pageAppNewSqlDTO.getDescription()); + workflowConfigDto.setSpaceId(pageAppNewSqlDTO.getSpaceId()); + workflowConfigDto.setCreatorId(RequestContext.get().getUserId()); + Long workflowId = workflowApplicationService.add(workflowConfigDto); + + // 添加分组 + if (StringUtils.isNotBlank(pageAppNewSqlDTO.getGroupName())) { + List resourceGroupDtos = resourceGroupApplicationService.queryList(pageAppNewSqlDTO.getGroupName(), List.of(TargetTypeEnum.Workflow.name()), pageAppNewSqlDTO.getSpaceId()); + if (!resourceGroupDtos.isEmpty()) { + resourceGroupApplicationService.addResourceToGroup(resourceGroupDtos.get(0).getId(), TargetTypeEnum.Workflow.name(), workflowId); + } else { + ResourceGroupDto resourceGroupDto = new ResourceGroupDto(); + resourceGroupDto.setName(pageAppNewSqlDTO.getGroupName()); + resourceGroupDto.setDescription(pageAppNewSqlDTO.getGroupDescription() == null ? "" : pageAppNewSqlDTO.getGroupDescription()); + resourceGroupDto.setIcon(""); + resourceGroupDto.setType(TargetTypeEnum.Workflow.name()); + resourceGroupDto.setSpaceId(pageAppNewSqlDTO.getSpaceId()); + Long groupId = resourceGroupApplicationService.add(resourceGroupDto); + resourceGroupApplicationService.addResourceToGroup(groupId, TargetTypeEnum.Workflow.name(), workflowId); + } + } + + WorkflowNodeAddDto workflowNodeAddDto = new WorkflowNodeAddDto(); + workflowNodeAddDto.setWorkflowId(workflowId); + workflowNodeAddDto.setType(WorkflowNodeConfig.NodeType.TableSQL); + workflowNodeAddDto.setTypeId(pageAppNewSqlDTO.getTableId()); + workflowApplicationService.addWorkflowNode(workflowNodeAddDto); + + return tableUpdateSql0(workflowId, pageAppNewSqlDTO.getArgs(), pageAppNewSqlDTO.getSql(), pageAppNewSqlDTO.getProjectId()); + } + + private PageSqlResultDTO tableUpdateSql0(Long workflowId, List args0, String sql, String projectId) { + WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryById(workflowId); + WorkflowNodeDto workflowTableNodeDto = workflowConfigDto.getNodes().stream().filter(node -> node.getType().equals(WorkflowNodeConfig.NodeType.TableSQL)).findFirst().orElse(null); + if (workflowTableNodeDto == null) { + throw new BizException("Invalid data"); + } + Long nodeId = workflowTableNodeDto.getId(); + WorkflowNodeDto startNode = workflowConfigDto.getStartNode(); + WorkflowNodeUpdateDto startNodeUpdateDto = new WorkflowNodeUpdateDto<>(); + startNodeUpdateDto.setNodeId(startNode.getId()); + startNodeUpdateDto.setNextNodeIds(Collections.singletonList(nodeId)); + if (startNode.getNodeConfig() == null) { + startNodeUpdateDto.setNodeConfig(new NodeConfigDto()); + } else { + startNodeUpdateDto.setNodeConfig(startNode.getNodeConfig()); + } + startNodeUpdateDto.getNodeConfig().setInputArgs(args0); + workflowApplicationService.updateWorkflowNodeConfig(startNodeUpdateDto); // 更新开始节点信息 + workflowApplicationService.updateNextIds(startNode.getId(), Collections.singletonList(nodeId)); + + WorkflowNodeDto endNode = workflowConfigDto.getEndNode(); + + WorkflowNodeUpdateDto workflowNodeUpdateDto = new WorkflowNodeUpdateDto<>(); + TableCustomSqlNodeConfigDto tableNodeConfigDto = (TableCustomSqlNodeConfigDto) workflowTableNodeDto.getNodeConfig(); + tableNodeConfigDto.setSql(sql); + if (args0 != null) { + List args = args0.stream().map(arg -> { + arg.setEnable(true); + Arg argDto = new Arg(); + BeanUtils.copyProperties(arg, argDto); + argDto.setBindValueType(Arg.BindValueType.Reference); + argDto.setBindValue(startNode.getId() + "." + arg.getName()); + return argDto; + }).collect(Collectors.toList()); + tableNodeConfigDto.setInputArgs(args); + } + workflowNodeUpdateDto.setNextNodeIds(Collections.singletonList(endNode.getId())); + workflowNodeUpdateDto.setNodeId(nodeId); + workflowNodeUpdateDto.setNodeConfig(tableNodeConfigDto); + workflowApplicationService.updateWorkflowNodeConfig(workflowNodeUpdateDto); + workflowApplicationService.updateNextIds(nodeId, Collections.singletonList(endNode.getId())); + + WorkflowNodeUpdateDto endNodeUpdateDto = new WorkflowNodeUpdateDto<>(); + endNodeUpdateDto.setNodeId(endNode.getId()); + if (endNode.getNodeConfig() == null) { + endNodeUpdateDto.setNodeConfig(new NodeConfigDto()); + } else { + endNodeUpdateDto.setNodeConfig(endNode.getNodeConfig()); + } + endNodeUpdateDto.getNodeConfig().setOutputArgs(List.of( + Arg.builder().name("outputList").dataType(DataTypeEnum.Array_Object).bindValueType(Arg.BindValueType.Reference).bindValue(nodeId + ".outputList").build(), + Arg.builder().name("rowNum").dataType(DataTypeEnum.Array_Object).bindValueType(Arg.BindValueType.Reference).bindValue(nodeId + ".rowNum").build() + )); + + workflowApplicationService.updateWorkflowNodeConfig(endNodeUpdateDto); + PublishApplySubmitDto publishApplySubmitDto = new PublishApplySubmitDto(); + publishApplySubmitDto.setTargetType(Published.TargetType.Workflow); + publishApplySubmitDto.setTargetId(workflowId); + publishApplySubmitDto.setRemark(""); + publishApplySubmitDto.setCategory(""); + PublishApplySubmitDto.PublishItem publishItem = new PublishApplySubmitDto.PublishItem(); + publishItem.setScope(Published.PublishScope.Space); + publishItem.setAllowCopy(0); + publishItem.setOnlyTemplate(0); + publishItem.setSpaceId(workflowConfigDto.getSpaceId()); + publishApplySubmitDto.setItems(List.of(publishItem)); + publishApplicationService.publishOrApply(publishApplySubmitDto); + customPageRpcService.bindDataSource(RequestContext.get().getUserId(), Long.parseLong(projectId), "Workflow", workflowId); + PageSqlResultDTO pageSqlResultDTO = new PageSqlResultDTO(); + pageSqlResultDTO.setApiId(workflowId); + String authorizationPrompt = """ + 调用API时需要在header中传递referer,以下是根据系统环境变量构建referer + {PLATFORM_BASE_URL}/page/{PROJECT_ID}-{AGENT_ID}/dev/ + + **页面上禁止使用/api/v1/4sandbox开头的API**,/api/v1/4sandbox开头的API仅用作内部调试。 + /api/v1/4sandbox/page/ 仅仅用于创建API后的调试使用,禁止在页面上使用。页面上使用时需要转换:/api/v1/4sandbox/page/ -> /api/page/ + """; + pageSqlResultDTO.setApiSchema(authorizationPrompt + iAgentRpcService.queryApiSchema(TargetTypeEnum.Workflow, workflowId, Long.parseLong(projectId)).getData()); + pageSqlResultDTO.setApiSchema(pageSqlResultDTO.getApiSchema().replace("/api/page/", "/api/v1/4sandbox/page/")); + return pageSqlResultDTO; + } + + @DSTransactional + @Override + public PageSqlResultDTO tableUpdateSql(PageAppUpdateSqlDTO pageAppUpdateSqlDTO) { + return tableUpdateSql0(pageAppUpdateSqlDTO.getApiId(), pageAppUpdateSqlDTO.getArgs(), pageAppUpdateSqlDTO.getSql(), pageAppUpdateSqlDTO.getProjectId()); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TenantCreatedTaskServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TenantCreatedTaskServiceImpl.java index f6a6d639..7d58af8f 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TenantCreatedTaskServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TenantCreatedTaskServiceImpl.java @@ -8,7 +8,6 @@ import com.xspaceagi.agent.core.adapter.dto.PublishApplyDto; import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; import com.xspaceagi.agent.core.adapter.repository.entity.Published; -import com.xspaceagi.agent.core.spec.enums.ModelApiProtocolEnum; import com.xspaceagi.agent.core.spec.enums.ModelTypeEnum; import com.xspaceagi.system.application.dto.*; import com.xspaceagi.system.application.service.*; @@ -118,7 +117,6 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { // Get model list ModelQueryDto modelQueryDto = new ModelQueryDto(); modelQueryDto.setModelType(ModelTypeEnum.Chat); - modelQueryDto.setApiProtocol(ModelApiProtocolEnum.OpenAI); List modelConfigDtos = modelApplicationService.queryModelConfigList(modelQueryDto); if (CollectionUtils.isNotEmpty(modelConfigDtos)) { Long modelId = modelConfigDtos.get(0).getId(); @@ -146,9 +144,11 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { List userDtos = userApplicationService.listQuery(pageQueryVo).getRecords(); if (CollectionUtils.isNotEmpty(userDtos)) { Long userId = userDtos.get(0).getId(); + UserDto user = userApplicationService.queryById(userId); RequestContext.get().setTenantId(tenantId); - RequestContext.get().setUser(userDtos.get(0)); + RequestContext.get().setUser(user); RequestContext.get().setUserId(userId); + RequestContext.get().setLangMap(user.getLangMap()); List spaceDtos = spaceApplicationService.queryListByUserId(userId); if (CollectionUtils.isNotEmpty(spaceDtos)) { Long spaceId = spaceDtos.get(0).getId(); @@ -161,7 +161,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setDescription("Default agent for the site"); agentConfigDto.setSystemPrompt("You are a helpful assistant."); agentConfigDto.setOpeningChatMsg("Hello {{USER_NAME}}, how can I help you?"); - Long agentId = addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + Long agentId = addAndPublishAgent(spaceId, agentConfigDto); tenantConfig.setDefaultAgentId(agentId); tenantConfig.setRecommendAgentIds(List.of()); // Initialize default agents @@ -191,7 +191,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { return false; } - private Long addAndPublishAgent(Long tenantId, Long userId, Long spaceId, AgentConfigDto agentConfigDto) { + private Long addAndPublishAgent(Long spaceId, AgentConfigDto agentConfigDto) { log.info("Adding default agent: {}", agentConfigDto); Long agentId = agentApplicationService.add(agentConfigDto); // Publish default agent @@ -255,7 +255,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/e77afda2d4da44858f3dc1b4fa44bd2d.png"); agentConfigDto.setDescription("I am your professional career consultant. Feel free to consult me about any career-related questions."); agentConfigDto.setSystemPrompt("I want you to act as a career counselor. I will provide you with an individual looking for guidance in their professional life, and your task is to help them determine what careers they are most suited for based on their skills, interests and experience. You should also conduct research into the various options available, explain the job market trends in different industries and advice on which qualifications would be beneficial for pursuing particular fields. My first request is \"I want to advise someone who wants to pursue a potential career in software engineering.\""); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //产品经理 - Product Manager @@ -283,7 +283,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/77ba864646a94d019059240c3fbf2a3c.png"); agentConfigDto.setDescription("I am an experienced product manager with deep technical background and keen insight into market and user needs. I excel at solving complex problems, developing effective product strategies, and optimally balancing resources to achieve product objectives."); agentConfigDto.setSystemPrompt(prompt); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //销售运营 - Sales Operations @@ -296,7 +296,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setDescription("I am a sales operations manager who knows how to optimize sales processes, manage sales data, and improve sales efficiency. I can develop sales forecasts and targets, manage sales budgets, and provide sales support."); agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/78349b3db75c43b3915ebc83c54587f4.png"); agentConfigDto.setSystemPrompt("You are now a sales operations manager. You know how to optimize sales processes, manage sales data, and improve sales efficiency. You can develop sales forecasts and targets, manage sales budgets, and provide sales support. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //市场营销 - Marketing @@ -309,7 +309,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/697d544783114c05b6448e4cb14dd170.png"); agentConfigDto.setDescription("I am a professional marketing expert with deep understanding of marketing strategies and brand promotion. I know how to effectively use different channels and tools to achieve marketing goals, and have deep understanding of consumer psychology."); agentConfigDto.setSystemPrompt("You are now a professional marketing expert. You have deep understanding of marketing strategies and brand promotion. You know how to effectively use different channels and tools to achieve marketing goals, and have deep understanding of consumer psychology. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //项目管理 - Project Management @@ -322,7 +322,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/4ff6d9e2e264456ca262a831c0795b68.png"); agentConfigDto.setDescription("I am a senior project manager proficient in all aspects of project management, including planning, organizing, execution, and control. I excel at handling project risks, solving problems, and effectively coordinating team members to achieve project goals."); agentConfigDto.setSystemPrompt("You are now a senior project manager. You are proficient in all aspects of project management, including planning, organizing, execution, and control. You excel at handling project risks, solving problems, and effectively coordinating team members to achieve project goals. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //前端工程师 - Frontend Engineer @@ -335,7 +335,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/73634a7e0cef40f6a527fee20ab807e8.png"); agentConfigDto.setDescription("I am a professional frontend engineer with deep understanding of frontend technologies such as HTML, CSS, and JavaScript. I can create and optimize user interfaces. I can solve browser compatibility issues, improve web performance, and achieve excellent user experience."); agentConfigDto.setSystemPrompt("You are now a professional frontend engineer. You have deep understanding of frontend technologies such as HTML, CSS, and JavaScript, and can create and optimize user interfaces. You can solve browser compatibility issues, improve web performance, and achieve excellent user experience. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //运维工程师 - Operations Engineer @@ -348,7 +348,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/59232b1a4c504339a14f534e2313d352.png"); agentConfigDto.setDescription("As an operations engineer, I have deep understanding of computer system operation, performance, and availability."); agentConfigDto.setSystemPrompt("You are now an operations engineer. You are responsible for ensuring normal operation of systems and services. You are familiar with various monitoring tools, can efficiently handle failures and perform system optimization. You also know how to perform data backup and recovery to ensure data security. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //开发工程师 - Software Engineer @@ -361,7 +361,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/2d3d8211b3d44f95ba314d3b9f301707.png"); agentConfigDto.setDescription("As a software engineer, I have deep understanding of computer system operation, performance, and availability."); agentConfigDto.setSystemPrompt("You are now a software engineer. You are responsible for developing, testing, and optimizing software systems. You are familiar with various development tools and can efficiently complete project tasks. You also know how to perform code optimization and performance testing, ensuring high-quality software products. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //测试工程师 - Test Engineer @@ -374,7 +374,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setDescription("As a test engineer, I have deep understanding of computer system operation, performance, and availability."); agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/b163c0e41d0d4fd497ccf0c6be0a6659.png"); agentConfigDto.setSystemPrompt("You are now a test engineer. You are responsible for testing the functionality, performance, and availability of software systems. You are familiar with various testing tools and can efficiently complete project tasks. You also know how to perform code optimization and performance testing, ensuring high-quality software products. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //HR人力资源管理 - Human Resources Management @@ -387,7 +387,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/5f03892a684a4ce6b9d63f2c8e02008e.png"); agentConfigDto.setDescription("As an HR management professional, I have deep understanding of human resource management."); agentConfigDto.setSystemPrompt("You are now an HR management professional. You are responsible for human resource management. You are familiar with various HR tools and can efficiently complete project tasks. You also know how to optimize human resources and conduct performance assessments, ensuring high-quality organizational management. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //行政 - Administration @@ -400,7 +400,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/d9b46cf735934a33a36bcfdaea0f949b.png"); agentConfigDto.setDescription("I am an administrative specialist. You excel at organizing and managing daily company operations, including document management, meeting scheduling, and office facility management."); agentConfigDto.setSystemPrompt("You are now an administrative specialist. You excel at organizing and managing daily company operations, including document management, meeting scheduling, and office facility management. You have good interpersonal communication and organizational skills, and can work effectively in a multi-task environment. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //财务顾问 - Financial Advisor @@ -413,7 +413,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/ef705a85de9f409ebaff02f5dca79365.png"); agentConfigDto.setDescription("As a financial advisor, I have deep understanding of finance."); agentConfigDto.setSystemPrompt("You are now a financial advisor. You are responsible for company daily affairs. You are familiar with various financial tools and can efficiently complete project tasks. You also know how to perform financial optimization and risk assessment, ensuring high-quality financial management. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //法务 - Legal Affairs @@ -426,7 +426,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/b179c49d805947c18610f457d227b6f2.png"); agentConfigDto.setDescription("I am a legal expert. I understand corporate law, contract law, and other related laws. I can provide legal consultation and risk assessment for enterprises. I am also skilled in handling legal disputes and can draft and review contracts."); agentConfigDto.setSystemPrompt("You are now a legal expert. You understand corporate law, contract law, and other related laws. You can provide legal consultation and risk assessment for enterprises. You are also skilled in handling legal disputes and can draft and review contracts. Please answer the following questions in this role."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //翻译助手 - Chinese & English Translator @@ -439,7 +439,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/03a4bc05e39c401db66ee97475f90871.png"); agentConfigDto.setDescription("As a translation assistant, I can translate various languages into Chinese, and also translate Chinese into English."); agentConfigDto.setSystemPrompt("You are a helpful translation assistant. Please determine the language sent by the user, translate English into Chinese, and translate all non-Chinese text into Chinese; if the user sends Chinese, please translate it into English. Everything I send you is content that needs translation, you only need to answer with the translation result. The translation result should conform to the habits of the relevant language."); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //脱口秀喜剧演员 - Stand-up Comedian @@ -452,7 +452,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { agentConfigDto.setIcon("https://agent-statics-tc.qiming.com/store/b10e1f83dd064b76a6c8ebc9c7c154cf.png"); agentConfigDto.setDescription("As a stand-up comedian, I have rich experience with various events. I have the ability to create a routine based on the given topic. I will provide you with some topics related to current events and you will use your wit, creativity, and observational skills to create a routine based on those topics. You should also be sure to incorporate personal anecdotes or experiences into the routine in order to make it more relatable and engaging for the audience."); agentConfigDto.setSystemPrompt("I want you to act as a stand-up comedian. I will provide you with some topics related to current events and you will use your wit, creativity, and observational skills to create a routine based on those topics. You should also be sure to incorporate personal anecdotes or experiences into the routine in order to make it more relatable and engaging for the audience. My first request is \"I want an humorous take on politics.\""); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //面试模拟 - Mock Interview @@ -476,7 +476,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { ""Hello, I am the mock interviewer for the position you are applying for. Please describe the position you want to apply for and provide your resume (if convenient), and I will conduct a mock interview with you to prepare you for future job searches!"" """; agentConfigDto.setSystemPrompt(prompt); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //爆款文案 - Viral Copywriting @@ -503,7 +503,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { - Only start answering when the user asks a question. When the user doesn't ask a question, please don't answer """); agentConfigDto.setOpeningChatMsg("I can generate viral internet copy for you. You can tell me any requirements for the theme and content of the copy~"); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //心理模型专家 - Psychological Model Expert @@ -563,7 +563,7 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { - Step 6: Summarize experience, refine character building methodology, and provide reference for subsequent projects """ ); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } //提示词专家 - Prompt Expert @@ -685,6 +685,6 @@ public class TenantCreatedTaskServiceImpl extends AbstractTaskExecuteService { Please avoid discussing the content I send. Only output the optimized Prompt. Do not output extra explanations or guiding words. Do not surround with code blocks. """ ); - return addAndPublishAgent(tenantId, userId, spaceId, agentConfigDto); + return addAndPublishAgent(spaceId, agentConfigDto); } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TenantVersionUpdateServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TenantVersionUpdateServiceImpl.java index 67f13951..b1a85b39 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TenantVersionUpdateServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TenantVersionUpdateServiceImpl.java @@ -112,6 +112,22 @@ public class TenantVersionUpdateServiceImpl { i18nImportService.updateConfigToTenant(tenant, "1.2"); }); + tenantVersionUpgradeMap.put("1.0.8.4", (tenant) -> { + // permission + permissionImportService.importDiffToTenant(tenant, "1.5"); + // i18n + i18nImportService.addConfigToTenant(tenant, "1.3"); + i18nImportService.updateConfigToTenant(tenant, "1.3"); + }); + + tenantVersionUpgradeMap.put("3.0", (tenant) -> { + // permission + permissionImportService.importDiffToTenant(tenant, "1.6"); + // i18n + i18nImportService.addConfigToTenant(tenant, "1.4"); + i18nImportService.updateConfigToTenant(tenant, "1.4"); + }); + new Thread(() -> { // Customized content required for version upgrade while (true) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/WorkflowApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/WorkflowApplicationServiceImpl.java index 4494044f..51b84eba 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/WorkflowApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/WorkflowApplicationServiceImpl.java @@ -4,10 +4,7 @@ import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.google.common.collect.Lists; -import com.xspaceagi.agent.core.adapter.application.ModelApplicationService; -import com.xspaceagi.agent.core.adapter.application.PluginApplicationService; -import com.xspaceagi.agent.core.adapter.application.PublishApplicationService; -import com.xspaceagi.agent.core.adapter.application.WorkflowApplicationService; +import com.xspaceagi.agent.core.adapter.application.*; import com.xspaceagi.agent.core.adapter.dto.*; import com.xspaceagi.agent.core.adapter.dto.config.*; import com.xspaceagi.agent.core.adapter.dto.config.bind.ModelBindConfigDto; @@ -58,6 +55,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import reactor.core.Disposable; import reactor.core.publisher.Flux; @@ -122,10 +120,15 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic @Resource private ResourcePricingRpcService resourcePricingRpcService; + @Resource + private AgentApplicationService agentApplicationService; + @Override public Long add(WorkflowConfigDto workflowConfigDto) { WorkflowConfig workflowConfig = WorkflowConfig.builder() .spaceId(workflowConfigDto.getSpaceId()) + .type(workflowConfigDto.getType()) + .agentId(workflowConfigDto.getAgentId()) .name(workflowConfigDto.getName()) .description(workflowConfigDto.getDescription()) .icon(workflowConfigDto.getIcon()) @@ -313,6 +316,24 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic }); } } + + //知识库创建空库 + if (workflowNodeDto.getType() == WorkflowNodeConfig.NodeType.KnowledgeInsert && !workflowConfigDto.getSpaceId().equals(targetSpaceId)) { + KnowledgeInsertNodeConfigDto knowledgeInsertNodeConfigDto = (KnowledgeInsertNodeConfigDto) workflowNodeDto.getNodeConfig(); + if (knowledgeInsertNodeConfigDto.getKnowledgeBaseId() != null) { + KnowledgeCreateRequestVo knowledgeCreateRequestVo = KnowledgeCreateRequestVo.builder() + .icon(knowledgeInsertNodeConfigDto.getIcon()) + .description(knowledgeInsertNodeConfigDto.getDescription()) + .name(knowledgeInsertNodeConfigDto.getName()) + .dataType(1) + .spaceId(targetSpaceId) + .userId(userId) + .build(); + Long knowledgeConfigId = KnowledgeRpcService.createKnowledgeConfig(knowledgeCreateRequestVo, knowledgeInsertNodeConfigDto.getKnowledgeBaseId()); + knowledgeInsertNodeConfigDto.setKnowledgeBaseId(knowledgeConfigId); + } + } + //数据表结构复制 if (workflowNodeDto.getType().name().startsWith("Table")) { TableNodeConfigDto tableNodeConfigDto = (TableNodeConfigDto) workflowNodeDto.getNodeConfig(); @@ -416,6 +437,15 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic if (workflowConfig == null) { return null; } + + List agentFLowArgs = null;//agentFlow补全开始节点的输出参数 + if (WorkflowConfigDto.Type.AgentFlow.name().equals(workflowConfig.getType()) && workflowConfig.getAgentId() != null) { + List agentComponentConfigs = agentApplicationService.queryComponentConfigListByAgentIdAndType(workflowConfig.getAgentId(), AgentComponentConfig.Type.Variable); + if (!CollectionUtils.isEmpty(agentComponentConfigs) && agentComponentConfigs.get(0).getBindConfig() instanceof VariableConfigDto variableConfig) { + agentFLowArgs = variableConfig.getVariables(); + } + } + WorkflowConfigDto workflowConfigDto = new WorkflowConfigDto(); BeanUtils.copyProperties(workflowConfig, workflowConfigDto); completeCreator(List.of(workflowConfigDto)); @@ -442,6 +472,24 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic //更新聚合变量出参关联的参数列表 completeVariableAggregateOutputArgConfig(workflowConfigDto.getNodes()); workflowConfigDto.setSystemVariables(Arg.getSystemVariableArgs()); + if (agentFLowArgs != null) { + // AgentFlow的系统变量不展示上下文 + List systemVariables = Arg.getSystemVariableArgs().stream().filter(arg -> !GlobalVariableEnum.CHAT_CONTEXT.name().equals(arg.getName())).toList(); + workflowConfigDto.setSystemVariables(systemVariables); + WorkflowNodeDto workflowNodeDto = workflowConfigDto.getNodes().stream().filter(node -> node.getType() == WorkflowNodeConfig.NodeType.Start).findFirst().orElse(null); + if (workflowNodeDto != null) { + workflowNodeDto.getNodeConfig().setInputArgs(new ArrayList<>()); + agentFLowArgs.forEach(arg -> { + if (!GlobalVariableEnum.isSystemVariable(arg.getName())) { + arg.setSystemVariable(true); + workflowNodeDto.getNodeConfig().getInputArgs().add(arg); + } + }); + } + } + if (WorkflowConfigDto.Type.AgentFlow.name().equals(workflowConfig.getType()) && workflowConfig.getAgentId() != null) { + workflowConfigDto.getNodes().stream().filter(node -> node.getType() == WorkflowNodeConfig.NodeType.End).findFirst().ifPresent(workflowNodeDto -> workflowNodeDto.getNodeConfig().setOutputArgs(new ArrayList<>())); + } return workflowConfigDto; } @@ -502,6 +550,7 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic workflowConfigDto.setNodes(nodes); workflowConfigDto.setPublishedSpaceIds(publishedDto.getPublishedSpaceIds()); workflowConfigDto.setScope(publishedDto.getScope()); + workflowConfigDto.setPublishDate(publishedDto.getModified()); workflowConfigDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(workflowConfigDto.getIcon(), workflowConfigDto.getName(), "workflow")); return workflowConfigDto; } @@ -519,6 +568,12 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic return JSON.parseObject(publishedDto.getConfig(), WorkflowConfigDto.class); } + @Override + public List queryPublishedWorkflowConfigs(List workflowIds) { + List publishedList = publishApplicationService.queryPublishedList(Published.TargetType.Workflow, workflowIds, null, true); + return publishedList.stream().map(publishedDto -> JSON.parseObject(publishedDto.getConfig(), WorkflowConfigDto.class)).filter(Objects::nonNull).toList(); + } + @Override public Long addWorkflowNode(WorkflowNodeAddDto workflowNodeAddDto) { String name = workflowNodeAddDto.getType().getName(); @@ -606,7 +661,6 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic configJson = JSON.toJSONString(nodeConfigDto); } case Knowledge -> { - var knowledgeBaseConfigs = Optional.ofNullable(workflowNodeAddDto.getNodeConfigDto()) .map(AddNodeConfigDto::getKnowledgeBaseConfigs) .orElse(null); @@ -614,6 +668,24 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic knowledgeNodeConfigDto.setExtension(workflowNodeAddDto.getExtension()); configJson = JSON.toJSONString(knowledgeNodeConfigDto); } + case KnowledgeInsert -> { + KnowledgeInsertNodeConfigDto knowledgeInsertNodeConfigDto = KnowledgeInsertNodeConfigDto.addFrom(workflowNodeAddDto.getTypeId()); + knowledgeInsertNodeConfigDto.setExtension(workflowNodeAddDto.getExtension()); + configJson = JSON.toJSONString(knowledgeInsertNodeConfigDto); + } + case Agent -> { + Assert.isTrue(workflowNodeAddDto.getTypeId() != null, "Agent ID cannot be null"); + AgentConfigDto agentConfigDto = agentApplicationService.queryPublishedConfig(workflowNodeAddDto.getTypeId(), false); + if (agentConfigDto == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentNotFound); + } + name = agentConfigDto.getName(); + description = agentConfigDto.getDescription(); + AgentNodeConfigDto agentNodeConfigDto = new AgentNodeConfigDto(); + agentNodeConfigDto.setAgentId(workflowNodeAddDto.getTypeId()); + agentNodeConfigDto.setExtension(workflowNodeAddDto.getExtension()); + configJson = JSON.toJSONString(agentNodeConfigDto); + } case LongTermMemory -> { List args = new ArrayList<>(); Arg arg = new InputArg(); @@ -1558,21 +1630,22 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic } }); - Map nodeMap = workflowConfigDto.getNodes().stream().collect(Collectors.toMap(WorkflowNodeDto::getId, n -> n)); - //检查节点是否有循环连线 - WorkflowNodeCheckDto workflowNodeLineCheckDto = new WorkflowNodeCheckDto(); - try { - checkIfHasLoopLine(workflowConfigDto.getStartNode(), new HashSet<>(), nodeMap, workflowNodeLineCheckDto); - } catch (Exception e) { - workflowNodeCheckDtos.add(workflowNodeLineCheckDto); - } + Map nodeMap = workflowConfigDto.getNodes().stream().collect(Collectors.toMap(WorkflowNodeDto::getId, n -> n)); + if (!"AgentFlow".equals(workflowConfigDto.getType())) { + //检查节点是否有循环连线 + WorkflowNodeCheckDto workflowNodeLineCheckDto = new WorkflowNodeCheckDto(); + try { + checkIfHasLoopLine(workflowConfigDto.getStartNode(), new HashSet<>(), nodeMap, workflowNodeLineCheckDto); + } catch (Exception e) { + workflowNodeCheckDtos.add(workflowNodeLineCheckDto); + } + } if (!CollectionUtils.isEmpty(workflowNodeCheckDtos)) { return workflowNodeCheckDtos; } - - checkIfStartToEnd(workflowConfigDto.getStartNode(), nodeMap, workflowNodeCheckDtos); + checkIfStartToEnd(workflowConfigDto.getStartNode(), nodeMap, workflowNodeCheckDtos, new HashSet<>()); if (!workflowNodeCheckDtos.isEmpty()) { return workflowNodeCheckDtos; } @@ -1636,14 +1709,18 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic return workflowNodeCheckDtos; } - private void checkIfStartToEnd(WorkflowNodeDto startNode, Map nodeMap, List workflowNodeCheckDtos) { + private void checkIfStartToEnd(WorkflowNodeDto startNode, Map nodeMap, List workflowNodeCheckDtos, Set checkedNodeIds) { if (!CollectionUtils.isEmpty(startNode.getNextNodeIds())) { List nextNodes = new ArrayList<>(); startNode.getNextNodeIds().forEach(nextNodeId -> { WorkflowNodeDto workflowNodeDto = nodeMap.get(nextNodeId); if (workflowNodeDto != null) { nextNodes.add(workflowNodeDto); - checkIfStartToEnd(workflowNodeDto, nodeMap, workflowNodeCheckDtos); + if (checkedNodeIds.contains(nextNodeId)) { + return; + } + checkedNodeIds.add(nextNodeId); + checkIfStartToEnd(workflowNodeDto, nodeMap, workflowNodeCheckDtos, checkedNodeIds); } }); if (nextNodes.isEmpty() && startNode.getType() != WorkflowNodeConfig.NodeType.End) { @@ -1672,20 +1749,6 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic } } - private void lookupStartNodes(WorkflowNodeDto node, Map nodeMap, List startNodes) { - if (CollectionUtils.isEmpty(node.getPreNodes())) { - if (node.getLoopNodeId() != null) { - WorkflowNodeDto workflowNodeDto = nodeMap.get(node.getLoopNodeId()); - if (workflowNodeDto != null && node.getId().equals(workflowNodeDto.getInnerStartNodeId())) { - return; - } - } - startNodes.add(node); - return; - } - node.getPreNodes().forEach(preNode -> lookupStartNodes(preNode, nodeMap, startNodes)); - } - private void checkArgs(WorkflowNodeDto node, List inputArgs, Map argMap, WorkflowNodeCheckDto workflowNodeCheckDto) { checkArgs0(node, inputArgs, argMap, workflowNodeCheckDto); if (node.getType() == WorkflowNodeConfig.NodeType.LLM) { @@ -1959,6 +2022,7 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic return; } AtomicBoolean update = new AtomicBoolean(false); + NodeConfigDto.ContextPassingType contextPassingType = node.getNodeConfig().getContextPassingType() == null ? NodeConfigDto.ContextPassingType.Manual : node.getNodeConfig().getContextPassingType(); inputArgs.forEach(inputArg -> { if (inputArg.getName() == null) { inputArg.setName(""); @@ -1968,11 +2032,11 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic workflowNodeCheckDto.setSuccess(false); workflowNodeCheckDto.getMessages().add(I18nUtil.systemMessage("Workflow.Validate.InvalidParamName", inputArg.getName())); } - if (inputArg.isRequire() && StringUtils.isEmpty(inputArg.getBindValue())) { + if (inputArg.isRequire() && StringUtils.isEmpty(inputArg.getBindValue()) && contextPassingType == NodeConfigDto.ContextPassingType.Manual) { workflowNodeCheckDto.setSuccess(false); workflowNodeCheckDto.getMessages().add(I18nUtil.systemMessage("Workflow.Validate.ParamNotBound", inputArg.getName())); } - if (StringUtils.isNotBlank(inputArg.getBindValue()) && inputArg.getBindValueType() == Arg.BindValueType.Reference) { + if (StringUtils.isNotBlank(inputArg.getBindValue()) && inputArg.getBindValueType() == Arg.BindValueType.Reference && contextPassingType == NodeConfigDto.ContextPassingType.Manual) { Arg arg = argMap.get(inputArg.getBindValue()); if (arg == null) { workflowNodeCheckDto.setSuccess(false); @@ -2064,7 +2128,7 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic nodeConfig.setExceptionHandleConfig(new ExceptionHandleConfigDto()); nodeConfig.getExceptionHandleConfig().setExceptionHandleType(ExceptionHandleConfigDto.ExceptionHandleTypeEnum.INTERRUPT); nodeConfig.getExceptionHandleConfig().setRetryCount(0); - nodeConfig.getExceptionHandleConfig().setTimeout(180); + nodeConfig.getExceptionHandleConfig().setTimeout(nodeConfig instanceof AgentNodeConfigDto ? 3600 : 180); nodeConfig.getExceptionHandleConfig().setExceptionHandleNodeIds(new ArrayList<>()); nodeConfig.getExceptionHandleConfig().setSpecificContent(new HashMap<>()); } @@ -2284,6 +2348,26 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic I18nUtil.replaceSystemMessage("WorkflowKb", knowledgeNodeConfigDto.getOutputArgs()); } } + if (nodeConfig instanceof KnowledgeInsertNodeConfigDto knowledgeInsertNodeConfig) { + KnowledgeConfigVo knowledgeConfigVo = KnowledgeRpcService.queryKnowledgeConfigById(knowledgeInsertNodeConfig.getKnowledgeBaseId()); + if (knowledgeConfigVo != null) { + knowledgeInsertNodeConfig.setName(knowledgeConfigVo.getName()); + knowledgeInsertNodeConfig.setDescription(knowledgeConfigVo.getDescription()); + knowledgeInsertNodeConfig.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(knowledgeConfigVo.getIcon(), knowledgeConfigVo.getName(), WorkflowNodeConfig.NodeType.Knowledge.name())); + } + I18nUtil.replaceSystemMessage("WorkflowKb", knowledgeInsertNodeConfig.getOutputArgs()); + } + if (nodeConfig instanceof AgentNodeConfigDto agentNodeConfigDto) { + AgentConfigDto agentConfigDto = agentApplicationService.queryPublishedConfig(agentNodeConfigDto.getAgentId(), forExecute); + if (agentConfigDto != null && agentConfigDto.getSpaceId().equals(workflowNodeDto.getSpaceId())) { + agentNodeConfigDto.setName(agentConfigDto.getName()); + agentNodeConfigDto.setDescription(agentConfigDto.getDescription()); + agentNodeConfigDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(agentConfigDto.getIcon(), agentConfigDto.getName(), WorkflowNodeConfig.NodeType.Agent.name())); + if (forExecute) { + agentNodeConfigDto.setAgentConfigDto(agentConfigDto); + } + } + } if (nodeConfig instanceof TableNodeConfigDto tableNodeConfigDto) { if (tableNodeConfigDto.getTableId() != null) { DorisTableDefineRequest request = new DorisTableDefineRequest(); @@ -2527,6 +2611,7 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic .enableSubscription(tenantConfigDto.getEnableSubscription() != null && tenantConfigDto.getEnableSubscription() == 1) .billUserId(userDto.getId()) .devTest(workflowExecuteRequestDto.isTest()) + .apiKey(workflowExecuteRequestDto.getApiKey()) .traceTargets(Lists.newArrayList(TraceContext.TraceTarget.builder() .targetType(TraceContext.TraceTargetType.Workflow) .name(workflowConfigDto.getName()) @@ -2541,8 +2626,7 @@ public class WorkflowApplicationServiceImpl implements WorkflowApplicationServic estimateTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.WORKFLOW).targetId(workflowConfigDto.getId().toString()).build()); } resourcePricingRpcService.completeWorkflowEstimateTargets(estimateTargets, workflowConfigDto, new HashSet<>()); - PriceEstimate priceEstimate = resourcePricingRpcService.estimatePrice(tenantConfigDto.getTenantId(), workflowContext1.getTraceContext().getBillUserId(), - List.of(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.WORKFLOW).targetId(workflowConfigDto.getId().toString()).build())); + PriceEstimate priceEstimate = resourcePricingRpcService.estimatePrice(tenantConfigDto.getTenantId(), workflowContext1.getTraceContext().getBillUserId(), estimateTargets); if (!priceEstimate.isPass()) { WorkflowExecutingDto workflowExecutingDto = new WorkflowExecutingDto(); workflowExecutingDto.setSuccess(false); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/support/PlatformEnvHookSupport.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/support/PlatformEnvHookSupport.java new file mode 100644 index 00000000..ee6296d2 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/support/PlatformEnvHookSupport.java @@ -0,0 +1,153 @@ +package com.xspaceagi.agent.core.application.support; + +import com.alibaba.fastjson2.JSON; +import com.xspaceagi.agent.core.adapter.dto.HookEntryDto; +import com.xspaceagi.agent.core.adapter.dto.HookScriptDto; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 将平台会话变量注入沙箱 agent 环境。 + *
    + *
  • Claude Code / Codex:SessionStart + UserPromptSubmit hook 写入 {@code CLAUDE_ENV_FILE}
  • + *
  • OpenCode:仅写 {@code platform-env.sh},由 file-server 安装的 platform-env 插件注入(hook 脚本在 OpenCode 下 no-op)
  • + *
+ */ +public final class PlatformEnvHookSupport { + + private static final String ENV_SCRIPT_RELATIVE_PATH = "hooks/platform-env.sh"; + private static final String INJECT_SCRIPT_RELATIVE_PATH = "hooks/inject-platform-env.sh"; + private static final String INJECT_COMMAND = "bash .claude/hooks/inject-platform-env.sh"; + + /** + * 将 platform-env.sh 同步到 CLAUDE_ENV_FILE(Claude/Codex)。 + * OpenCode 下由 opencode-hooks-plugin 传入 OPENCODE_PROJECT_DIR,脚本直接 exit 0。 + */ + private static final String INJECT_SCRIPT_CONTENT = """ + #!/usr/bin/env bash + set -euo pipefail + + # OpenCode 走 platform-env 插件,不通过 hook 注入 + if [[ -n "${OPENCODE_PROJECT_DIR:-}" ]]; then + exit 0 + fi + + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + env_file="$script_dir/platform-env.sh" + if [[ ! -f "$env_file" ]]; then + exit 0 + fi + + if [[ -n "${CLAUDE_ENV_FILE:-}" ]]; then + grep '^export ' "$env_file" >"$CLAUDE_ENV_FILE" || true + fi + + exit 0 + """; + + private PlatformEnvHookSupport() { + } + + /** + * 若存在可导出的用户变量,写入 env 脚本并追加 Claude 侧 SessionStart / UserPromptSubmit hook。 + */ + public static void apply( + Map hookArgs, + Map> hooksConfig, + List hookScripts) { + if (hookArgs == null || hookArgs.isEmpty() || hooksConfig == null || hookScripts == null) { + return; + } + + String envScriptContent = buildPlatformEnvScript(hookArgs); + if (envScriptContent == null) { + return; + } + + hookScripts.add(HookScriptDto.builder() + .path(ENV_SCRIPT_RELATIVE_PATH) + .content(envScriptContent) + .build()); + hookScripts.add(HookScriptDto.builder() + .path(INJECT_SCRIPT_RELATIVE_PATH) + .content(INJECT_SCRIPT_CONTENT) + .build()); + + Map handler = new LinkedHashMap<>(); + handler.put("type", "command"); + handler.put("command", INJECT_COMMAND); + handler.put("timeout", 10); + + // SessionStart:会话开始时写入 CLAUDE_ENV_FILE(官方支持) + HookEntryDto sessionStartEntry = HookEntryDto.builder() + .hooks(List.of(handler)) + .build(); + hooksConfig.computeIfAbsent("SessionStart", key -> new ArrayList<>()).add(0, sessionStartEntry); + + // UserPromptSubmit:每轮用户消息前刷新变量(SessionStart 之后变量变更时同步) + HookEntryDto userPromptEntry = HookEntryDto.builder() + .hooks(List.of(new LinkedHashMap<>(handler))) + .build(); + hooksConfig.computeIfAbsent("UserPromptSubmit", key -> new ArrayList<>()).add(0, userPromptEntry); + } + + static String buildPlatformEnvScript(Map hookArgs) { + if (hookArgs == null || hookArgs.isEmpty()) { + return null; + } + + StringBuilder script = new StringBuilder(); + script.append("#!/usr/bin/env bash\n"); + script.append("# Platform-managed session variables. Auto-generated; do not edit.\n"); + + boolean hasExport = false; + for (Map.Entry entry : hookArgs.entrySet()) { + String exportLine = toExportLine(entry.getKey(), entry.getValue()); + if (exportLine != null) { + script.append(exportLine).append('\n'); + hasExport = true; + } + } + + return hasExport ? script.toString() : null; + } + + private static String toExportLine(String key, Object value) { + if (key == null || isEmptyEnvValue(value)) { + return null; + } + String stringValue = toEnvValue(value); + return "export " + shellSingleQuote(key) + "=" + shellSingleQuote(stringValue); + } + + private static boolean isEmptyEnvValue(Object value) { + if (value == null) { + return true; + } + if (value instanceof String str) { + return str.isBlank(); + } + if (value instanceof Collection collection) { + return collection.isEmpty(); + } + if (value instanceof Map map) { + return map.isEmpty(); + } + return false; + } + + private static String toEnvValue(Object value) { + if (value instanceof String str) { + return str; + } + return JSON.toJSONString(value); + } + + private static String shellSingleQuote(String value) { + return "'" + value.replace("'", "'\"'\"'") + "'"; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/resources/templates/deepagents-flow-ts.zip b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/resources/templates/deepagents-flow-ts.zip new file mode 100644 index 0000000000000000000000000000000000000000..4cb5bc8e9eb800b9aa3b1717f1c306d144681852 GIT binary patch literal 548442 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[xw%z@Hs@r51!!(_S4MIat~=1u`3RoRx@8t0k2 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

HLDev^&}vGtB-{{a>H}1 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 ~1sya>_CU zVHy;~M>iiIE3QKKcaL6IAf=kc=m56ge`vi=Cl4V$y%NtR4Y~?yrkYyi5D_u z=vyq9WMyim?bqoa=Y{d$JL)BOgy@VzxG7{i^yT5YK=%{yX^HAVVN8RrcH_rbq*DlK zri26D->(MJBmkRHn$LKdsmtE>W;4BG6HP`f%TdWp}Z@9tqV=ng@ za98}5ca|0;o50sZ0}z2k@v%VC7?Eb@;)40Kdt+Wvg00Bp=})I)hPwbOiIl4S}n- zq36^f&}k!vxdL>pnHu{BST(|^9*iGdNHDRPmt++zWX-yVopoi;qJ}3{75B+Qh8^t* zNs*yUOXc61c6$ajPZX$9icp4!%*yOgQ@IxB&sx35WtDC?!;OW86YRy!>*++$pyE>4b@h3Gcbe-?`Lr^L4I{e>)?9hqzcC2Sv)c82XaU40) z7HNAT&=iFPfigZ~+*rnXs`b@YUX(iU`wbf<$NByht5}U-Tb$&g11t=V+SVirs@k&m zUg#or{Ko8bkZ;|F@vB zfBrE3$5H>VzkemM{d{2m8~gj$Q3h7d#&!;K&-?#z>VIdd@qR$xy?_7! zeEs|o)qgnE!C2qW>HnYOO0kNz?1l=)KmA>VM?6T_bs)Ln!f$6wRgg`o1*IrQAl#VC zH5ttm5eYT_pxFM7B>p@()^#9szToB9xR~#Ghrf2ix~raESwr@E8LSi$S&rM!?!7tB z_CaP%zyR6FreF-t($ZQ=k$8@ZVbefaCncv2dj%RTE0oQ-+Y5m^m(jzq?H_C7kl7n( zru= z4uW%6zWp(D70}eU+Uu>cu4fO99rE=naM=gQCq|Px8ZIuB&4a=^E9yri+0|LuAA4Mr z8=0@`6GT!FveS~5C$bE^*xVWd?4FM|UcUJ*X|58QF8XU6t1gtK80j|3byQCuwJx`* zCiw{^+e9H08Vw~8P-s)#brcCz$bWMbJwPd@R^x<)zviT|v-WYd)$~oeG69P z4Zy0?>?g&DDND-4=aqMJ8`}1iEc|M^1Bo;?+x@HjcSq;ofmlHCPepF*?WHUO`^cz%Yb)wlTC`}c= zrneqmw=g{G8rj&{0LhD(#W4GJq1%q${v)jnYYjI)p?2A61X2)V!S`4_yH8L{7F+9bbu#e*5vpikBadFT$StkwlHisetI(*=vaCqG z7D7@6Fb2i2OWq(D(vuPc)<)O^KGULk&mES4{V}0T+Z+HTY*Q(0+huJ&^GkKNfp0P_ zEEKUUpD)+P-jPu{ON2^klfSh^$3nFoS=#gGjXl)?63dMY;spQlVY#ip7D zqOQYV(C4Z&BP-l4_VVw`re%K_TWs*< zzv8PIe{E*gK-24(`~^TzAz284A_|sIl?omUkqW-hC)c*LZqeUzxHF9chg%lXVd_k0 z-e0|f++<~8Ja)B~NlBNSr~o4>(o$GuV&qAHuc3`U{Csci3+V?fjh}V;J?n>CZSFm+ zdwFshAM6Fv4a`GojQn?W?{}=OS88w0We%jnZEHKIIB%j0Ol1^?u>%kEEiiujIP~`1 znCgRBM&1D=Ny+^oyy-Kq5_o{B2$8#97oxcZ_N86wmwZNll5jG!B&N6 zl3Bt}Q$U)L-*4}B!PlP`x2kIH)7!JWO8(L-^X{wEeaEDg-ul{Ci{^fb6~%P3P0e=I z1Eaz~qk5rg`sy|3>pHj99UH3SzK~6KJFnc+c)_a`<|#XIwdEc(Sh;1NyLrEC^67dJ zy`&obYvVD{II{jO! zNmn|KPS^N~ts&4bb9jvba7Fe2%64SMa~+mmHfw{r;eMJmkWE#6^LaAWm~yX2q%O@} z@y=F**gUQ@%zEgiyh9aTDq|=lYGOdA(z<6SEuf<%3HDqq&)SCiZt-v)RHM=#KU-d2 z*ywg)S#rX|4yiHHbYX|m@^~hiRnP8Ip4yvs@HY0J@yjV@uM#39<)4ZE%_R%N$}u*! zV7opTAmafyYP9P!z~?&_SAEQB%Z>Q$V>`;G)6$dG|0rfy1oii&i?B6pN6v3sc+ zZ~Ay!Iv;xfDTcB)bGPB;nTwGio?UjCx-~VUsHW;tAQr_Up!}`UycO;F-c&l#Zcju_aK-J~f&mY$yL5pqcf2ryMWx*b~~h4IA9WZNM3;EDxEH&$=#4uRSIg;Pyp?hok@ zWJZz)h~@k4)AwTr_Nf2!L>(T(5e)3SEQp{vrazb0w*$NpU0*oRchgIrd2UAvUt$nV zqNh~$tCtNJsorgIRKSgfb$!Eu>%@xjc<2l0za^H}jy)}AMxdF~AHCEB8~}j!Kfte( zgT9Ng)jw&dHa5l%|59GS7=$gHSpSa^XtAQU?YhZ7;|uJ39RQM-5>=tIW#!5z#AR|j zCTYckw3Wq5h7{`-6BNh>xjgaY8B*zik<*4?WzCFgc~zba?T6f=u8|)Q_W0m8q!Y|w z9v3)%;hyoFzV8nb;DPAxN+W3?6w^Rq4*tvtLv9$8K$73Bstbl6Qgas|7^fBu*^TNSbgI3sHR)_D4WCg_YHzl8WM>d@I zY#+lzHWJ)RiQJ-W-TY80f4f!IM~!Xk1K5DdC8+;ZE*uojfm1e**P^HTZ0~mrYh=?h zi1HqWe3~jI4Uy@Plm!0E`CbC1%k$&9?THbvw)gr@#YHlUp=UTQdUUSD=k)I~#=Yq? zC3L0eBGV)(jb%RH?V{pbPdd(#z~L{0i!74OKK4!e3g3Af|79uln+C+CC_y}?eP}S* zs|645S@$R>_Q>u9V>zma4&Hs17RJx57<;Fvtag%$Ra3((tDT~ioDA;dEuH6Ey}po-ikd&FBPFi`v9qrM3?7x z#>zVLu_Izu>4J)ozr_RM1r8DTmI5MJ$(u$T&0gDti_S#3$?T1gswoLVkZHMrargdA z&@&PHT$5YD{^7Yxk83$xLNfy5Xd^J` zN&h2}5l0#ajyu5;o`Oz%waTDh+dyyGen+?+>|L+@2yl#T zZ5>2k6qZs4MRfWNa881VEO@o<`eGHh)yB&G+24GTyB*<+E9-^P$-?Bx#G1(0t346T zmCXh`3{V-P4tuGe)$$gra@wEJ^E#>b?3eO&-n|Y&l~y*w^Qpf^yqT!OS>-0*x5s#1 z!i#}3da$Sr6PlSS@4Fg70ij{O_ktI=luS?m@z4EB&uSqZ!S`tx*pG#TS}PO?`Z)U$aR< z5P^Y@9YD_+HW0~oI_8$X&S+iU< zVO{i3Ter94vgYEkskYODN~1d%s`x{*`EF{xE9eLy4Fm5ooTo<(oFsSRH?%l98%?_? zq-wE^BCA#T%kg%`VjksO6d@>FsSd2`kNVE~)Z;w0?u2%>4jjw8IK?;dDhqzY z&UweV=73vXl$lQ-st2TMesKb!vY?WT^{fBh;K6Z$?RToK<3=V`JV@jHybx-I@Xi-8AF9$p%zgw0--& z8-os0%?(U1*!T2-{+Xdi!AG#N@BkZTH|GTIzYuKq%1d^P)esb|7f!DG0xKvUzVrh$b?dzaiaGx;-LP(Fa!67C1%HDhl>hVT1G&#;!bDH9kcJM!&jw&H{UtFXDKGL)5uf~CEyA(dNdb?3 zR5++hR)Wj*rh1}l^NIYnv6Uk%-fs?f^kE9!b=w(VLDix=0#$oJYvJ)@)g)AzeP?5O ztHZlRY;w}shz5-tOjzdKSys#kIUfdnduw078!wVDB526n@9Q|VO+U7K_Z4Q)P8}dO z`ij8Ern!aQhxf3aWKl4HF>DbMp>vjy^Jl=!3gdk!-FYrlRSY>Rvs8t@4Q7E8$z2?7 zU6wf1TPlY`Uh~>!-xCdG6S;yM3BoQR9I=a?(|tm^|;z15#QOgYGU zoAn`rhU(4WNCzCeo7QR1Y&gv|Q*hYtHALG_ST;XZDw`AS+Kn5y+~-b7xMYg)<3}IY zSH)LE71Urr6z!H~D?nPbT)*N;5j*;Or4^XF1?rNpzEx|3GKw~3JEE4J#cS4=P~NrC)~m^m_YFi{_d!+_o@A;v zpzpS;<`a6_7Ponm$PKr?HrMsLY1vO*%`TsP)|su554&4C5$>LZZTocI zT*E(58`c4aR0%a(_QVz%B{tI*L-7W35}#LaJD&$g7JAJ)TW`3!8qYb?jV{BU-McQV zef41u;khpvc2IC6?blXJrt+sawYSgGEjM7#k&eT;IB;jTx-<>l+;sui#AF8iNHAi9 z@jzO|fF)vDy_&FqR&9@@Y8WiA>Pk2lMVjWOtG5eAV-7c5M_iDJ~bUx-Y0J{ zk^jRCmM5I(V-c*3KE?|m!gvEK@#W%|FW9pcWlV`t05?~(6~^R5&e|eGje9~ z@15WSgu>(Tz&dGx1U{D}X_?JxO3t*COxw>(;Fgq zvi*99{3-IX@+s$2Deq{RFk8;DxDFo?ErMm7J5Gypvr-2fjbT3TgJ3(Kih}Rfc(Z4em-zU%a3E~gEv;eX5S(xX0Du{4007ee zQ1*t-j!w4L|EhO3bO8$u5`q6+T5rbsbX**Hbp1xvgk6|(pYGV1!3b3_JL9`qwK4rY zVmR5st;tV@lK?l$Z8Ji;HwdXW&t9YlRKiTK06=Q~E6RS)SDbq5mFz3Hr&V{Ia|d*x zi7BVC$>V){qxIUeb#`Y*^ldy!wQy2u0&&r$ZS;v-V z56{=pVQ4;(Ga==306CNm2AUWCa1l_AjABH9PrI|3AR>^%qQ9W`@_TuAQ+a7= zNvM8RN2Bip@@e52BZ3q3`-i3`RI=^Mc84c7$d>H9X1`Zru`4Djuj_Ko`%u7b}QiKgK{L9tJV6+^GE+a3MysctlSZ z5B$ea3hVn|Ox~{?IWFj81`QDlFN*8dG8q$V0K(TV9&waSHG{ZaMf=w z!*>M!*gKHfPb42N3P_x3LC*^B=@VcD?`7Ex0U+10@uPb5qZsi{`%GMaX}LtK1mkvV&ipu zG7*P?2I>XU!9gPf##FKwR-$dv`y!!9mp{OzMh?5;^J7P);Fw1ut|rMvOmNV7?evWF z@T_F+03-+NN@6*_xm8{XS{Fs)`erQaPfcs6SuU6?hoRdGkQ$`m(v4Ye!yGXzJ*>IbaHb zl6nFtH@_^@c~B;p*XR&%YdQ*gAc;!P-V^!jx|rVMA2vo~C#o@Dk4$4TV3Lt*l>bDt z${YCkGDqNOUin=|ok7+HL~@QvIa^KFD6Cooj<=Bmp+3!yMe5_W*mYUlnm&euGJFMd zv);js!TE>lGkUM1W7zTG_l=79V4+!Jz+#7^6{M+%U>JBO@WeQB7OnFzl*&O(W0N)z zju(;fxIMw>9;(+v5y(qB5l%m(>|(rs`6eG|(gnt?R?FQGkrfi!VS{dS7&#}2)SM{n zdLO2~SsFh4 zVoNjQpC{0&n8uKFdCB5Rg)sM9$x+!n1RFLiypS1_W+%n?+m7i)(d1NtrfMm`xiQN4 zII&7)0fcgc3-+8`edqp%Rt{a(`lsB~6CdvNj_7Qs`s+G?`h6JuPw^fx8H z$2&slZ;?Sq=BSPkzrReegB;e07#1XqUr#2X@1&FHOMrO>#jN~&RW9b5?dFse4G*+|pfce&h{#X0$YAfk4MOWJJv*Wf3yex=0yXH^h_m9# z7L~h6)iT zM_6AwZD6bpQJDmm{@iC3I+wan-7 z&5OK>8bdY!D4(~655y@`i=WM#9%A+Jor>kb)ugRj|Qr1)IqTefQ{H|LPpx(EAp?U+%Q{RF^}5LhopbGi614XxA)eOa7t) zkl*ufk12^jvrMhHEd+>t!#;BVWZ0YYdRx39(3NiYxN@xf+EEM=_0nT;%`yC4e@bc@ zxkzrHL9KJ`tjbLUt>TNwIQaV;#1*nLnUA3Qmw?#-U z_=0bHR&GZ3k)oB-)0>h09sNPjtz(R6={Yx!P#j>6Q3Gk&IiK5f!ndx3MGIwh19(Ww zNBSd%bxQF-$%<;5dABtstsF%QC=soPgGnzz$DyRy|%1xK=K z@-n$IF;cayT-%LEugt#ttsoXijcYfSJV-Z(c?MG{3=c-j!=k;&W(AFibp;W%mrS?C zG*4tshcy9n^;jMJ!}1_py*467hYkqXEl2$4h{v=Zyz9$lh{yyd=u%U)+t*I=8(GoM z(mw&Hg83%1&(irFH8SQ-q$k~Ef$23r%GfFu}A1aYmT zdXoEuYWRI#CvnR`Z%+ScENL*CMxY4Z!|h%c?yS$rFSmYC;0=y3Ra@LMXQh?ojE(AB zU1``1eP#BPL59*0rw9UY42dvm%j+Wn=dS~bC>`8?xZ6y51`%y?r^ZV#c9NKAsb#Mz z$`oufNzZuL)j;9Lo6)&y0>>U(QOrM1SofjemzJvK8H)x$y^JWJ!zwO-h-ErBfP>zf zct~LtdWkG|MVZVsN=sR&ji@C%B#6XwOLo@zITG{WG)1ExZ$1YLhFS^Qzr6GuQ_qfxKt7G@(e~5#Jf3~>4B!mHY0w)qrdXY70&H) zKpG+Lca;}nFx)og2t@V4S$?KX?Gl*#%i>UsL2Lkv0pG>}MYYFaI_0YdLL|B@srLa7 z{m?-17T&=QWa!GnLT5(dgd@v8m%}k1W+3VXew+$g6heQ0(L-3>(acjO}y&d58@J{GQvtMVyH$O zrVac1?yQ3G_tZpoetKF@jep_OuM}{c*RE273bwxXjUfq%B&|>>0ufQrE?M0?*D|w| zl<|Zy=KrDW9iwbvk}S}&ZQHhO+qP}nzGYjtY@4@i^OkMi(yMRHo9Uij-EY44O@5_jl9L*IAW2ca~>rL%E&s3h$7) z{UK|t0J$L1AZQwk^!&Tc;W<;Zeq?3%zX&xiplW@l?dWc}p}U}hhDojIJ^7(?1t*59 zp)46d*Wv*UE`b0oZFQ`-zB(QqB9no_?^$n}kY)k;Wa_B?qC#PUKW< z>c(rp&5SY;l}{tyfmpRBrjxh#7{mIZHAY5jFCy?Vil#HDeBw1*Qm?IVrsHnmkQ|O| zR_G~|MhMDifqxt-`m^Qki<2bk&jS`=!kz|gLJDam^o`{6C?Kw4(h0Z;@3d|MqxfbA(iI@QsOY$z(>{;j&D~_52k0^N7Lze}mHE25rN6$)FyT7= z&g+B}MVx9<)lMlatj>>7w9nqL(>w_j?|dgBb0BPneo054d=?veg0NqQA@CSNjNm4a znd=L5(V1d6_nmLN<;EaJ>(!k*8EkY#e8zmNG6k}50I^bJGppa}kCPMgdLG;wcCF)K zkiLPhj+IZuUTGJ-n-Hd8^dpSAfu>%TnR=A!!X14ZFASf{@HKJRt=$Ob*d|tx^2l8b zfHFa73Sk#12K{{)LdQmWW>-zdBq~cy1S;*7(w%w1xH3wN?tBjGp~#nuPpt{ZLBCRD z)*TDz<|tU~y_br5zfv(AkpX+I6a?U}qZ72U#T`9%I9d&Rc_mn;L(n9B$lxp*bm`At z2<}?KSxjFp%q%NgBjvz3a?*qrqtQ*t6ok0_-0*@y0zkCFDU8lAH^+Ar^7GMh4j*Zc z_qN415o(*sA^Hl*f^MS@G1|NV4WIK!sdkb4=6+!}4$?R#!7_L7K2#A3O__^g-jayE z{@|(Qs$;JI04Sc8K*^bI!xIFvH&C#Dkzuv@Qv7~*i8K$kj}+V#NO>q2pSAcBys_Aq z6kKOCr1?@7w#_JT!0=b&V#;uwK$Ν+3m+r}0trkTknH8<|~|BrJ|;cK4dF)s<}f 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 z z0YGe9+pc-ln1a%n+s%8aW@7Ym`gm<-I1G@RreQz91-eqF9uj3@q%bg;4l0{D?@-c= zOwE3!5fl>}SZXdR8#J5CkH52+IylUA<@Z)(ehdi*o z{Cw(N{8mG2-Ws*@2wZ(p-uB&Cn)x7hfGEEvY4LP{E%^L_ znx?t>2CmO)`*@9D=}xgOu09j;@bG|iz1rmJ@i}#1{`|`8dd8_(g~%Q58rLCARf%31 zJEOW>+bNB`Rqfn0ouvuLMWQ@*2+G(+gcZ`qtcs6_V?u&}p{g4_`~A+zHw}%7?Agh~ zW||bw^lIW`DbXkRk-$ThAsQkD&_u`&$z5A0Rd)p4McXukK-h(Z31#y9&Bn}cZyFTx z$3S{mzas8|fx{Ort-=5R^pXJl)bwxw z93714{}bcx_+wdOZfisPBa1TnFa2oN>c0mnYkb=xi=%z>`VRlCpvq)sbLHm>q8Kmc zx)R!zkmG{PH#UkzWqaH_XtuxM zWzw;#W&e)VT**F3*LPAI9C`fBL}~Rq{D5@iWpwr?JGOCOu5-ko&0K>x&~hW$Y3HS7 zlr2jvNR8$3CqT&E;2^og`BUP}<>k{M{$Qp2U^VNW@^)?8^exgWaQQACxfdNCCJtYX z@%_0j=Je5Vqzfs$!x2!fB-rtmjoeIe7%dYhvASSRT?N)f=Mjpz!hF`6~Uf!+Ue>XI3Xn9t=Z-S`Pna7?VR4Q1P zp~tjKsU!lEc11`I+ekI*R&G|3&gI_Ab=#@Zsuh1Q&ncvZc}!1SEW+6YdkGVVku~D? zGC6}*`MK{ZMy$)iVL!etO7zh3fMsP6$FD1oA)At7l%2YZP;wfLEtbi92gv%MaVu&M zl2|aw6HOQ@rWA1@r!dWpyzw;dJ*cU9r6*iIeJgpYMcRdIuUC2^+Q%z%hjFJtBVE!U@z~}tX^2z#4ZA$Ns!8iXL=zWC>@};FeT9A-b~>+YbD-c2lXXx zIu3rv&3S!fzfg92BM3mGy}gaV^8CqlH<`QWN`U4Uj^-I10>f1jx#{!_Y9VkqM)=Z3 zyEu$^&`HbXnL(YyaE$^ugr_SSK(xy>9mKENZ5!*44bdet;jqY9>c4ca<{o%20n%9| z2nEH38_W#^*G>C5jxi=3F*|~7&nUNX-jWsix@we(58xc+#PTtn312=;%O}6y^l@N& zM@8Gi;)d`-S5`0JHJZ9FM>3I6Ag#XTaB~6V%~@1s_BWfl>Nlq?-Ybk8EMh z!=#}Rq>^~LSe}MH4O$jm{H(fC#AB`J)FAoDtooM%Z-ZaB@&K<_XO2k45DW@irCTt4 z2s21)K9TYR*s&Z-5)iTL?1a#GxV$N*A%-*dy&@>iP#_P>u2FUV5i@8Hev6Cio;+wz z`)5-?_=n9Of4NW^g4#NJnx3axHMihwty+4vAX{x&T6>B6lfI&6F*pZU*~i=9 zI|TUf(*(@*vyIILvXxLDbv#32#+pwJ?eI#-F5=^1I)d#3)}N_MO?0Q-$Q6phI&!14 zI9^-x>+^kcu&{7&v3cDc(t_B1HahvYbvqGyypZU6VAN84b9?JNA5)h;K+6(c^tP?M zJ$H6jj{b$Z_<M`06X_z^8s3Ojrh zxN$fZl0fQ$z^IWDT5(~^4Zb~4{n%YS^6m&3FIMu=&1mf0a$k2ju(mUgK2c5?0d=7U02tVq|7RH;43)MRQ5?g){62s*VIjaA7CO zrEjaT*Xdj#V`66)PB(L3VrX8-GX~y*Ub0eq*X1X(zw1+CAK9T?+ zvU)IFKiprUj>oHH3^7;w-Z?^ZzVAt=l*ug8IC*(FFR4htJie*|zVWztjB$Nuu(ww} zlW3Urv&jbyeJOPdtS;4=1**GX;n(`xb)NTu)0OJ6P`uaIKcehL$ z;2d}^*$m(vTwLR8x@SooZ2m0vTADGwF&?+r*tzyh)_AiS_=?k6m%oaciQ(OHSUsI$ zq6CeYphP8mFK$4}Qa> z^b0dMh|h{bE4f_NI{@)#C=l?cl<*q+U(uLC;0+9$fWmrk43x5a5TAs%80}J+ni9d< zcPKITkkKr06R*ryE~`fh^1VsWC1J#cqhMPThAOx;CzC*@i(Klv)QtCHvfJ7IVObzF z3Zk9EEGCc?wHi{Ri>y$)ui=~4VM8r|`~B`{nZACzCVEN4(|RK2oG1C_8;(4e8KvJ=5H^rIoS$dx zhXUc$old~BZx9||dy1ZWnXA{b)Elb3>ean+`K5Fu>t0CSleb}JYt^Lc3t7t(S?|;8 zP3y9`dWeVJ#Fb}5w3}DJa{9Ik!td49iZ*afcC&%+Ez`FS?JM%Y%GEvRJ_m&KiQC)v zkk6FZBUtY=T95?}MG{aErmw!Z>thksnHQg-q^)CgCs(w;g`+lFGqw7QW8R2;Hv+8} z;&`Fh@ZdMHHdVNxI}3Sw@O(kPNIWREbxyy8y8M|sam0%18q=PZHzfxVagNKu|?7sxEQHj7LQFma&_Y0K;y43uBI0Cd-zFF`ci$^(ye^GnA20 z&!!3EGrc)M5pqn0V|z=}FZ?k<-Ybx&IY{+4gzPCR88S`p#{f2y*%gK&U>LsPBklW! zk>kt6L%)G%6ywW^c!K)nz!9GU?-`OwhYTmBmMZY$pUfWrn5kw1 z{0Jx&{*8F)KLnKj-`(kdQBYd{C@9xeF?wcd-4-Kti~O#U&9Mye)v^^9I?b_S9EEsr z*6%p;1(l0$5Ftnj^Qo$WYp4s&i91MVEd(HDzm*)Ujw5>Oo>!*HCE{^<`HtOlp56J2 z>^MLSUIdwQc*aj;WRYJoJpK81Jk_omNq?9Bp@#jcGX1tUwDsBzCjRYu$ff24f0ykQ zu10^m_^D+79O<^h6&g=_{PF2*1E~mF_xXnJ`MY|P3o_LGHE!8;eahLhaeM-N_hw48 zpVO9Ol=jPX|EOTa=J0j6<-Pz+H7^LC{#9@H{LC><78$?WHaXzaz0qw0_w+xz0zE_dl14t zVEps;zkJlUzeEW4NzNq?9uIrcyAwF7?-c%gjQdrY`@ad{_;;pD2%u9CpPECXGQUNo6zjt4k$|y;cmRECO&GAl2&#UVW7pQAvn`>d2xa;tOnx`rV`M@FeI1|-DOLm z8s!jp&>-h#k-nKZ$t^b3E2KITm95#fmho&GHspk1ubK-hCxf2oNhyWD$CN~L&`K7p$=Y}Wp<|4vK5j@zPuHYqK zH`H~9I3Iy2tZVohS{?*gdvODRCnqaxL1^&jXa3n&L}WEC(1e&6%~iv#XGx>xR{7#k zS+G9CmF_%yqb6+}JWiyZ0L!>H!j=nY23at0r+?l|?-EeD{0-+N8mgP`qTV%=g=wCV z|9c*F*N3BSTgA-;8GPY&!$H$`^)TZ8r`eRGus~AsD7%CF#n@d^g7g9$6i>o5WMX2& zaM%wR24Ip>@tlP^sJ`XEHf#z^^%fS;sCJyXK9)i-lqA}z*v}HlEGRnvRZj>;&qUjZ za*6c7+z~mzH*uekH+#quj}%0eViBPvNfrPhZGUVYlJ&CK3E-nR`<#QxHxL#ucA~IR z9e|IsO3(f(ifKl6MH(hW_90WeW3OkVNO<){6DgI*@AJK|AMRvGajcILDjB##P^)0$ z>v8xuHrv-V>#{;!fk}3^%fI^zkex%1e%1DwJ9L=P%@>8ZRa}*TpRtP5Vh@IwDN2j= zF;lQc^a#xBc%I<-B(RWyqjKi#29Ibo%qSzzJ7D^es6kq1*T8rN)v2u zc47AWziTiQKR%X?D%Kh$^@<&9I zdj9pxk~pOj5~^MAnc3;t-%*L#Su*jGOj@of z4^Am0c0dJ@q@|#o%V?M&z#_Qu%+T1Ov~Tc{gvSl}HY{mz_EGD7t-L#~b#Zw3*xwc0 zdv9;+J^JRnosTLiopq2%8|$c)0V1V>wd&9T+TMNbnL%cETrPJ%mh`?-Rk$BcZo5@> zHu>EF3U;KKq&T8xjkVXbq(oz_un3Gh8jOJ# zjmhdmssb5kDQRxRLrT->Q3;!!V&la}?69S$ji14Z*2>r!At*l|4n4XNh5%F$WTbbY zv3$bkB<2`_jAZpaw!tjY^ObR)7Z>c2g!ex7=S@X$Qz&fJ%Es1(gvLBmNpv2%$N8Fs$E!K&y{)b==6PtWAXgVHK5`Fc)sB`e2Hw%ji?F&0Z z6w<*UC2o@eHzO#myOu;|ZQd;R8GVF5tZ?SJ`Wm3nY31NmSK6q66F7Yh1wy2|O-5oA zcSHDKP$Az~;c4H$6z%Sj&O*C?nw`O=hEgmnb9lah{~b{OkSbI}c?FD-pMWACswCU@0@w$o$|ONfK;1tStJ5c*Ew=tZ-dDm{7dS0L zOlzQN0C83XC!4OS0_}-B=Pd2sqjr54M|@&i1rx*iH&8YF%fPwXH<=bgde;}jDIcVZL$&4DNochtC{bViu!(L{OU?k*|CSMvSG ze0UXa7RGPJ8md6XTC*c!5^U|&FC_PmPpsyH00V*_%y6?a%UX_`?W8N?Vy+*{8=n}xchO{vx`OmjC{ zL>;(osh8$nc2~hJEWO+F6PNY`z5jQK0XEf5q5Ui|i=Si=?VtASpEm zu*pgj|3Hj&om01Ts4!{2YOXLu6$roy&VjisIAYk7GR8~zYd1400e^=181xoN$KMf; zk%9xFxVpG-G)At{_+xH_-6{Ts;Gg9(VcAHqK)q}~sN^m_N9#}TPk|@_Y5-<-M0FKsBvtce8w?X>&K*G!9HF)Q@@V76f2MKBMMH1}p@%GG7Ih%09^ zEk8N0V@n;XX(KjA)?B=5;M^i8xfl~PoH&wr0^R3^NRp`kp5jBQxo!O%3C^E!QEacl z;q)vhaol2V7Y8qDKDAF*!4a-H?aJl-u(N%4fYhmumN*d&Qc97HB2^LXBScWTDoN~* zpJjiZR*Fi$xA{G&e`OGw-cMqF`d_&ZzPurzcD?gl-_#_|EQ;Q+x!r$VjHX`;I`bIKQ07YQecf@6_0L(D@P@vwN`?tIIpmo1N9y}!HMX{#4`OV6!0O*5V; z%%$+SYm{+JpzQ@9({Dp&1oTKK2N6<5obtq*K_$0=vLIKrTFc^8S{Ht39NWFS7~+kOo@$qi%q4FprVhBVEDj7%%wd06Tgj_ z((L@PR5isIJ(Z#a^^`Q-pUD~E3Q5`t8CrPBYKl1uKqOVDVN%g__fsfVsA6}@a`SZe za!M|!_Hb0F7N`(0u#A-h)QWZ#6s83f78F4;Gb)tglq9r6!h>r6`9H7j!=z9G)$Q#9 zuq{vo$sPnXAtkKZ>>_BLfCQ)tpdOs%?x&=fP;4y(q5a9iCV~<9M37XA479_X>3_6RLhmOiR#E#mLqjRU88S2L<5AocQPU&*&BSe+pPr$NwMX zmD@$GV(TBAfHEuq0Or3w=m)%FY-symDck=*5Exed&kG<&v`c&*Ne{e`GHZEfhKe(K zcUi^W5c$sHZMat!kWsq6C4m*Bv6gw}gOO-p$}iH>P_{gh8@(l*(e^$12VTAwWOCh3 z$wV^$zT++*{-1kb?(+O!6%g5cX)Fza3%l54`0C|opYvbeJ2t+a@Sn{N;64i1#>hG} zD15{oa@oR9joVeD&MjX{-Uj>W-O47|%|7=n-Ycf1t)%bobnmLS` zska4j@TkD^6UaoL@ae1HORE}(@v?ffG%j%$-&~*K1MCHJ2Dvb_l~`p`%fj#V$|4GO zL09{Zvn!f)(ClZplcQvgi*b@EQlc+D+%%{PCS5!pU)-Q_w9H^xUFVcZ9e`r4 zUIuR6f?49jcQ~K_;nLC*R#{4h(GUR*JRsnE2LCx(?Gw=sXyoWHD#GJ~U}&f%3oNB? z_$l>qzl^KRs9TzU-B)6;d>2qC#{LYk;tf<);{;TF>){P@LMuN_q zWG0HN2PO~6@n^!+<%=o*Z0{KB&kb@mhP#Gx@5p`$Sje%~3sefL{XuA=o9>^2atoE9 zs2$PP7=4k4TQ!xSAOkXCGS6jTE7Id%%JC6wby+sOMNY+yx% zJun9`$N7A75n18I;~Je~HK?UuRoaJmWH!?AVJxhVh)=Om&`wG0e`%@I{x+6KIkALJ zg*O;#jB0tDaO8Yt?)4{R@G<_5b7MZlyTSU46VV<^iB84B%)AeT;M#SgnjtD2`UErQ zbcMWsqa7))qf&0R`RC)BSnuQ+%w=Xi5+8^+MEhHU`m zv2tDmi{-3fdQcd>qKR3wh&Ur?lzO@4{K$QZ0;9BzbNg$n)#u~2>)!A4?br>6GHPzt}I%ol-pXf9Pc@BeJ(Faitf?)8=`aw{o(Ch{bAy&#G1k48$5&LJLc?>S;TWO9kg4{H;q>#(|0l)w9sylgiqqwT` zqRny@`??`|1S`EEC{N$G;Um<_qaKfmR%oD}T=f*M-30nVG>>9LU4g+P>Vpv8>L%O9 zGraz{H4I)&U3E*M3#!sm+fAWvsd?*q_jJu5_U{0Cjx;yoae0t{e5RvH2=FPE2=%D= z(zrTaLELU!ENC6`dV%A;@S93t=1|L zVyb6uXhP`f?ExQnk(U_8(#h3}D;5iUyjQkr{ zEUgGSI6r9x4GS(!0Gt)~M+18~L7=;XU5&%Jg@ z-$!BohD746g(SyNJ3~v*DI_+a4gI#mX>C$@_LE&mfw?t|=z&8ZM?PUMy+VDoB#j4` zwlRJjSs(;XGSczS|DC!0;~;=l^hh80p=PC^{;T5Q=OFmU{eafl$=vFHv&o89KUYT9 zK>ME7voCJR0ulDeUX@q4T1sk;Enm~9uGel=&5})InYEnHKC{7#*;hdb5JV1RPD}pb zfeu7v1uQ)7 zNM%c>%Eyv2mXE#}mmI`ZQ>gk#81XaKOHv^zITtrtq`(oP*=+rh7jg`3YCf#3^F2ET zPrfK<+~X7D7-{N*HXMX8(2m*ov8`$Zc?`e43bf^A)VuyRt8NNpXf7Kq-GP^!RDYsZ z|puTEM8jy24V!liYa=moz1eQk43i!U=}}%!^^NYO6LaqPQ^_=Bec*OHKtu` zv*JkRf~x&UaUFH7R%oLxPSqN;wywke?DXHuBcqQaKD!b=(`m2V^aq-T%+RI8E)9*k zOl5*7QB0j~hs#J`0&;&~j2I=Rf65 z6__Vu$AN&;X-#;eX$`Lt^OWMAZfv(CH|AyzaHw7)UvH*ETh4-8CSB%>tu zthk6Q(a_Px^X0It(8XV{hFJ7!_D8uz$eaVp4IjSWHSAdyQ@aDmeyGf9Oy7;u6=A`{ zi^?Ys-L%43XCX-r4rfX0;J;WY!=WBK3EC+7Ubu)-`^d`TkvcZW`?%d9f`e*peYW1^ z>z^~Vyx9r4T7xJUDU;wrvu`lh%Kpx@>3*-947u5#IjOLVOwHPk<;gt*e4O%0LeTG5 z6kt=x&V%Yek;%2?I&z8E0$+gO*Ag<8Gn?jt`4x8ahSyN{$qP_2!jaA}o3=;A<}k2# z1UR3yS7xw~$V+gU*&){;7!yW%xd!2zUu-7EOL>MeD&E6ctlhvBc3g`w?QIz-sCEV7 zagZJgUiSf=XMe*1%1QEd&u8$EVeu^@&8K~vc^eXtdu5cMd;`|WKa`igas>2x zfr~!80`gpOOYnunjv+z3`z(Ef`_wyzz1+!+p>J;2aHx z0yvsp3Fzw}Ym&(T!zm?XfO8PS_QMk^xZ_|0E+G3RBq9wv0Y+Ok5VrsJ8wSv4@VSh} zs+qbkabb)w+(v8)Qv^A}1$`M*7p78pQHp~0O-`PJ+!2A`izf1;2jI%7=BuUvFO6=e zkPqVnrW}Z?5PF|t#5o0EBIp|>IOutSQX&ZO@2j))=>pJLVbWAN^*+);XLRE?sWmoFU&EBKsey_#p_GF zrpVhU=5!B;BI&e4VK7S_NH+2~`k>(n0H|he5Qn z3bEtx=k9RkvR-3g=NZ)IAbEaY9qa1~I%R|tVQ}3DVzOVogGDEXP*tQLCEl?s5k$+W zug=kto5diOg7bd!OoaBlrL?OaG*Ve_(ZJ++2q_s;+6M|S8bj#W_dqE z1Y>v^(5Xy*kZf7Elu9lMqZN5$)~n#`=D9bUonjRjpqKaO{aOte)J~x ze#}F=?)84Eg#oK1snHJWIitd@96Y)tKdOr#<190UL8Q~HT2Bq?cpct(Jt9%A2L05p z>1n=IOGFT4>z(#zAjiDMM&U~m^zjVht)nIuVy}#`z=DA0wsw6U7gsbGQvEv8u`e~; zkQEHV*N`~>8yIV#l zX2%xu8sm%oQG~V}Jh884=in#UvC1DbEgEt0ApCFP{}uQj+mCsS?_ zG2Rrz(j32dn$K|Z@J%3&9eaL8C$R->AU@FVdFna2Do%*+;t(D9t{I@7{MsYerRm?? z>$;5XN|+cWnI}>araf)_2<{RRm|$){fIlNph(Hj{?iC%-#k`Y%QIRBaV#*!i2|j)Q zJ1_i4(fDO;QZfIhts8~(Z}0UE=61gwOzi%bqTZsqrTn@n%GY$QTYUwkL3m<=cUk~b zliZr9<#NShlO!!QnR}v}y3|Qt9jXVQn8BrH6RO#=%?8jXJ#Ou#*B<>v z7y7!t@xuh0H4?&mch-dCwEHwcGcRbcaqqhe zD34$1A@~P;+b*rwiH~}J&eERshuZJobuZ~K%R3e(Jhj*nll_n=YB9avo9&q%!&I)@ z7iPLj)pq?HJrgHs|5Bs6TEco;7&8)8LSIdl`l{6U0saNEbzaueS*x9zPvyANoo=7l zPB$j)=+0;23=)?fo*rhHn!Nv?mnvKf7XrZaI7@`4Tdn=-xeuKthH1J zdNt<07!q4$u{x%M3G4(EzI8>*)BzfIVXgAS)-s#EaLo4w1X`jPOgc8_p*5K6(ZxV}Mqca#3SN6r8QAFw2skKp&DD+lZ)L2rpA0jBcIW-~i>fFgUY z%%tatfoyGUQE!~ah9#sbi8(u?GtX>>aVS$5MslW3rL53=F`Qs-TxTMwv+KUjVWfVx zgUP_rMo2F7epq{Q+ae+^j+_7-@x+QmF|E5VkkCB!HjTncX>$8#&yqs{~~-rVD#GP z_&a`YTD5BwJ}a0KkH(@T#CnB0l2-V9bQPt+N_p}A1UrP&E9sGDx}0m;0hGol9pd*_ zICJk|szO@9^yOUl<@TplnaBBr@jYj{7Zl*(oKSA1#}Y=Jl3AXgEX}?k%<64=4P1%B z2n8A{Z{*&mK1w7Fp0WRU@?#Y27IC7MM@l7c{#k@OB@(aIGr~^ckpjsBOmpc%KC@z+ zQL1crzw=G(Hlid^D$DRRrYU0aZV5}?nXR*wA{izjo_1Yv)hfaY!6}i_P9lo%F)1_s z;}F3_T@GV|z8;jM3HLMH1*{zcpAsQwymbmAR*Vbd>ZA>sMvK)joz|pZQSF*qj8g6|@JD2jA)ftxHEx6qx?J&|h{OZXZJogQ)@kao|yG zx%yt(ydo`Y@qS}e48B_!FA4C1Ei4BAY0J9Zw>OnfGwmYij=wPDfcWJw06}1J?h6p& z(hL+Rw0;<9Cho4$Y+${?1Zy@GT^yU2TGInOzdg(1RXJ-li8i;-)l_6p7Z|2q_;v`Y zP9`bz94Gel%V#c8j^e^GE8IOW*W|pN1~xT{Kl&_HAF`K@8s zhVBf^y##h(#7oH3XF_9m&#d@)2srVM7yRPH^g6IjP?P(WBVZvk>#9;Cy;mNLgdvfD zgp1=Q8X3Uj7G+}~>q?cgiP*31OHOE(`UshPGRYAX%#8F7aUTWVt2|=ygj6y}W{w zqcq(@(!uVq=BdHH1yZG;uK$u=e%PVRVPqM6&~x61ow9?sLCY_uaJLGMv>K_ilVn8Y z?n+01Z=eZn1cm{K@Rp(!CE8JX(~-)e8T8}1=T-E`AAQc?6n!q3^l4XVfr`IzYm=o9 zB46nk^Uyceh;YG`+%e@Fu^II`!u4DAhs^&qrekK|VTB67uQ+{Jmo7A#80R#am~9&g z?~_-~Jb$=rHk%7tZ?wQNDGV4wC7WlwmX1%npZORM>BIxu80&T=vs`-ZZfO};{D_a1 zbWe%2O1d4!)F>?rxd-@R7{AkWK#mk9I z>4UG&;+Vwh*4v;@Eu`X}&XeU^7?17l+W^S6e_&@0ykaSU-FrT2)X@n){tS0|K_E)j z*bRlyUp5qivguBbh~#M3@-*!AN8W=^7EfRQGk`GgY$A!U_#LnIl_PXq{wj~owQ$!V z1b>#5Yf749)NlPF217kJI4ols?F($oJU#^T*$RSkBp#|f8uHS;i^9c5QquyiC-4y_ zXf7V=Nm=0gA55G7+Z1Su-~4}g?ElQPaWrvoG5>K(|G%vPWlIc z|3c@8Cubyt#UU+QjR)(U583bGk{7|O%AoEKTbF}E2~}U(U@8Yu?xvL@#?i3Z2IAJG zlMvM5-Xo-4Gby$f^?sl}W&KbKHz6)IjYmvUi$X(sL;=0Y8|caj{@5QEjcv$}#)5X3v&mh(DD@F{n) zbcfEnb@N+QA517)F7;_XuCp4Jd1#X^Z3VINSV!{`Tqn3JTKzFqvS3MOnKUDl{`UUy z8eSw&^(knmSDZqnuI=&c=D+jsg^{O31+SVroR^zk>lk!v9DS4S1Qxh{{L0cvtMl~{ z@d_dvy_}nqGDuFt?Wh=n98<*eHa~u&P6R)E#k6!orrhShmOnc#{JmPxV*KHCU@0uU z{qw{0yzcPM)gbZ1Ou)JrPGTBJ!IACoUx169WJR!3FAo_Zoi5)g- zUkV$UGUZ4YviCxp=0(xt`4VQE>5wEW?VA}5K&IQofzCD0Qn1MMfGxLzF^4bWQeDh+ zsIm@pirmYF{jilkRT#OtDh)m4NB$lpCj<`+UHmC)PC7t%eNIZSiFKy<yR&XkD{D_(*nJ*Kj?|6E^9M@AZ( z?27RCWm=+&I6VR!7_v?}@SMOOp-Rg6`# z0r#i3h-QaWI~kP_pP&l>cL6o*HqdH+Z%ZTejWu12elN$sK;pAT58!Cfuw?9_hF97+ zz@9w|D8ra7U>Lz2an;jyI3RDYP}~;IT!X#3Yo7R_xcu=Z!TfQ~Z``q51G}@O2RY(E z3OaOjT!{o<7$5m9eo-lKcS9$N(yvQkh=St4;Q491A-6#RKXE_|$-Z850g@i@H((q! z{7axAjgq=0a?FkXH5~6OPw=4WT?~VO=~l}O)1?uT&?M}X0KgGl0+c_ddp8UecBlDh zQ2ts?f$0JMaF1*>rUKsdFhU_gmIigL*S2e~u%7N}Zf7`-C=NrT06>d-GA2gDwh@jy z4&g~l)lHS=;Al;z_K%j)^s5u=C=&Ny3OfcHkWhn~BUZBw0Bf(IG81vH{S-@y+#~~V zRtPEfGfla*tFGh%5#zfkRwBexC}-z_d@1^bfpT9Y26@0LP@NjQA$kxVCa{s*BcKaf zgMo*8I`N+$Y)4ORFfj9Gg<<*|CnJY-@+XcYg_evQtU&gR7u#FM1@kzaQcQcpgiaKwE#q6QWXxm zJQ7Jat5XRs0y8W$QF3z47CJIwqD7@|)Q4X5ktz@$0RL|gppyxd};$`zWf<$2`W~oCP?XX}QZ>wSsq0Itm zNg&Nht+D#1sN3t$_Jpk@iafr{!Y#00*fGmhd&x9ts$G^ffviDkbuA2!)B}Z{3D-~N zAv10fPjsz6%d0kUH>rI%!_$$! zd`?|&rPLI0X7R7#eA!q_yO-ouz2t8KBj}7_L|P(bkqLx@@SQp&JkbK*#s^1->o%%) z=m$?RezRmXt+02~E$k3)`b+gD%1G(LO+ulm=xo%)p9UKr?118w^I|lM+m5)xx!)>H5agAe_mxU&Kl$Q+!{)i4HD;39E#Q z1srQqo*GM<_uLfnWM}RDnc8=88rf0vP?yxj9-{MkBmL*^9NnTyUYlak(EnlVoT3D2 zyDXiRwr$&$wr$(CZQHggZQHhOSK7wpH{Ct`_e{^4xp*()=B&7hh!f{Ld++X>f)9gq zmsW&fO~$#vst|4t39W=H0ALS$heFmHoje9a2(ayu?>8HGPnW`s%z-<@bA*0#a$R=6 zGq0>8SUu*1`J~o4Ky4-LX{~y+V+fKDeU^B+$8V{Cw>g}PId{a~Hps(2E;r<2R@&+> zE{Y_xJ})i6oA$xnfxlsvvGp~9&CT386wvEhuHcfQ(zma|!Uw=;Tx9hM<4OLh<)b(ILM)cX zKRw7YEbfgNC2!AXoDc{zMHIe8N`^cU&3k^t!`9RLl{OKdxoeL1F&4jf4CY%vm;{Ta z--R@tbLZ^UtU`I5PX}z;0n!EK;LQf*^-@BAwC`d3&go+1ZZX!)3$3LFG*h97<<&|k zh#MhBCe8m5q^6v`UCNUaPfA#M{C4Mr6|uPNaEm0y^h^DDc8$%F2Uwg-vec|rfKrH& z!tM&rv$re2I@=XAFQ=TW=enu&BA2Zp3u?R9(6vM+t=&_4@ha8m$&X-a(O=p z2+Ic=X&czhe-(WJ-aJ()qEQduk`z-0tbg{AK?mHp7|sK;Jzz|TO66mK$ehkR8QG^x z9#9h=4pJW~?EGC!is2{2do<^B6TLRO%@#OQ!AEa!F{5lf@E56f?;rJ%FMACq?hvW5 zps?N@y*}Qr_-U73zl--aM?bVnQqm+pdg6sM8aWEhLUgiVFAFQwSp2J6dVU%mB6#}q z-&6+10$peOXcVO@UfT0;;#SFr4l?3#Cg|hU>@_$S6QrYq5 zqFbD7k`4 z8H^esk2lI7*)61+@Y1yOO_qU>zfLsfBmf0!kq+}C!#2cZjvGMR*is#d-qqpF+o{AL^ z-p22KMoF#D(PS9s-HBMkos1YiEQbgmCic*y@xA`J5^{dsjTF6WHoJzi@8jqnM)tu^ z)Yj+jRif&GXIhAC@%N+r8g!v%L9U$aErc9Ah;>ht&N4< z>ashCaS!ImhDNyDoT#EgwQOWb;fMz2V&nWrxzh&$n;gMzStQ7yl^jOf53{w%jPcFv zjaZ@`XnT;Tv`MynL+xMg;Ef^$`5ib9DO~EHhvTK`Wh7gp5xUTwCe!B3^+FtiVPH24 zefYN-(~fhP#9nn2W}rSI$!)VI({1{aQ(>Q8rnU<}rihTg`%i(Wz@kaA7&-OHHT{nsoK&WnHXsF7Ye0bLHvGOolgqw-%XDXzG(-FFQl@$sI9IZ` zpseJp_t6X>N+Y}FiB3&r$A{-UQx?;ed@>D9n(U3 z4|e*nFXs5rno0TI%$OH&w8_UfLt{Z+Xi!8*a0Ieqk^3f5p@%FzSH@&O`ih9~<`Ji{ zuwbr$PzdmH{**n2q=>DPGA=hKE(h0Zzh~_L$2b2Iz`)8QcEmIMB}{RO!ye(J3KTS- zj)@N**nxM%ar1_O1%_>WJa_d4bul)V06-@X_m&$Lo`cp%q{kw7(&=J;X-Ou9_Wy01`;`-M>nj-f=bdY{~t z!1HXrtLin!#g%BjEzjK`3DVNJdtGPAvR__DXUZ>1U-@`su}5V^$B>vGfL;v{%v*Rvyt=D++Lz0v;bjUZHntS@chHsKcdcZhH{rk1|Ln|iiGY~--SFq8mjGVM{ zr}r5=Uj<-F=RDMXvWV>N%>=x>M_#DPMM>L3kp%pQ>1C&ba=KK#ccUb0&zFX!J6Lmx zho}WqKr!dt69RZyr<|<$RyJ}-5X*065*(R*X?kgpLf&T>%v|9j_AuiL+!!Nj;kM&p zsogJ2I)&GN5U-1Rr3@~1S-bXW;n8%%TD`*-1`}*IeYUC={6xb17c?J9K^Wb~WIF1@ zV!2zCp2Ov18z>%Vvz0fwx#&|`kW&7Nt+`NUPG0qTxTcwM%Y@$AlWTk5LOkxk| z+rwOfIKR_s5d0mw8(S1;r!^!!kE#gyn|8(O+4y;z?P@WDqJ1WyB$tN;9gYya&7;2J z9bhV@`IUiN4VF@*@X75pB=wqCKS?{%PyU%6g?^?sS|@*y_>bnQ58CQt1XAl0|SfvHprO}h<+ zt5dV4xa6U3)enPJg`0j`)S!m-zgEpx5lh?nGNsV>Q1P$IyPOiEIYx{XMK%&e|N0V< z1V6^#n5mqqa<~;$!h7f*a@M`e)~TjFRP)#AAdIb`pzjhwBcIRcJJW4?YVLB;qhYp% zH4{n2?sJ!)m< zz_D#x9T$4TZ)3n-)}=*VkA!0xyPkQ4C`3>k$e~XOo44!-lM#b2uTRg0oX~Bt@wm+Y&-6UIJnh zZ@La*zC4LZNVjC-#7Jxz!D@2Zg58?y;2%~FmgRD-8)}|HOk42tO1)@WTrI}J=@2_a zKPZv?axd{(zHxAN5ze@tTe)vVP+=2>ON*;QtMR%Xpi4jMnd=3T~I>aDIQ@6NjtOMq}s_vu@>L?w#;&M${#45;!lAwv1p|{9QpT>7ifLO8Q0T*8YIBMWp;fQ& zcJ_AuhWgJ<-ak{l2k82WLxi8)XS)A0@aun#;jFBzX&j6l&8^IhZT{2U=ARMYe=~o} z{%8Is+Wcc(Jy#*|hZu`^B(tjt4NGmg&Q%-4WqjAuR+$ncaIv@Y55}b#aDG^MEz;yb zC$?b}42wx%v}+f7r@htWeaA()=)CSLq^FGQj8rVs{(7baiP^DJ?!w6}XKc=3nIg;~ z^54@_;-lp06sZgZq& zhliTFO!41|O9{<3m^VbpEsfnwBhWBZOy~!`<-j3mB&$Pps1L1hS=+6>-}!k@2)+rY%qQ+ zZo#QeY0$fjo83Ro*9GSSPm}a-5vD5}qRts=HIi*DAOd zD!6_uc;fQKq^pCt;J}ruFVt-US{DyuadWB ziE4TKu-t_k$jJ18EI}p7OAKtPf2VhXEA6bx?DBhp;{2v;RflntW68joL&AQy2_+QI z)xZt@Hi;-cV7J(4XNV&@I%$!llB`CUmGx++bJgke=;|nGfO3K-j)hWofm(1n{t(7d zt@+pkQgKNe<{Leme)>LdvwIv5K3}QU?wr@|U@P=?>|;uBpC}0NPQMPSK!SnsAhpzB zsS~YFib~kAWM3C|WViN90Q}oB2_61HTZI8M#xKo^D85h-9_5xBndOKUo6yKk&sQZ< zC+*4N>3=@HL5M4RDiYy?^sT6uoHFSHaoyk&%5EoK@>fYlJ06G!o>98_4@sHY8UV}a z+Gup6SkCQXUXF|~(O)M}AYs|_MX`CYA8MmSCVqZB1zHm~rGIdjNSzJ2bgV#-11E&{ zf`6;kpZ5LE-2E-Z(Y5X4qc}_ix;^(A{>HPvdB+oB&}KSy+p6Dlk?=DlS4^y=oo@xN zM(5zziXbc!8lFo|2bigu6lg;Yl8Q4U8jMH(3 zX&No7jR+R_7!2T_lc}k}QPa0lXV~o`WmZ*PVC*$tc{8)q87u*5^{MeWx~VLibHRst zeNh_gR*c~i2aTV>4PFuT<&0z{AhQJp8(zm}Tr@8cu1Q^R+x}HnLzDa_ z4wY=@ZGDWXg>33>sEJ1ys1!O%W()H>qCe%iruyla;WlBgC6d_sv0xc!9Pqc-glf2N z`EI?mGe|NaKqP^Z{)?)hO56pC;>$8Jqq2LY)@e>&73Q|MW+ri&y@Xh+I6y0#8a1vI zCWEgsdJ@x}Yac)vktiP(9)YAXZ8=e#5C(xPJ+0*-#KN~II5KbsrF;Jz_z)$q0fa?* zl_@Y%OitmCGQU&z1K998oYcA$MIy2WgpX75n&A~H_^ZYS?o9{Q{t?OZQEZVXe0)Hi z94Ptja+d;B+g1jTzR`(p4}iDuQQs?BXIsC47v(uO-sQu4u3l>5fE(hEg1P1R6og{I zJg|I(r(@A)e=2d=ulq)}1=)IFjdh1+AEOGo@#`xA-2-RVddDrA_Smk5#KMyVi0J{_FeZx?}L$Ro7_VK{|v(kPtE5iuv|PW1!I zAs-aq#3tw!x>&FI>vmCE-|sCc?VZhJj4I z|1LS`O;Z#Bn5-qwb^<|W6nTHZKYSm=H&#H)o^Q_9^e$lmlSU9fF}OvZ+|C9}u$g~i zWx+Qcc~eCcoz;q9!4c-LE@edp2U0WvAjoh`wei>lxAOZLAnzH5R+@4Kw?MR}=*@^J zk;=#cUaYH5jmV8ORcO>ulPDoj$lY3P!CJM54K-)3raroQF!~qotA>?OZ*rrPhmbu4 ztxXj1!fbQfU4&`3u@Vx92sWB95J2w- zpZm%54Ch+QEQ%{@H~44%-A6G$wBF#~o;_U8uzNy%biXWWSsN{8PYecJJypVX`O|*C zPCMGc|5ArY&nHGGvihI69CT>?~L#ChZK(R?Acf!t@#w= zSJy%mrY(A-QF#PBnb&}o_G3PT8Nfbz#zbQV%5$7-P}156U5*N zkY9>e%Ss|Fi~11;I)({ z;z{q)+u=2KUusP-oU8hXaAQbiqf zxjGQd5kUqE9k3vH{p_xnEeTM1%acq)Vs^sPiGNidt#}F9Q>I?E|%qK0010AxTVaR1JM3stgLyyZFlF57wI9($< zJP#Yfq<{gz?PTbM+}L>WjvWK%^n_8mjYo^ipCzs% zDIo1W`N#p_t{f5GS((d@viW=Z2#1rYSxcWcSa&2`r~k%ynuI0bmLV)>QlqXq*d7oh8+tmfgLCG0$o&$)!-il zg=pAWN1EDH&-Oy1ZU3?1g+4P#~j$DR!By#HB1qo7J z;M|~-%;bH^5!{6ADmNLR86@O;zk6zKVA`vctuI@f?@McGY_2>&(#T=(Z$qyf)6CYb z;1YxqGrkNoy?EhA`3)7h7=y%C-_$k#mPQ^SD63J!bM!YskE*tF3N4cy=!jo>P zR%sqz;f;WDcqdLFqT`#qW`@sBKoAVzW1lx+!dNuuJWgK~!UwVp z%nPRB<6B!Z-9hGXr<#R%)m^w631qB^;gh}%4#5n#c&a z+2naq+61J;V7RVV%u)>==J!i0gyhl)qP5Rv}$_~0i;<{x{QO9p{F9)%7?DB4_ zN$eDtyGrVai)0&6$q}&Qu!I`wLaA$iaOiM4?{bKZL{2}L$*s@6?&}DS(-DWV z(pBpAB=8oLG=y`iC7bSb(Fu%9(Ir^q-z|A>JcRKX@p$0wK^D~E+m+%?%U=BjR2ZK0fs~q5o#xsvKwnld~({_3-YEDONhJNT9Rnn=`A77FAY@=izEh! zW?RYJL?Dk?!}0_Gmht}sXp(P2d0B(vI|p2zu;Y?`bqM&$m9R?5AexiYAwY@4ucsW1mZ5*X2A#iQXrXF zrx31{N6e`8K9x}h;(2ffk-gfb)HTyXggQga2`ZKRTems*XY$)Us64l9i?e^G`81H^g!~G3a35;+%|J<&brFJ~$8(}2o!;z->hI_-$_ZRD7 zu%)q8kC#)+b`i}*^`jRVD$;Dk;bMQ<%$qyRjugjhm*HoDTFT%Q*;DqC;JXXYrg*k$ z`wwwTD9}5)QdMPlmV2`*JV}1oyYl*!tHR)$pomJrd>QAmqUEw2xXAb>pMMa4p)nKL6kqBji z%L%zceaplQdcn`Xk5em;s?oO$FGcPlDr>!j>;)hnM&Z$T7vi}y{`^oKT!3issd{DU zDpNHHvs6rf7O{Owq4JViBu-&Nks^ieS5E8Yvn(!Aln( zQ{j91xAlb=jZ>o)e7{4(EKBCh(|F)7wPE=1It&`p_i@@q4^E49?Q*&BU+m_(|eo+ zw+?5}?4)nu?y2GKO)7F1R=aowteh;d3!tV@Sz`v^qw0~BXW_;o!E7~(^Q z7B%g*aBGgV|03%p5N2C!-(LLyL`wJOE1^so{O=Z`8|@?=R^fglh{C(Ih}H(h_;{bRZH4jV+C3<0M@zNu(s=@4EPwwme64cGHtr9@bX2M=VwR5m!BG8!)ZD(7AYIwN#3} z%j*rc^0^cn+eXujHG6Sy=mgc&;i_v~d|o;oUyu8yRa>1BoP0K~tbEUHx9?e10?0m0#@Ur=i?6e>8Zi6`Xy|MegLZ0GX zXeqmVHfh%G>wcQ_ES-ILrFKH}#eXi7_&y`D8?rkd0^gP@dkwjqE*5Bt3AtZOm>zsz zTCO}_R|+KI8U_HLv##${zFOwG@N`I9@#u$-Q;g`Fn#DKrb}#94BXPCPwn3l$!1%cd zh-Br{o|;v=AKuEW^w}7g^%cy=5cBu}453WoV}{BFpIddC4*3RaHpRVM8TvdxQz&rA(lI_@wEk`@odnIW~ZN9>Mg0sD+z zx|Q;Gkl-cMEor7uNRxF?k#*t=lfAL3uEE-W(h+Cu{+Q%kK?v4D3||J%5_^&btPnd> z>(3}saIcdOd)3ogg+8w}y%q^9l(|p1RCWq3Aav`1gt_+gUe6s1k{ElI_G;0NE@IWl z#xd;A|2B$%^wy9x(3K2C;p6c!z#6?KA)gnQVQ=a4(<*CgDrYhDn^M@K0ar`yUEJep zu=B{dOj#hdwO{`nJ|lc%~=3`_uIGW&Yf+lVl1|JG3peS-}MfxJM%a zf*7vv2*`K3%GX6op;jOV;ml2WmP56&*D}aeP=VR1l8mSl5rvW8txd==XwI=n6A9^V z4?VzyXp#h}GFz#pCHgyrzqhz{i$fW6T(Inl4W6g~IZ#qEHDtb*x=n6oYY@D$F9!Ub zLj#=k!9@5Co8_RlmEQU*;c_lOZ@U0Ce@u~I@{H|}VRSzXyyNb<}W zqdGxR2cT>*p)GS31GcE#w0H{X8kRMq{dU{QhBP&E3_zzz%jTuktzLIBA06Y0qG*vjMph{Yzb~)k_fma+863vPHZi71q1XB%BywDKAb$q*K z5y^(|cBC!WqU0jo;?Ns`OH6(+_<2_*;_X91>BIU7|4`U!+=|MWLsqA98jw~g7IkY! z6|HIO5VFxC%9iJ`T(YZ`JU>!(1G#j)i5u_zbwMB+ zsw1W2Xq9it&dNyAFvMp)i-{2&+fyb@@Y+8fjwEsc@#8%c^k==1+i~)ATwF;_*r;J3 zTH!b6X6-Md8I}g4Rs>tdd|c%%diKpHB|P!OlU+!Q=@5P4ZbHkgOn@aZTnlD2bC&nj zXK~MrEtWGyoLDrYSL1X5_Kd4~F#|KvCuKH73Gnimfbiz7&nBaVPjA9Y8pO5Pj+%3b_QG;99mTUH^2Vyts0M25@I~g<8+%%tk?nE5Y()K>+;?E zSPG*^#VdXUz$&msZlhXFi^Q-ySuTwyG>}f@hSaHCo8XYSEGq$kDX7{XmsJM~xqZ6a zoW}P!%Kx4pi%!nbmLfG_?*cXQ=#$O-dK<~E!1}s63d~;myrc_^zDC4ouHlx-7zWNY zdrN0~zUe`!_^N^nOOY^EDBfNFUDsvu5;S5Y_TW?kYxPoMHs2%WDZu}>aUaWMvKaB{ zYHY=n5eR#YzUrbhQW>&qyVDJ8_K?~mhYrqi{T4e-a7%`e=W#519#JCjHJ1hz$rwFX zWOy;P3-b0aaqs_F9sK~RZ<(1jvuwxM^ z<7c4K>hv4LKrbXiV59KLk&Ap9#WKj}iiwAk&<9QP7>DYgSUy)~7c}dt( z(};?Np~U;by10yGsjVxwN@;jsT6x;+4^-z@R^Cy>;|oP&y|i-KyTa|SyicQYgQvqs48qJD;l2uB#*zpH@T`_goom-l<-vV2#gznl~qcwA{{gx zDKi?W4~!$RHhV2+SZss9Sz!pkK0;p; zj=;JWD+zOU2`)9%OO(s{Pf)9c%e&xI3=pyll5UO{QyCe|0Kwzhk1pA13T$ z6)X9^ZFf6mW1n#eT-?iAQXPD8WEr3A?_{AZ)Q&|5*R2z+zQ0Gm8qYC**LygJLmRCpTZLT`)+=^)?KfCVj}SkCZl@njr@P*yRqCv^vV#?A%>U)RGITVxpd-th z%K4na)y3ZsS6QxCaz6T~`20cleosMcASF&Woron@+3GAEMKyhR7&0wDJ~#rLqjwao~FH1;;iOMHHYZY%(E1DIks$;~-LxIh}W(t~~# zn=@G=%vQ4^6zS#q9#C(&qVBAibhp^@KDUZ7GgAZ?*r*vLbr_}}!gTQ&IB#(;AIE+z zW4%@7K)LuOX3?T_=}6DaNy+?H6n0aRiV(_B*l5nD7gKX=2`I1P(JiCd0Yh`j;i)~L z6>$Q8PjOuIlitPETY#U5ar(L*+pg};DcthbCVT$W^;N|)C)0x_TlnjQrCI;wwRvS5(h&sl}`7-gBzu2f(D5?W6GN-hOvXc-Q*En)GJU zU2;}C=UdtGe)tub-8CxIu$v$5GZ0i{hRS^BkdhF#fkUGK2~SWc9aKbbgw%+L+R$pf z7%Ms$$^X`x%57PjuD`*ufN=Fhc|UH%qbVr#bYv4 z)!LwN(;}nwLMRC8?@4KCEp1$_9j5kl$R6cVb%=N31MJxeme5SsMaZcsc63S2h9$%b zEfwjSwNuUhyexk&#VZt;C>X}1J6}(ZSb!X=`Ti)FK|(PE%|HVr#gpToAIxtNcfE>D z%2FK!D5gz83RRzslc3-m-#s18LgtckpiA~WG*eaPh9xI|C$z2*7MQM1B51g{!L={E zRx72J7iVNc%RHi=U$8jkc3a#aFRn-@Cr&vm?g_RBln}8-<7&9zx)V>hCOQ8h3TFO# z(eP4CGE_)xA26~&)*RR6DK1Ghi57`%du%<`0ccIrX${A6o03h|JSZ+I3MM|n&CKu# zUI7xb*(`t?R{E4iqy~&G@ zaWNoXJ02y=?Y-CW3dw%`vXJEgXawoatA9gcs{#L*jz;mkinlvx$4Al1XnS)B;oTFf zJ@GM_?g1iHjX(C%-J(vN)`Ar>WkH3x!V9P>FkVuK$xxt13S@;}wkltQY^|HlHVqGOYyu z9NjrxgBg$pqcW42x135}_!9QRn)>g*PdXJ$$g?*eru+_-a z#z&Ctd4Sc#&92+gGYE}#i<(yxeV=*}>6-0X!zRN=)$O^GlUdc}7NVA>87{Vk`=Q|Z zHQQ$Ad2lAJzgo{;U>t6`tvBD!AqCgJUVP{PPk)08tNMHwazMtxMlKnT&snVr(Fnxv za%K9y84KVDPvJ0-TfXgcrR8;c+sW{?os(A8sae_88e`=+A@VNudGxZ+Pc|mgrWkNiSmR4rIwhx@SK-c2!35e zL>kE%0ooQ2Amnjq1LQ2S$9JjP0ud?!F)9=WLw~V@>A^pNN#jEE%+ADl__S1PrURAM zWkmgMqY`taURW^f9wv~#AN4KJC1QrbI)#UtH&-=a*+7;d?~tVj za?(pby#c-Y^Vo;ia)fuSw(91jon&iLqg;}^no7HI zvZGgd2i%J?fTAqQ`#V~Bp_OMC4las$EkR#ztyvo!qAHwIfvq;&9)dL7h&$H`3ArzU z+tg_JWCn+1%m`697r&aoM643ul0-c^94P3OY#w7il86&u)9_dx|ItF{TxvR2 zw7yWkbqrAVg<%Nk(1zU5(niJ8xTS+y94UbOWs536-M-Q<6Z9=#-K#_1C3>?hFlU1a?mK)O;x0AhiX~des>%lfd-J^V0r4eF}C|>a44%<2IwZ& z;Y-t0%h-|6{ZI4|CKs4Ki5A1y*W)@5sJxH)&K`Xor^Dc3JBL7!qg@Je%1Hvb5NYvb zi;_x$`8}l10N0y(rS%x}IJGnN&aU=L18R$4@qW$tEkhoIV zJu`B^hkBjmQTBrY2S1coHl|`9{Cg7HctCj)Nt8+ieKB}z4+vK5$hH}t$V(s8OCWMo z_4W0S!Yn!-35Rx23DBeU-AHeNBs zlUDVMgLb`awM6;8c+PewbBd7*4U%Bj5Yl=Ea(9lUMb&=+3CxRs(b#jCn9B`&V3z05 z%<|9cAzhe;6dgdQaQc6u63XR*`1{S)EnGv0mu^1Ly_&T`=W6YG((1L@sSziMAe;uC|He+i-&c~V?M=o{QOF|?XG~QY z^`G5^@gvIt`u)(jWBJ%63QQ?;4zB^Nx8?Iij=YB?IW8s=;TZ{(PP%d|0+>U<2Nt>H zvk0qUX2*0p?2&`pAMpitgkXUV!;PS-60btgHd(wEG$==IM|ypVM=_6%-EX(fH)&jp z|EP+jCz^!b*RZi9TvUT?&8KYqA`X=9D?TA_2h<+UALHg~D{((Ik;x+8oL*cCuB z_PqKNT7$pQCAk$9PlXNB3t*X*p+S=!Qsg?RA+Mza!gdmht9L-IgsiQc zG31o3^|7}FE93Ia!mluhw>P^cgd5`29{4$7%$sF5?P1*mQ8&Ou2k#)ZVv86-0QL)f zR?u+Ad-Sb^dcLpEB4mF1ejBVdolYsbtLLVD9DJ-zsYufwlKnB{`%>C@lp)_Mm)LD8IAul8ohuW*2LJKQ^wGD$sKYbfu;j~F zxf07}!y0FO;SwpWyen!=aKqcKcwd=o0srbKCgi|x3`RR49%*qq(Mozy$|BHZEe=mu z{xi3{3SNGXn5VrR!<&H+Of?VH)Po^L&_H7DrK8B7JKkPCfCGkInIS$MwR0Dp5e zjKpw@g+iJfA;GMLm7DdP{Ck=J7h*21^?MSfy^$GBh5!@8kx4gLP%Z-K^!vI2t|qWc z#z2E%K2cx=)hz9RIqn_Ao5F|%Wom(D*n2B{9I}p7HF~dPD&^Cxd06y^iQo7cHAM?~ zz*#tf>loJ>WBWtMDbn-wX<;{I0o%==m_$I=cAVvDvK5%9$6=}1sYPJr`K*{iRRsWZ z#wkbf*>}sQfEhv91-qrs0$XeRR_X)pTKe;U$vVQoDNroK8Pzrais~?dfA)?YAWg%h zzatKw)ZoAcN*+NZ4v{e38CgYB<+9{k8j%?cR1A(IVPT&?a2S+|ro%QG8n7VHd07fR zw51VJqH|ynhe>hRNkne^r7K9vTpw7HDy+^hi4Vpl22ymISRn1@sNt3od+YM@453m23pfxU_pgaOXVb0;#@x(q_2;jFvOpk;S-A-9&=I{c`hY6{u z`-ev`5xY-{@D6E)?_VwN=xdBG&Kise#MsdeHX}?(MZdyo9^=(f-hXkZ{%6F5Kms+F z`Qw4!2mYUKn*XxoaxEyp1hK7Pm_A`xHdB!^<+G$Y`@x?ATAo|2>-IJrqzRryEq9^^}V#6{&9Ye zOJEVAP8Ard3&j>FV;AYAFVm_0TPjE>vYSg=_juAevPRcX`6;3Ix!t@KW+j|MV@IUmZrFn-!B$mkU>@oE2Hgv?=%H3q_HqmT+KrRISr^Alsw0u{t7e4An& zXZ0n(FyQ32@TND&QWfb>GKCW(xD}4r_p-4{;Tgp?2u_M^TxtQzJ6(E|A)LUUVx)z$ z$N7ixFH>zaPAf5vimjWitD1Ai3gdOlv;InLWWm5LUi0dRx=QxnEE|snnhR`OEuTe5 zqvWJ@?R|?KeAE3={;mt-A_>&e^R#97Bt?LBYk5{&icM%Nf_mj??Z2@=h2e(8*Gbra zp8OXzldDNL8?*y3ZuJgZ69W1Gas{tIixzIroj9OYCT<-`&<2gaf5*I&B;jmm4y6=w ze6BIu9>6uP-kHj9kVQ|21_PU`c(1T)n0QLhnI!qZo`9^XxGvV=yIx|35r_hbfuQ#T za)a*~lA!GEbyAfr(!dW3fPr70F zLvxVecLQg}&Y}8lvv~lvB3(Ob*jkb7p>BcTJFeuP*|N){96WF;NRki# z6=Sf!1dLEq>OW1qmd(bA?oFFI#I6BxmK2x_r7`jeY~UQ$ zy5D$lEu8Ki#|sgVBnF_%9&>5bx3T(ohWF!b=+rf-NQLNv^e0$xj{_WB*+HLsB|QvY zXsbRNA}Ee46VXSoxX|Wn*i<2ig7A$_MD=EQ*Lffd@-(9YE>H?vR2;wchk_m?v1ze@hQFjktl&^W)6v0KILwGvozbgdM9FeXXa4n&Gg95nw*nmIen!LE8 zDX!&icK7WFWe8@VD5`@Rvj2GVWqIN0so5VjH;b?2#>S~N7F=4&BNd^6N~-K>2W)H4 zepQsmSQc@hq@vWP7dNVK4JU%MrFE|>tK0stH^DGPZ59YZIE@m)qI=7Fn^i6;(Q5za zInq^rPvrAo62Sg>;l0Hv!SDVAKPZ6ww=X;ceMd7IQ)hh#qyN~&|1(Vg=lpNG_^Kpr z+j%|Ye?ogi4Ny<9yLP<9nYPb{Xom&`IL)YO%sE+4QZ7KW?8$!zcJZgcoQNz&lMv$5 z9dwY-EI16Q-k9gsPeq|4NoO<$Iewk^`h1rdV+;d92eHvWo_cp|a9yAvy$le3s7)q7 z-K-<9@a$c}DA}l2B_&yAcAnpYY6 z1Ju|O0!acsbK|FYvh}8El%*c7t@R?{Bv))z)o6dV${{w;Kp}tZ@4ak$bQyz==EDWC z^@i2i5|lV(!!0S5HkGxc4njjIhL{#@Sdl@@=#*C_Yn><3ym6JbhA2c#GAN=%0DI~@ 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%0DrbSzHu)})+k#X4 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


a4|KK<(wsZ)K3rvb$|Sayh7#()hcQ^h8b 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
+%uJ43S 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 literal 0 HcmV?d00001 diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/resources/templates/package-ts.md b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/resources/templates/package-ts.md new file mode 100644 index 00000000..e69de29b diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/resources/skill-template.json b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/resources/templates/skill-template.json similarity index 100% rename from qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/resources/skill-template.json rename to qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/resources/templates/skill-template.json diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/AgentDomainService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/AgentDomainService.java index f42718ef..209ca0a3 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/AgentDomainService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/AgentDomainService.java @@ -96,6 +96,8 @@ public interface AgentDomainService { */ AgentComponentConfig queryComponentConfig(Long id); + List queryComponentConfigsByAgentIdAndType(Long agentId, AgentComponentConfig.Type type); + /** * 查询智能体组件配置 * diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ConversationDomainService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ConversationDomainService.java index 00a79deb..633a27ee 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ConversationDomainService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ConversationDomainService.java @@ -68,5 +68,7 @@ public interface ConversationDomainService { List queryConversationMessageList(Long conversationId, Long lastId, int size); + List queryConversationMessageList(Long conversationId, Long minId); + List queryLatestSandboxConversationList(List exceptSandboxIds); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/IComputerFileDomainService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/IComputerFileDomainService.java index 1d4540d1..b61f3033 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/IComputerFileDomainService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/IComputerFileDomainService.java @@ -18,37 +18,47 @@ public interface IComputerFileDomainService { /** * 获取静态文件(流式返回) */ - Flux getStaticFile(Long cId, String targetPrefix, String relativePath, String logId); + Flux getStaticFile(Long cId, String targetPrefix, String relativePath, String logId, String customTargetDir); /** * 获取静态文件完整响应(保留状态码与响应头,支持断点续传) */ - ResponseEntity> getStaticFileResponse(Long cId, String targetPrefix, String relativePath, String logId, String rangeHeader); + ResponseEntity> getStaticFileResponse(Long cId, String targetPrefix, String relativePath, String logId, String rangeHeader, String customTargetDir); /** * 查询文件列表 */ - Map getFileList(Long userId, Long cId, String proxyPath, UserContext userContext); + Map getFileList(Long userId, Long cId, String proxyPath, UserContext userContext, String customTargetDir); /** * 更新文件列表 */ - Map filesUpdate(Long userId, Long cId, List files, UserContext userContext); + Map filesUpdate(Long userId, Long cId, List files, UserContext userContext, String customTargetDir); /** * 上传文件 */ - Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file, UserContext userContext); + Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file, UserContext userContext, String customTargetDir); /** * 批量上传文件 */ - Map uploadFiles(Long userId, Long cId, List filePaths, List files, UserContext userContext); + Map uploadFiles(Long userId, Long cId, List filePaths, List files, UserContext userContext, String customTargetDir); + + /** + * 导入项目(zip 替换工作空间,保留白名单目录) + */ + Map importProject(Long userId, Long cId, MultipartFile file, UserContext userContext, String customTargetDir); /** * 下载全部文件(zip 流式返回) */ - Flux downloadAllFiles(Long userId, Long cId, String logId, UserContext userContext); + Flux downloadAllFiles(Long userId, Long cId, String logId, UserContext userContext, String customTargetDir); + + /** + * 获取沙盒日志 + */ + Map getLogs(Long userId, Long cId, int tailLines, UserContext userContext); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/PublishDomainService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/PublishDomainService.java index 38ef7b95..869fa6b3 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/PublishDomainService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/PublishDomainService.java @@ -32,6 +32,8 @@ public interface PublishDomainService { void savePublished(Published published); + void updatePublishedGroupInfo(Published.TargetType targetType, Long targetId, Long groupId); + /** * 审核不通过 */ diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ResourceGroupDomainService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ResourceGroupDomainService.java new file mode 100644 index 00000000..29bd9be4 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ResourceGroupDomainService.java @@ -0,0 +1,19 @@ +package com.xspaceagi.agent.core.domain.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroup; + +import java.util.List; + +public interface ResourceGroupDomainService { + + Long add(ResourceGroup resourceGroup); + + void delete(Long id); + + void update(ResourceGroup resourceGroup); + + ResourceGroup queryById(Long id); + + List list(LambdaQueryWrapper queryWrapper); +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ResourceGroupRelationDomainService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ResourceGroupRelationDomainService.java new file mode 100644 index 00000000..c392247e --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/ResourceGroupRelationDomainService.java @@ -0,0 +1,25 @@ +package com.xspaceagi.agent.core.domain.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; + +import java.util.List; + +public interface ResourceGroupRelationDomainService { + + void add(ResourceGroupRelation relation); + + void delete(Long id); + + void deleteByGroupIdAndTarget(Long groupId, String targetType, Long targetId); + + List listByGroupId(Long groupId); + + List list(LambdaQueryWrapper queryWrapper); + + int countByGroupId(Long groupId); + + ResourceGroupRelation queryOne(LambdaQueryWrapper queryWrapper); + + List listByGroupIds(List groupIds); +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/AgentDomainServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/AgentDomainServiceImpl.java index b6a3e4bd..8483411a 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/AgentDomainServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/AgentDomainServiceImpl.java @@ -197,6 +197,11 @@ public class AgentDomainServiceImpl implements AgentDomainService { return agentComponentConfigRepository.getById(id); } + @Override + public List queryComponentConfigsByAgentIdAndType(Long agentId, AgentComponentConfig.Type type) { + return agentComponentConfigRepository.list(new QueryWrapper<>(AgentComponentConfig.builder().agentId(agentId).type(type).build())); + } + @Override public List queryAgentComponentConfigList(Long agentId) { return agentComponentConfigRepository.list(new QueryWrapper<>(AgentComponentConfig.builder().agentId(agentId).build())); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ComputerFileDomainServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ComputerFileDomainServiceImpl.java index 1d64e8fa..6c8e2463 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ComputerFileDomainServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ComputerFileDomainServiceImpl.java @@ -27,9 +27,9 @@ public class ComputerFileDomainServiceImpl implements IComputerFileDomainService private ComputerFileClient computerFileClient; @Override - public Flux getStaticFile(Long cId, String targetPrefix, String relativePath, String logId) { + public Flux getStaticFile(Long cId, String targetPrefix, String relativePath, String logId, String customTargetDir) { log.info("[Domain] logId={}, 获取静态文件, targetPrefix={}, relativePath={}", logId, targetPrefix, relativePath); - return computerFileClient.getStaticFile(cId, targetPrefix, relativePath, logId) + return computerFileClient.getStaticFile(cId, targetPrefix, relativePath, logId, customTargetDir) .doOnError(WebClientResponseException.NotFound.class, e -> { handleFileNotFound(logId, targetPrefix, relativePath, e); }) @@ -42,10 +42,10 @@ public class ComputerFileDomainServiceImpl implements IComputerFileDomainService } @Override - public ResponseEntity> getStaticFileResponse(Long cId, String targetPrefix, String relativePath, String logId, String rangeHeader) { + public ResponseEntity> getStaticFileResponse(Long cId, String targetPrefix, String relativePath, String logId, String rangeHeader, String customTargetDir) { log.info("[Domain] logId={}, 获取静态文件完整响应, targetPrefix={}, relativePath={}, range={}", logId, targetPrefix, relativePath, rangeHeader); - return computerFileClient.getStaticFile(cId, targetPrefix, relativePath, logId, rangeHeader); + return computerFileClient.getStaticFile(cId, targetPrefix, relativePath, logId, rangeHeader, customTargetDir); } /** @@ -109,33 +109,39 @@ public class ComputerFileDomainServiceImpl implements IComputerFileDomainService } @Override - public Map getFileList(Long userId, Long cId, String proxyPath, UserContext userContext) { + public Map getFileList(Long userId, Long cId, String proxyPath, UserContext userContext, String customTargetDir) { log.info("[Domain] 查询文件列表, userId={}, cId={}", userId, cId); - return computerFileClient.getFileList(userId, cId, proxyPath); + return computerFileClient.getFileList(userId, cId, proxyPath, customTargetDir); } @Override - public Map filesUpdate(Long userId, Long cId, List files, UserContext userContext) { + public Map filesUpdate(Long userId, Long cId, List files, UserContext userContext, String customTargetDir) { log.info("[Domain] 更新文件列表, userId={}, cId={}", userId, cId); - return computerFileClient.filesUpdate(userId, cId, files); + return computerFileClient.filesUpdate(userId, cId, files, customTargetDir); } @Override - public Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file, UserContext userContext) { + public Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file, UserContext userContext, String customTargetDir) { log.info("[Domain] 上传文件, userId={}, cId={}, filePath={}", userId, cId, filePath); - return computerFileClient.uploadFile(userId, cId, filePath, file); + return computerFileClient.uploadFile(userId, cId, filePath, file, customTargetDir); } @Override - public Map uploadFiles(Long userId, Long cId, List filePaths, List files, UserContext userContext) { + public Map uploadFiles(Long userId, Long cId, List filePaths, List files, UserContext userContext, String customTargetDir) { log.info("[Domain] 批量上传文件, userId={}, cId={}, fileCount={}", userId, cId, files != null ? files.size() : 0); - return computerFileClient.uploadFiles(userId, cId, filePaths, files); + return computerFileClient.uploadFiles(userId, cId, filePaths, files, customTargetDir); } @Override - public Flux downloadAllFiles(Long userId, Long cId, String logId, UserContext userContext) { + public Map importProject(Long userId, Long cId, MultipartFile file, UserContext userContext, String customTargetDir) { + log.info("[Domain] 导入项目, userId={}, cId={}, fileName={}", userId, cId, file != null ? file.getOriginalFilename() : null); + return computerFileClient.importProject(userId, cId, file, customTargetDir); + } + + @Override + public Flux downloadAllFiles(Long userId, Long cId, String logId, UserContext userContext, String customTargetDir) { log.info("[Domain] 下载全部文件, logId={}, userId={}, cId={}", logId, userId, cId); - return computerFileClient.downloadAllFiles(userId, cId, logId) + return computerFileClient.downloadAllFiles(userId, cId, logId, customTargetDir) .doOnError(WebClientResponseException.NotFound.class, e -> { handleDownloadFileNotFound(logId, userId, cId, e); }) @@ -147,6 +153,12 @@ public class ComputerFileDomainServiceImpl implements IComputerFileDomainService }); } + @Override + public Map getLogs(Long userId, Long cId, int tailLines, UserContext userContext) { + log.info("[Domain] 获取沙盒日志, userId={}, cId={}, tailLines={}", userId, cId, tailLines); + return computerFileClient.getLogs(userId, cId, tailLines); + } + /** * 处理下载文件未找到错误(404) */ diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ConversationDomainServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ConversationDomainServiceImpl.java index 2866c394..c128f8ea 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ConversationDomainServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ConversationDomainServiceImpl.java @@ -116,7 +116,7 @@ public class ConversationDomainServiceImpl implements ConversationDomainService queryWrapper.eq(Conversation::getUserId, userId); queryWrapper.eq(Conversation::getDevMode, 0); queryWrapper.eq(Conversation::getTopicUpdated, 1); - queryWrapper.eq(Conversation::getType, Conversation.ConversationType.Chat); + queryWrapper.and(wrapper -> wrapper.eq(Conversation::getType, Conversation.ConversationType.Chat).or().in(Conversation::getDevTargetType, List.of("Agent", "PageApp"))); if (StringUtils.isNotBlank(topic)) { queryWrapper.like(Conversation::getTopic, topic); } @@ -129,6 +129,7 @@ public class ConversationDomainServiceImpl implements ConversationDomainService } queryWrapper.orderByDesc(Conversation::getModified); queryWrapper.last("LIMIT " + size); + queryWrapper.select(Conversation.class, info -> !"variables".equals(info.getColumn())); return conversationRepository.list(queryWrapper); } @@ -143,6 +144,7 @@ public class ConversationDomainServiceImpl implements ConversationDomainService queryWrapper.eq(Conversation::getType, Conversation.ConversationType.TASK); queryWrapper.orderByDesc(Conversation::getId); queryWrapper.last("LIMIT " + size); + queryWrapper.select(Conversation.class, info -> !"variables".equals(info.getColumn())); return conversationRepository.list(queryWrapper); } @@ -152,6 +154,7 @@ public class ConversationDomainServiceImpl implements ConversationDomainService queryWrapper.eq(Conversation::getSandboxServerId, sandboxServerId); //最近25小时的会话 queryWrapper.ge(Conversation::getCreated, DateUtils.addHours(new Date(), -25)); + queryWrapper.select(Conversation.class, info -> !"variables".equals(info.getColumn())); return conversationRepository.list(queryWrapper); } @@ -257,6 +260,15 @@ public class ConversationDomainServiceImpl implements ConversationDomainService return conversationMessageRepository.list(queryWrapper); } + @Override + public List queryConversationMessageList(Long conversationId, Long minId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ConversationMessage::getConversationId, conversationId); + queryWrapper.orderByDesc(ConversationMessage::getId); + queryWrapper.gt(ConversationMessage::getId, minId); + return conversationMessageRepository.list(queryWrapper); + } + @Override public List queryLatestSandboxConversationList(List exceptSandboxIds) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/PublishDomainServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/PublishDomainServiceImpl.java index fe81838b..d51442a5 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/PublishDomainServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/PublishDomainServiceImpl.java @@ -3,6 +3,7 @@ package com.xspaceagi.agent.core.domain.service.impl; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.xspaceagi.agent.core.adapter.dto.PublishApplyQueryDto; @@ -14,8 +15,10 @@ import com.xspaceagi.agent.core.adapter.repository.PublishedStatisticsRepository import com.xspaceagi.agent.core.adapter.repository.entity.PublishApply; import com.xspaceagi.agent.core.adapter.repository.entity.Published; import com.xspaceagi.agent.core.adapter.repository.entity.PublishedStatistics; -import com.xspaceagi.agent.core.spec.enums.UsageScenarioEnum; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; import com.xspaceagi.agent.core.domain.service.PublishDomainService; +import com.xspaceagi.agent.core.domain.service.ResourceGroupRelationDomainService; +import com.xspaceagi.agent.core.spec.enums.UsageScenarioEnum; import com.xspaceagi.system.application.dto.TenantConfigDto; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.PageQueryVo; @@ -43,6 +46,9 @@ public class PublishDomainServiceImpl implements PublishDomainService { @Resource private PublishedStatisticsRepository publishedStatisticsRepository; + @Resource + private ResourceGroupRelationDomainService resourceGroupRelationDomainService; + @Override public void deleteByTargetId(Published.TargetType targetType, Long targetId) { Assert.notNull(targetType, "targetType不能为空"); @@ -69,8 +75,25 @@ public class PublishDomainServiceImpl implements PublishDomainService { queryWrapper.eq(Published::getTargetId, publishApply.getTargetId()); queryWrapper.eq(Published::getScope, publishApply.getScope()); publishedRepository.remove(queryWrapper); - publishedList.forEach(published -> publishedRepository.save(published)); + // 分组内其他工具时间更新 + if (!CollectionUtils.isEmpty(publishedList)) { + Published published = publishedList.get(0); + if (published.getGroupId() != null) { + List resourceGroupRelations = resourceGroupRelationDomainService.listByGroupId(published.getGroupId()); + resourceGroupRelations.forEach(resourceGroupRelation -> { + LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>(); + queryWrapper1.eq(Published::getTargetType, resourceGroupRelation.getTargetType()); + queryWrapper1.eq(Published::getTargetId, resourceGroupRelation.getTargetId()); + publishedRepository.update(Published.builder() + .groupId(published.getGroupId()) + .modified(published.getModified()) + .build(), queryWrapper1); + }); + } + } + + PublishApply publishApplyUpdate = new PublishApply(); publishApplyUpdate.setId(publishApply.getId()); publishApplyUpdate.setPublishStatus(Published.PublishStatus.Published); @@ -81,6 +104,23 @@ public class PublishDomainServiceImpl implements PublishDomainService { publishedRepository.save(published); } + @Override + public void updatePublishedGroupInfo(Published.TargetType targetType, Long targetId, Long groupId) { + if (groupId != null) { + boolean update = publishedRepository.update(Published.builder().groupId(groupId).build(), new QueryWrapper<>(Published.builder().targetType(targetType).targetId(targetId).build())); + if (update) { + //更新相同分组的时间 + publishedRepository.update(Published.builder().modified(new Date()).build(), new QueryWrapper<>(Published.builder().groupId(groupId).build())); + } + } else { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("target_type", targetType); + updateWrapper.eq("target_id", targetId); + updateWrapper.set("group_id", null); + publishedRepository.update(updateWrapper); + } + } + @Override public void rejectPublish(Long applyId) { PublishApply publishApply = queryPublishApplyById(applyId); @@ -150,7 +190,7 @@ public class PublishDomainServiceImpl implements PublishDomainService { queryWrapper.eq(Published::getCategory, publishedQueryDto.getCategory()); } if (StringUtils.isNotBlank(publishedQueryDto.getKw())) { - queryWrapper.like(Published::getName, publishedQueryDto.getKw()); + queryWrapper.apply("MATCH(name, description) AGAINST({0} IN BOOLEAN MODE)", publishedQueryDto.getKw()); } if (publishedQueryDto.getTargetSubType() != null) { if (Published.TargetSubType.ChatBot == publishedQueryDto.getTargetSubType()) { @@ -172,6 +212,9 @@ public class PublishDomainServiceImpl implements PublishDomainService { if (!CollectionUtils.isEmpty(publishedQueryDto.getTargetIds())) { queryWrapper.in(Published::getTargetId, publishedQueryDto.getTargetIds()); } + if (!CollectionUtils.isEmpty(publishedQueryDto.getExcludeIds())) { + queryWrapper.notIn(Published::getTargetId, publishedQueryDto.getExcludeIds()); + } // 技能扩展字段筛选(ext JSON) if (publishedQueryDto.getTargetType() == Published.TargetType.Skill) { Boolean supportTaskAgent = extractUsageScenarioFlag(publishedQueryDto.getUsageScenarios(), UsageScenarioEnum.TaskAgent); @@ -187,6 +230,16 @@ public class PublishDomainServiceImpl implements PublishDomainService { ); } } + if (publishedQueryDto.getTargetType() == Published.TargetType.Workflow) { + queryWrapper.apply("(JSON_UNQUOTE(JSON_EXTRACT(ext, '$.workflowType')) = 'Workflow' OR ext IS NULL)"); + } + if (!CollectionUtils.isEmpty(publishedQueryDto.getAgentTypes())) { + List types = publishedQueryDto.getAgentTypes(); + String placeholders = types.stream() + .map(t -> "'" + t.replace("'", "''") + "'") + .collect(Collectors.joining(",")); + queryWrapper.apply("(JSON_UNQUOTE(JSON_EXTRACT(ext, '$.agentType')) IN (" + placeholders + ") OR ext IS NULL)");//NULL兼容之前的数据 + } List recommendAgentList = new ArrayList<>(); if (publishedQueryDto.getTargetType() == Published.TargetType.Agent && publishedQueryDto.getShowRecommend() != null && publishedQueryDto.getShowRecommend() @@ -198,12 +251,12 @@ public class PublishDomainServiceImpl implements PublishDomainService { excludeAgentIds.add(tenantConfigDto.getDefaultAgentId()); } //未选择分类时排除推荐的智能体,后续会统一展示在最前面 - if (StringUtils.isBlank(publishedQueryDto.getCategory()) && !CollectionUtils.isEmpty(tenantConfigDto.getRecommendAgentIds())) { - excludeAgentIds.addAll(tenantConfigDto.getRecommendAgentIds()); + if (StringUtils.isBlank(publishedQueryDto.getCategory()) && !CollectionUtils.isEmpty(publishedQueryDto.getRecommendIds())) { + excludeAgentIds.addAll(publishedQueryDto.getRecommendIds()); //展示在最前面 if (publishedQueryDto.obtainPage() == 1) { //查询推荐智能体 - recommendAgentList = queryPublishedList(Published.TargetType.Agent, tenantConfigDto.getRecommendAgentIds()); + recommendAgentList = queryPublishedList(Published.TargetType.Agent, publishedQueryDto.getRecommendIds()); if (StringUtils.isNotBlank(publishedQueryDto.getKw())) { recommendAgentList.removeIf(recommendAgent -> !recommendAgent.getName().contains(publishedQueryDto.getKw())); } @@ -217,7 +270,7 @@ public class PublishDomainServiceImpl implements PublishDomainService { } //根据tenantConfigDto.getRecommendAgentIds()从前到后的顺序对recommendAgentList排序 recommendAgentList.sort(Comparator.comparing(agent -> { - int index = tenantConfigDto.getRecommendAgentIds().indexOf(agent.getTargetId()); + int index = publishedQueryDto.getRecommendIds().indexOf(agent.getTargetId()); return index == -1 ? Integer.MAX_VALUE : index; })); @@ -227,15 +280,22 @@ public class PublishDomainServiceImpl implements PublishDomainService { queryWrapper.notIn(Published::getTargetId, excludeAgentIds); } } - Integer page = publishedQueryDto.obtainPage(); + Integer size = publishedQueryDto.obtainPageSize(); //查询总条数 - var dataTotalCount = publishedRepository.count(queryWrapper); - + var dataTotalCount = 0L; + Integer page = publishedQueryDto.obtainPage(); + if (publishedQueryDto.getLastTimestamp() == null) { + dataTotalCount = publishedRepository.count(queryWrapper); + queryWrapper.last("limit " + (page - 1) * size + "," + size); + } else { + queryWrapper.le(Published::getModified, new Date(publishedQueryDto.getLastTimestamp())); + queryWrapper.last("limit " + size); + } // 不查config字段 - queryWrapper.select(Published.class, info -> !"config".equals(info.getColumn())); - - queryWrapper.last("limit " + (page - 1) * size + "," + size); + if (!publishedQueryDto.isReturnConfig()) { + queryWrapper.select(Published.class, info -> !"config".equals(info.getColumn())); + } queryWrapper.orderByDesc(Published::getModified); //查询数据列表 var dataList = publishedRepository.list(queryWrapper); @@ -249,6 +309,12 @@ public class PublishDomainServiceImpl implements PublishDomainService { targetIds.add(published.getTargetId()); return true; }).collect(Collectors.toList()); + if (publishedQueryDto.getTargetIds() != null && !publishedQueryDto.getTargetIds().isEmpty()) { + dataList.sort(Comparator.comparing(published -> { + int index = publishedQueryDto.getTargetIds().indexOf(published.getTargetId()); + return index == -1 ? Integer.MAX_VALUE : index; + })); + } dataTotalCount += recommendAgentList.size(); SuperPage iPage = new SuperPage(page, size, dataTotalCount, dataList); return iPage; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ResourceGroupDomainServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ResourceGroupDomainServiceImpl.java new file mode 100644 index 00000000..cdf70ad3 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ResourceGroupDomainServiceImpl.java @@ -0,0 +1,43 @@ +package com.xspaceagi.agent.core.domain.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xspaceagi.agent.core.adapter.repository.ResourceGroupRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroup; +import com.xspaceagi.agent.core.domain.service.ResourceGroupDomainService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ResourceGroupDomainServiceImpl implements ResourceGroupDomainService { + + @Resource + private ResourceGroupRepository resourceGroupRepository; + + @Override + public Long add(ResourceGroup resourceGroup) { + resourceGroupRepository.save(resourceGroup); + return resourceGroup.getId(); + } + + @Override + public void delete(Long id) { + resourceGroupRepository.removeById(id); + } + + @Override + public void update(ResourceGroup resourceGroup) { + resourceGroupRepository.updateById(resourceGroup); + } + + @Override + public ResourceGroup queryById(Long id) { + return resourceGroupRepository.getById(id); + } + + @Override + public List list(LambdaQueryWrapper queryWrapper) { + return resourceGroupRepository.list(queryWrapper); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ResourceGroupRelationDomainServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ResourceGroupRelationDomainServiceImpl.java new file mode 100644 index 00000000..348d54d8 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/ResourceGroupRelationDomainServiceImpl.java @@ -0,0 +1,68 @@ +package com.xspaceagi.agent.core.domain.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xspaceagi.agent.core.adapter.repository.ResourceGroupRelationRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; +import com.xspaceagi.agent.core.domain.service.ResourceGroupRelationDomainService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ResourceGroupRelationDomainServiceImpl implements ResourceGroupRelationDomainService { + + @Resource + private ResourceGroupRelationRepository resourceGroupRelationRepository; + + @Override + public void add(ResourceGroupRelation relation) { + resourceGroupRelationRepository.save(relation); + } + + @Override + public void delete(Long id) { + resourceGroupRelationRepository.removeById(id); + } + + @Override + public void deleteByGroupIdAndTarget(Long groupId, String targetType, Long targetId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper + .eq(ResourceGroupRelation::getGroupId, groupId) + .eq(ResourceGroupRelation::getTargetType, targetType) + .eq(ResourceGroupRelation::getTargetId, targetId); + resourceGroupRelationRepository.remove(queryWrapper); + } + + @Override + public List listByGroupId(Long groupId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ResourceGroupRelation::getGroupId, groupId); + return resourceGroupRelationRepository.list(queryWrapper); + } + + @Override + public List list(LambdaQueryWrapper queryWrapper) { + return resourceGroupRelationRepository.list(queryWrapper); + } + + @Override + public int countByGroupId(Long groupId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ResourceGroupRelation::getGroupId, groupId); + return (int) resourceGroupRelationRepository.count(queryWrapper); + } + + @Override + public ResourceGroupRelation queryOne(LambdaQueryWrapper queryWrapper) { + return resourceGroupRelationRepository.getOne(queryWrapper, false); + } + + @Override + public List listByGroupIds(List groupIds) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(ResourceGroupRelation::getGroupId, groupIds); + return resourceGroupRelationRepository.list(queryWrapper); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/WorkflowDomainServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/WorkflowDomainServiceImpl.java index f9547b8d..e3186249 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/WorkflowDomainServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-domain/src/main/java/com/xspaceagi/agent/core/domain/service/impl/WorkflowDomainServiceImpl.java @@ -134,7 +134,7 @@ public class WorkflowDomainServiceImpl implements WorkflowDomainService { @Override public List queryListBySpaceId(Long spaceId) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(WorkflowConfig.builder().spaceId(spaceId).build()); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(WorkflowConfig.builder().spaceId(spaceId).type(WorkflowConfigDto.Type.Workflow.name()).build()); return workflowConfigRepository.list(queryWrapper); } @@ -165,6 +165,7 @@ public class WorkflowDomainServiceImpl implements WorkflowDomainService { WorkflowConfig newWorkflowConfig = WorkflowConfig.builder() .spaceId(targetSpaceId) .creatorId(userId) + .type(workflowConfig.getType()) .name(workflowConfig.getName()) .description(workflowConfig.getDescription()) .icon(workflowConfig.getIcon()) diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/pom.xml b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/pom.xml index 003c515f..2cbc79aa 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/pom.xml +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/pom.xml @@ -94,5 +94,12 @@ spring-webflux + + io.projectreactor + reactor-test + 3.7.0 + test + + diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/ArgExtractUtil.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/ArgExtractUtil.java index 4a0b932e..236845d5 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/ArgExtractUtil.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/ArgExtractUtil.java @@ -15,6 +15,7 @@ import java.util.stream.Collectors; @Slf4j public class ArgExtractUtil { + @SuppressWarnings("unchecked") public static Object extraBindValue(Object currentValue, String bindKey) { String[] keys = bindKey.split("\\."); for (String key : keys) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/AgentContext.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/AgentContext.java index b4a53a66..bd22342d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/AgentContext.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/AgentContext.java @@ -11,8 +11,11 @@ import com.xspaceagi.system.application.dto.TenantConfigDto; import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.sdk.common.TraceContext; import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto; +import com.xspaceagi.system.spec.utils.I18nUtil; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.ai.chat.messages.Message; import java.io.Serializable; @@ -92,6 +95,9 @@ public class AgentContext implements Serializable { @Schema(description = "是否过滤长期记忆中的敏感信息") private boolean filterSensitive; + @Schema(description = "智能体对话模式,yolo : 默认模式,所有命令自动放行;ask 命令需要用户审批") + private String agentMode; + private boolean debug; private boolean defaultModelChanged; private Function agentContextFunction; @@ -102,6 +108,8 @@ public class AgentContext implements Serializable { private AtomicBoolean ifInterrupted = new AtomicBoolean(false); private AtomicBoolean finished = new AtomicBoolean(false); + private boolean useSate; + private Map sessionState; public Map getVariableParams() { if (variableParams == null) { @@ -147,4 +155,66 @@ public class AgentContext implements Serializable { public boolean isInterrupted() { return ifInterrupted != null && ifInterrupted.get(); } + + public String getContextMessagesMd() { + if (contextMessages == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (Message message : contextMessages) { + if (message.getText() != null) { + sb.append(message.getMessageType()).append(":\n"); + sb.append(message.getText().replaceAll("[\\s\\S]*?", "")).append("\n\n"); + } + } + return sb.toString(); + } + + public String getContextMd() { + StringBuilder stringBuilder = new StringBuilder(); + if (StringUtils.isNotBlank(getLongMemory())) { + stringBuilder.append("## User LongMemory:\n").append(getLongMemory()).append("\n\n"); + } + if (StringUtils.isNotBlank(getConversation().getSummary())) { + stringBuilder.append("## Summary:\n").append(getConversation().getSummary()).append("\n\n"); + } + String contextMessagesMd = getContextMessagesMd(); + if (StringUtils.isNotBlank(contextMessagesMd)) { + stringBuilder.append("## Chat Context:\n").append(contextMessagesMd).append("\n\n"); + } + if (getHeaders() != null && !getHeaders().isEmpty()) { + getHeaders().remove("Authorization"); + getHeaders().remove("authorization"); + getHeaders().remove("Cookie"); + getHeaders().remove("cookie"); + stringBuilder.append("## Request Headers:\n"); + getHeaders().forEach((k, v) -> { + stringBuilder.append(k).append(": ").append(v).append("\n"); + }); + stringBuilder.append("\n"); + } + if (CollectionUtils.isNotEmpty(getAttachments())) { + stringBuilder.append("## Attachments:\n"); + getAttachments().forEach(attachment -> { + stringBuilder.append(attachment.getFileName()).append(": ").append(attachment.getFileUrl()).append("\n"); + }); + stringBuilder.append("\n"); + } + stringBuilder.append("## User Prompt:\n"); + if (StringUtils.isNotBlank(getMessage())) { + stringBuilder.append(getMessage()); + } else if (CollectionUtils.isNotEmpty(getAttachments())) { + stringBuilder.append(I18nUtil.systemMessage("Backend.Agent.Message.AnalyzeAttachments")); + } + stringBuilder.append("\n\n"); + stringBuilder.append("## Variables (Current value of the variable; it can be reused if there are no significant changes or insufficient information based on the context):\n"); + getVariableParams().forEach((k, v) -> { + if (k.equals(GlobalVariableEnum.CHAT_CONTEXT.name()) || k.equals(GlobalVariableEnum.AGENT_USER_MSG.name())) { + return; + } + stringBuilder.append(k).append(": ").append(v).append("\n"); + }); + stringBuilder.append("\n"); + return stringBuilder.toString(); + } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/AgentExecutor.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/AgentExecutor.java index 9a0ebe4c..cd1562cd 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/AgentExecutor.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/AgentExecutor.java @@ -1,6 +1,7 @@ package com.xspaceagi.agent.core.infra.component.agent; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; import com.google.common.base.Joiner; import com.xspaceagi.agent.core.adapter.application.ConversationApplicationService; import com.xspaceagi.agent.core.adapter.dto.AgentOutputDto; @@ -44,6 +45,7 @@ import com.xspaceagi.system.spec.exception.AgentInterruptException; import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import com.xspaceagi.system.spec.jackson.JsonSerializeUtil; +import com.xspaceagi.system.spec.tenant.thread.TenantFunctions; import com.xspaceagi.system.spec.utils.I18nUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -71,6 +73,7 @@ import java.util.stream.Collectors; import static com.xspaceagi.agent.core.spec.constant.Prompts.EXTRACT_PARAM_PROMPT; import static com.xspaceagi.agent.core.spec.constant.Prompts.SUGGEST_PROMPT; +import static com.xspaceagi.agent.core.spec.enums.GlobalVariableEnum.CHAT_CONTEXT; @Slf4j @Component @@ -138,6 +141,9 @@ public class AgentExecutor extends BaseComponent { } agentContext.getAgentExecuteResult().setStartTime(System.currentTimeMillis()); + if (agentContext.getAgentConfig().getModelComponentConfig().getTargetConfig() == null || !(agentContext.getAgentConfig().getModelComponentConfig().getTargetConfig() instanceof ModelConfigDto)) { + return Flux.error(new BizException("Incomplete agent model configuration. Please check if the associated model has been deleted.")); + } ModelBindConfigDto modelBindConfigDto = (ModelBindConfigDto) agentContext.getAgentConfig().getModelComponentConfig().getBindConfig(); int contextRounds = modelBindConfigDto.getContextRounds() == null ? 0 : modelBindConfigDto.getContextRounds(); if ("TaskAgent".equals(agentContext.getAgentConfig().getType())) { @@ -145,7 +151,7 @@ public class AgentExecutor extends BaseComponent { } List contextMessages = new ArrayList<>(); if (contextRounds > 0) { - contextMessages.addAll(conversationApplicationService.getRoundMessages(agentContext.getConversationId(), contextRounds * 2)); + contextMessages.addAll(getRoundMessages(agentContext, contextRounds * 2)); } agentContext.setContextMessages(contextMessages); @@ -166,25 +172,30 @@ public class AgentExecutor extends BaseComponent { Sinks.Many sink = Sinks.many().multicast().onBackpressureBuffer(); //优先执行变量组件 AtomicReference nextDisposable = new AtomicReference<>(); - Disposable disposable = variableSet(agentContext, sink).doOnError(sink::tryEmitError).doOnSuccess((result) -> submit(() -> { - try { - if (agentContext.isInterrupted()) { - sink.tryEmitComplete(); - return; - } - nextDisposable.set(doNext(agentContext, sink)); - } catch (Exception e) { - log.error("Execution failed", e); - sink.tryEmitError(e); - } - })).subscribe(); - - return sink.asFlux().publishOn(Schedulers.boundedElastic()).doOnCancel(() -> { - disposable.dispose(); - if (nextDisposable.get() != null) { - nextDisposable.get().dispose(); - } - }); + AtomicReference disposableAtomicReference = new AtomicReference<>(); + return sink.asFlux().doOnSubscribe(subscriber -> { + Disposable disposable = variableSet(agentContext, sink).doOnError(sink::tryEmitError).doOnSuccess((result) -> submit(() -> { + try { + if (agentContext.isInterrupted()) { + sink.tryEmitComplete(); + return; + } + nextDisposable.set(doNext(agentContext, sink)); + } catch (Exception e) { + log.error("Execution failed", e); + sink.tryEmitError(e); + } + })).subscribe(); + disposableAtomicReference.set(disposable); + }) + .publishOn(Schedulers.boundedElastic()).doOnCancel(() -> { + if (disposableAtomicReference.get() != null) { + disposableAtomicReference.get().dispose(); + } + if (nextDisposable.get() != null) { + nextDisposable.get().dispose(); + } + }); } private Disposable doNext(AgentContext agentContext, Sinks.Many sink) { @@ -212,8 +223,7 @@ public class AgentExecutor extends BaseComponent { .id(UUID.randomUUID().toString().replace("-", "")) .time(new Date()) .build(); - conversationApplicationService.addRoundMessage(agentContext.getConversationId(), userMessageDto); - + addRoundMessage(agentContext, userMessageDto); if (!directOutputResults.isEmpty()) { ChatMessageDto chatMessageDto = ChatMessageDto.builder() .role(ChatMessageDto.Role.ASSISTANT) @@ -253,7 +263,7 @@ public class AgentExecutor extends BaseComponent { String systemPrompt = buildSystemPrompt(agentContext, pageComponentConfigs); agentContext.getAgentConfig().setSystemPrompt(systemPrompt); - ModelContext modelContext = buildModelContext(agentContext, systemPrompt, agentContext.getMessage(), agentContext.getConversationId(), true, null, null); + ModelContext modelContext = buildModelContext(agentContext, systemPrompt, agentContext.getMessage(), agentContext.getConversationId(), null, null); if (CollectionUtils.isNotEmpty(autoCallTools)) { List messages = new ArrayList<>(); messages.add(new AssistantMessage(Joiner.on("\n\n").join(autoCallTools))); @@ -319,6 +329,9 @@ public class AgentExecutor extends BaseComponent { StringBuilder reasoningText = new StringBuilder(); return callMessageFlux.doOnError(e -> chatError(agentContext, msgSb.toString(), reasoningText.toString(), sink, componentExecutedList, e)) .doOnComplete(() -> chatComplete(agentContext, sink, msgSb.toString(), reasoningText.toString(), componentExecutedList)) + .doOnCancel(() -> { + chatComplete(agentContext, sink, msgSb.toString(), reasoningText.toString(), componentExecutedList); + }) .subscribe((res) -> { if (agentContext.isInterrupted()) { return; @@ -328,7 +341,10 @@ public class AgentExecutor extends BaseComponent { } else { msgSb.append(res.getText()); } - sink.tryEmitNext(buildOutputMessage(agentContext, res)); + Sinks.EmitResult emitResult = sink.tryEmitNext(buildOutputMessage(agentContext, res)); + if (emitResult.isFailure()) { + log.error("Agent output failed, cid {}, error {}", agentContext.getConversationId(), emitResult); + } }, throwable -> { chatError(agentContext, msgSb.toString(), reasoningText.toString(), sink, componentExecutedList, throwable); if (throwable instanceof AgentInterruptException) { @@ -338,7 +354,7 @@ public class AgentExecutor extends BaseComponent { }); } - private static String buildProcessingText(ComponentExecutingDto componentExecutingDto) { + public static String buildProcessingText(ComponentExecutingDto componentExecutingDto) { return String.format( "\n
\n\n", escapeHtmlAttr(componentExecutingDto.getResult().getExecuteId()), @@ -378,7 +394,7 @@ public class AgentExecutor extends BaseComponent { .time(new Date()) .componentExecutedList(componentExecutedList.stream().map(componentExecutingDto -> (Object) componentExecutingDto).toList()) .build(); - conversationApplicationService.addRoundMessage(agentContext.getConversationId(), assistantMessage); + addRoundMessage(agentContext, assistantMessage); } else { CallMessage callMessage = new CallMessage(); callMessage.setText("```\n" + e.getMessage() + "\n```"); @@ -427,7 +443,7 @@ public class AgentExecutor extends BaseComponent { .time(new Date()) .finished(true) .build(); - conversationApplicationService.addRoundMessage(agentContext.getConversationId(), assistantMessage); + addRoundMessage(agentContext, assistantMessage); } if (agentContext.getAgentExecuteResult().getSuccess() == null) { agentContext.getAgentExecuteResult().setSuccess(true); @@ -452,6 +468,54 @@ public class AgentExecutor extends BaseComponent { } } + @SuppressWarnings("unchecked") + private void addRoundMessage(AgentContext agentContext, ChatMessageDto userMessageDto) { + if (agentContext.isUseSate()) { + userMessageDto.setComponentExecutedList(null); + Map state = agentContext.getVariableParams(); + String key = buildAgentStateKey(agentContext); + Map agentState = (Map) state.computeIfAbsent(key, k -> new HashMap<>()); + List chatMessages = (List) agentState.computeIfAbsent("roundMessageList", k -> new ArrayList<>()); + chatMessages.add(userMessageDto); + ModelBindConfigDto modelBindConfigDto = (ModelBindConfigDto) agentContext.getAgentConfig().getModelComponentConfig().getBindConfig(); + int contextRounds = modelBindConfigDto.getContextRounds() == null ? 0 : modelBindConfigDto.getContextRounds(); + if ("TaskAgent".equals(agentContext.getAgentConfig().getType())) { + contextRounds = 10;// 用于后续补充上下文记忆 + } + if (contextRounds > 0 && chatMessages.size() > contextRounds) { + chatMessages.subList(0, chatMessages.size() - contextRounds).clear(); + } + } else { + conversationApplicationService.addRoundMessage(agentContext.getConversationId(), userMessageDto); + } + } + + @SuppressWarnings("unchecked") + private List getRoundMessages(AgentContext agentContext, int ct) { + if (agentContext.isUseSate()) { + Map state = agentContext.getVariableParams(); + String key = buildAgentStateKey(agentContext); + Map agentState = (Map) state.computeIfAbsent(key, k -> new HashMap<>()); + List messages = (List) agentState.get("roundMessageList"); + if (messages == null) { + return new ArrayList<>(); + } + List chatMessages = messages.stream().map(message -> { + if (message instanceof ChatMessageDto chatMessageDto) { + return chatMessageDto; + } + return JSONObject.parseObject(JSON.toJSONString(message), ChatMessageDto.class); + }).toList(); + return ChatMessageDto.toMessages(chatMessages); + } else { + return TenantFunctions.callWithIgnoreCheck(() -> conversationApplicationService.getRoundMessages(agentContext.getConversationId(), ct)); + } + } + + private String buildAgentStateKey(AgentContext agentContext) { + return "_agent" + agentContext.getAgentConfig().getId(); + } + private Map extractAutoInvokeComponentRequireParamValues(AgentContext agentContext) { List requireArgs = new ArrayList<>(); List allArgs = new ArrayList<>(); @@ -481,18 +545,7 @@ public class AgentExecutor extends BaseComponent { if (requireArgs.isEmpty()) { return new HashMap<>(); } - //args组装一起 - String userPrompt = buildVarUserPrompt(agentContext); - AgentContext agentContext1 = new AgentContext(); - BeanUtils.copyProperties(agentContext, agentContext1); - agentContext1.setContextMessages(new ArrayList<>(agentContext.getContextMessages())); - ModelContext modelContext = buildModelContext(agentContext1, EXTRACT_PARAM_PROMPT, userPrompt, null, true, OutputTypeEnum.JSON, allArgs); - modelInvoker.invoke(modelContext).blockLast(); - Object res = modelContext.getModelCallResult().getData(); - if (!(res instanceof Map)) { - return new HashMap<>(); - } - return (Map) res; + return extractParam(agentContext, allArgs); } private static void checkAndSetRequireArgs(List argBindConfigs, List requireArgs, List allArgs, Long id) { @@ -510,7 +563,7 @@ public class AgentExecutor extends BaseComponent { }); } - private static String buildUserMessage(AgentContext agentContext) { + public static String buildUserMessage(AgentContext agentContext) { StringBuilder stringBuilder = new StringBuilder(); //长期记忆 if (StringUtils.isNotBlank(agentContext.getLongMemory())) { @@ -520,7 +573,7 @@ public class AgentExecutor extends BaseComponent { if (withUserPrompt) { String userPrompt = agentContext.getAgentConfig().getUserPrompt(); if (StringUtils.isNotBlank(userPrompt)) { - stringBuilder.append("").append(PlaceholderParser.resoleAndReplacePlaceholder(agentContext.getVariableParams(), userPrompt)).append(""); + stringBuilder.append("\n").append(PlaceholderParser.resoleAndReplacePlaceholder(agentContext.getVariableParams(), userPrompt)).append("\n"); } } @@ -937,7 +990,9 @@ public class AgentExecutor extends BaseComponent { componentExecuteResult.setSuccess(true); componentExecuteResult.setData(CollectionUtils.isEmpty(qaVoList) ? "No relevant information found" : qaVoList); StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append("The following are the search results in the knowledge base:\n"); + stringBuilder.append("The following are the search results from the selected knowledge base. ") + .append("Answer must be based only on these search results. ") + .append("If the search results say no relevant information was found, tell the user that the selected knowledge base did not hit relevant content and do not answer from general model knowledge.\n"); if (!CollectionUtils.isEmpty(qaVoList)) { stringBuilder.append(JSON.toJSONString(qaVoList)); } else { @@ -1174,12 +1229,12 @@ public class AgentExecutor extends BaseComponent { if (variableConfigDto != null && !CollectionUtils.isEmpty(variableConfigDto.getVariables())) { //过滤出自定义变量,同时把前端已经传递的变量也过滤掉 List nonSysArgs = variableConfigDto.getVariables().stream().filter(var -> !var.isSystemVariable() - && var.getInputType() != null && var.getInputType() == Arg.InputTypeEnum.AutoRecognition && !agentContext.getVariableParams().containsKey(var.getName())) + && var.getInputType() != null && var.getInputType() == Arg.InputTypeEnum.AutoRecognition) .toList(); userArgs.addAll(nonSysArgs); variableConfigDto.getVariables().forEach((var) -> { if (var.isSystemVariable() && var.getEnable()) { - if (GlobalVariableEnum.CHAT_CONTEXT.name().equals(var.getName())) { + if (CHAT_CONTEXT.name().equals(var.getName())) { ModelBindConfigDto modelBindConfigDto = (ModelBindConfigDto) agentContext.getAgentConfig().getModelComponentConfig().getBindConfig(); if (modelBindConfigDto != null) { List messages = new ArrayList<>(agentContext.getContextMessages()); @@ -1214,8 +1269,15 @@ public class AgentExecutor extends BaseComponent { String userPrompt = buildVarUserPrompt(agentContext); AgentContext agentContext1 = new AgentContext(); BeanUtils.copyProperties(agentContext, agentContext1); - agentContext1.setContextMessages(new ArrayList<>(agentContext.getContextMessages())); - ModelContext modelContext = buildModelContext(agentContext1, EXTRACT_PARAM_PROMPT, userPrompt, null, true, OutputTypeEnum.JSON, userArgs); + agentContext1.setContextMessages(new ArrayList<>()); + ModelContext modelContext = buildModelContext(agentContext1, EXTRACT_PARAM_PROMPT, userPrompt, null, OutputTypeEnum.JSON, userArgs); + ModelConfigDto modelConfig = modelContext.getModelConfig(); + if (modelConfig.getIsReasonModel() != null && modelConfig.getIsReasonModel() == 1) { + ModelConfigDto modelConfigDto = new ModelConfigDto(); + BeanUtils.copyProperties(modelConfig, modelConfigDto); + modelConfigDto.setIsReasonModel(0); + modelContext.setModelConfig(modelConfigDto); + } Disposable d = modelInvoker.invoke(modelContext).doOnError(throwable -> { componentExecuteResult.setEndTime(System.currentTimeMillis()); componentExecuteResult.setSuccess(false); @@ -1259,6 +1321,15 @@ public class AgentExecutor extends BaseComponent { if (res instanceof Map) { Map resMap = (Map) res; resMap.forEach((k, v) -> agentContext.getVariableParams().put(k, v)); + // 通用智能体提前保存识别的变量 + if ("TaskAgent".equals(agentContext.getAgentConfig().getType())) { + try { + conversationApplicationService.updateConversationVariables(Long.parseLong(agentContext.getConversationId()), agentContext.getVariableParams()); + } catch (Exception e) { + // ignore + log.error("Update conversation variables failed", e); + } + } } } catch (Exception e) { log.error("User variable setting failed", e); @@ -1276,41 +1347,38 @@ public class AgentExecutor extends BaseComponent { }); } + private Map extractParam(AgentContext agentContext, List args) { + //args组装一起 + String userPrompt = buildVarUserPrompt(agentContext); + AgentContext agentContext1 = new AgentContext(); + BeanUtils.copyProperties(agentContext, agentContext1); + agentContext1.setContextMessages(new ArrayList<>()); + ModelContext modelContext = buildModelContext(agentContext1, EXTRACT_PARAM_PROMPT, userPrompt, null, OutputTypeEnum.JSON, args); + ModelConfigDto modelConfig = modelContext.getModelConfig(); + if (modelConfig.getIsReasonModel() != null && modelConfig.getIsReasonModel() == 1) { + ModelConfigDto modelConfigDto = new ModelConfigDto(); + BeanUtils.copyProperties(modelConfig, modelConfigDto); + modelConfigDto.setIsReasonModel(0);// 变量提取关闭思考模式 + modelContext.setModelConfig(modelConfigDto); + } + modelInvoker.invoke(modelContext).blockLast(); + Object res = modelContext.getModelCallResult().getData(); + if (!(res instanceof Map)) { + return new HashMap<>(); + } + return (Map) res; + } + private static String buildVarUserPrompt(AgentContext agentContext) { StringBuilder stringBuilder = new StringBuilder(); - if (StringUtils.isNotBlank(agentContext.getLongMemory())) { - stringBuilder.append("\n\n").append(JSON.toJSONString(agentContext.getLongMemory())).append("\n\n"); + if (StringUtils.isNotBlank(agentContext.getAgentConfig().getSystemPrompt())) { + stringBuilder.append("## Target Conversation's System Prompt:\n").append(agentContext.getAgentConfig().getSystemPrompt()).append("\n\n"); } - if (agentContext.getVariableParams().get(GlobalVariableEnum.CHAT_CONTEXT.name()) == null) { - ModelBindConfigDto modelBindConfigDto = (ModelBindConfigDto) agentContext.getAgentConfig().getModelComponentConfig().getBindConfig(); - if (modelBindConfigDto != null) { - List messages = new ArrayList<>(agentContext.getContextMessages()); - messages.add(0, new SystemMessage(agentContext.getAgentConfig().getSystemPrompt() == null ? "You are a helpful assistant." : agentContext.getAgentConfig().getSystemPrompt())); - stringBuilder.append("\n\n").append(JSON.toJSONString(messages)).append("\n\n"); - } - } else { - stringBuilder.append("\n\n").append(JSON.toJSONString(agentContext.getVariableParams().get(GlobalVariableEnum.CHAT_CONTEXT.name()))).append("\n\n"); - } - if (agentContext.getHeaders() != null && !agentContext.getHeaders().isEmpty()) { - agentContext.getHeaders().remove("Authorization"); - agentContext.getHeaders().remove("authorization"); - agentContext.getHeaders().remove("Cookie"); - agentContext.getHeaders().remove("cookie"); - stringBuilder.append("\n\n").append(JSON.toJSONString(agentContext.getHeaders())).append("\n\n"); - } - if (CollectionUtils.isNotEmpty(agentContext.getAttachments())) { - stringBuilder.append("\n").append(JSON.toJSONString(agentContext.getAttachments())).append("\n"); - } - if (StringUtils.isNotBlank(agentContext.getMessage())) { - stringBuilder.append(agentContext.getMessage()); - } else if (CollectionUtils.isNotEmpty(agentContext.getAttachments())) { - stringBuilder.append(I18nUtil.systemMessage("Backend.Agent.Message.AnalyzeAttachments")); - } - return stringBuilder.toString(); + return stringBuilder.append(agentContext.getContextMd()).toString(); } private static ModelContext buildModelContext(AgentContext agentContext, String systemPrompt, String userPrompt, - String conversationId, boolean streamCall, OutputTypeEnum outputType, List outputArgs) { + String conversationId, OutputTypeEnum outputType, List outputArgs) { ModelContext modelContext = new ModelContext(); modelContext.setRequestId(agentContext.getRequestId()); modelContext.setAgentContext(agentContext); @@ -1321,7 +1389,7 @@ public class AgentExecutor extends BaseComponent { modelCallConfigDto.setSystemPrompt(systemPrompt); modelCallConfigDto.setUserPrompt(userPrompt); modelCallConfigDto.setChatRound(0); - modelCallConfigDto.setStreamCall(streamCall); + modelCallConfigDto.setStreamCall(true); modelCallConfigDto.setOutputType(outputType); modelCallConfigDto.setOutputArgs(outputArgs); modelCallConfigDto.setMaxTokens(modelBindConfigDto.getMaxTokens()); @@ -1480,7 +1548,7 @@ public class AgentExecutor extends BaseComponent { arg.setDataType(DataTypeEnum.Array_String); outputArgs.add(arg); ModelContext suggestModelContext = buildModelContext(agentContext, buildSuggestSystemPrompt(agentContext), buildSuggestUserPrompt(agentContext), - null, true, OutputTypeEnum.JSON, outputArgs); + null, OutputTypeEnum.JSON, outputArgs); Disposable disposable = modelInvoker.invoke(suggestModelContext).doOnError((e) -> { log.error("Suggested question invocation failed", e); sink.success(new ArrayList<>()); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClient.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClient.java index f07c4966..8220a5fe 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClient.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/SandboxAgentClient.java @@ -3,11 +3,18 @@ package com.xspaceagi.agent.core.infra.component.agent; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; +import com.xspaceagi.agent.core.adapter.dto.AgentPublishVersionDto; import com.xspaceagi.agent.core.adapter.dto.ChatMessageDto; import com.xspaceagi.agent.core.adapter.dto.ConversationDto; import com.xspaceagi.agent.core.adapter.dto.config.AgentComponentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.bind.McpBindConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.bind.PluginBindConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.bind.WorkflowBindConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; import com.xspaceagi.agent.core.adapter.repository.entity.AgentComponentConfig; +import com.xspaceagi.agent.core.adapter.repository.entity.Conversation; import com.xspaceagi.agent.core.infra.component.agent.dto.AgentRequest; import com.xspaceagi.agent.core.infra.component.model.dto.CallMessage; import com.xspaceagi.agent.core.infra.component.model.dto.ComponentExecuteResult; @@ -35,6 +42,7 @@ import com.xspaceagi.system.application.dto.TenantConfigDto; import com.xspaceagi.system.sdk.common.TraceContext; import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto; import com.xspaceagi.system.spec.cache.SimpleJvmHashCache; +import com.xspaceagi.system.spec.enums.YesOrNoEnum; import com.xspaceagi.system.spec.exception.AgentException; import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.utils.I18nUtil; @@ -107,6 +115,13 @@ public class SandboxAgentClient { } """); + private static final JSONObject askQuestionMcp = JSON.parseObject(""" + { + "command": "npx", + "args": ["-y", "nuwax-ask-question-mcp@latest"] + } + """); + /** * Pattern to extract the data content from SSE data field lines. Matches lines * starting with "data:" and captures the remaining content. @@ -458,24 +473,63 @@ public class SandboxAgentClient { apiInfo.setUrl(frontendModelDto.getBaseUrl()); String sessionId = null; - if (conversation != null && StringUtils.isNotBlank(conversation.getSandboxSessionId())) { + if (agentContext.isUseSate()) { + Map sessionState = agentContext.getSessionState(); + Object o = sessionState.get("sandboxSessionId"); + if (o != null) { + sessionId = o.toString(); + } + } else if (conversation != null && StringUtils.isNotBlank(conversation.getSandboxSessionId()) && !agentContext.isDebug()) { sessionId = conversation.getSandboxSessionId(); } - AgentRequest agentRequest = AgentRequest.builder().user_id(agentContext.getUserId().toString()).project_id(agentContext.getConversationId()).session_id(sessionId).prompt(userPrompt).original_user_prompt(agentContext.getOriginalMessage()).open_long_memory(false).request_id(agentContext.getRequestId()) - .attachments(null).data_source_attachments(null).model_provider(null).system_prompt(systemPrompt).user_prompt(null).model_provider(AgentRequest.ModelProvider.builder().id(modelConfig.getId().toString()).default_model(modelConfig.getModel()).name(modelConfig.getName()).requires_openai_auth(true).base_url(apiInfo.getUrl().replace("SESSION_ID", agentContext.getConversationId()))// 官方代理接口通过这个id做上下文保持在一个后端key + + String projectId = agentContext.getConversationId(); + if (agentContext.isUseSate()) { + projectId = agentContext.getConversationId() + "_" + agentContext.getAgentConfig().getId(); + } + AgentRequest agentRequest = AgentRequest.builder() + .user_id(agentContext.getUserId().toString()) + .project_id(projectId) + .agent_work_dir(agentContext.getConversationId()) + .session_id(sessionId).prompt(userPrompt) + .original_user_prompt(agentContext.getOriginalMessage()) + .open_long_memory(false) + .request_id(agentContext.getRequestId()) + .attachments(null) + .data_source_attachments(null) + .model_provider(null) + .system_prompt(systemPrompt) + .user_prompt(null) + .model_provider(AgentRequest.ModelProvider.builder() + .id(modelConfig.getId().toString()) + .default_model(modelConfig.getModel()) + .name(modelConfig.getName()) + .requires_openai_auth(true) + .base_url(apiInfo.getUrl().replace("SESSION_ID", agentContext.getConversationId()))// 官方代理接口通过这个id做上下文保持在一个后端key .api_key(apiInfo.getKey()) - .api_protocol(modelConfig.getApiProtocol().name().toLowerCase()).build()) - .agent_config(AgentRequest.AgentConfig.builder().agent_server(buildAgentServer(agentContext, sandboxServer, modelConfig)).context_servers(buildContextServers(agentContext)).resource_limits(AgentRequest.ResourceLimits.builder().cpu_limit(sandboxServer.getPerUserCpuCores()).memory_limit((long) (sandboxServer.getPerUserMemoryGB() * 1024 * 1024 * 1024)).swap_limit((long) (sandboxServer.getPerUserMemoryGB() * 1024 * 1024 * 1024) * 2).build()).build()).build(); + .api_protocol(modelConfig.getApiProtocol().name().toLowerCase()) + .build()) + .agent_config(AgentRequest.AgentConfig.builder() + .agent_server(buildAgentServer(agentContext, sandboxServer, modelConfig)) + .context_servers(buildContextServers(agentContext)) + .resource_limits(AgentRequest.ResourceLimits.builder() + .cpu_limit(sandboxServer.getPerUserCpuCores()) + .memory_limit((long) (sandboxServer.getPerUserMemoryGB() * 1024 * 1024 * 1024)) + .swap_limit((long) (sandboxServer.getPerUserMemoryGB() * 1024 * 1024 * 1024) * 2) + .build()) + .build()) + .build(); String requestBody = JSON.toJSONString(agentRequest); - HttpRequest request = HttpRequest.newBuilder().uri(URI.create(sandboxServer.getServerAgentUrl() + "/computer/chat")).header("Content-Type", "application/json") + String apiUrl = sandboxServer.getServerAgentUrl() + "/computer/chat"; + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(rewriteComputerBaseUrl(agentContext.getConversation(), apiUrl))).header("Content-Type", "application/json") .header("x-api-key", sandboxServer.getServerApiKey() == null ? "" : sandboxServer.getServerApiKey()) .timeout(Duration.ofSeconds(180)) .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8)).build(); log.info("Initiate conversation request, agentId {}, conversationId {}, requestBody {}", agentContext.getAgentConfig().getId(), agentContext.getConversationId(), requestBody); try { CompletableFuture> httpResponseCompletableFuture = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()); - httpResponseCompletableFuture.orTimeout(180, TimeUnit.SECONDS).thenAccept(httpResponse -> { + httpResponseCompletableFuture.thenAccept(httpResponse -> { int status = httpResponse.statusCode(); if (status != 200 && status != 201 && status != 202 && status != 206) { sink.error(new RuntimeException("Failed to connect to SSE stream. Unexpected status code: " + status)); @@ -512,15 +566,19 @@ public class SandboxAgentClient { sink.error(new AgentException("0001", "session_id is null")); return; } - if (conversation != null && agentContext.getSandboxSessionCreatedConsumer() != null) { + + if (agentContext.isUseSate()) { + agentContext.getSessionState().put("sandboxSessionId", sessionId0); + } else if (conversation != null && agentContext.getSandboxSessionCreatedConsumer() != null) { conversation.setSandboxSessionId(sessionId0); conversation.setSandboxServerId(sandboxServer.getServerId()); agentContext.getSandboxSessionCreatedConsumer().accept(conversation); } Map componentExecutingDtoMap = new LinkedHashMap<>(); Set subEventTypeEnumSet = new HashSet<>(); - AtomicBoolean compeleted = new AtomicBoolean(false); - SseSubscription subscription = subscribe(sandboxServer, sandboxServer.getServerAgentUrl() + "/computer/progress/" + sessionId0, new SseEventHandler() { + AtomicBoolean completed = new AtomicBoolean(false); + String apiUrl0 = sandboxServer.getServerAgentUrl() + "/computer/progress/" + sessionId0; + SseSubscription subscription = subscribe(sandboxServer, rewriteComputerBaseUrl(agentContext.getConversation(), apiUrl0), new SseEventHandler() { @Override public void onEvent(SseEvent event) { log.debug("sse event, cid {}, type: {}, data: {}", agentContext.getConversationId(), event.type, event.data); @@ -540,28 +598,27 @@ public class SandboxAgentClient { if (event.type.equals("end_turn") || "end_turn".equals(jsonObject.getString("subType"))) { checkUnfinishedToolCall(agentContext, componentExecutingDtoMap); checkAndCompleteFinalResultFile(agentContext, componentExecutingDtoMap, finalText, sink); - if (sseSubscriptionAtomicReference.get() != null) { - sseSubscriptionAtomicReference.get().cancel(); - } - String reason = "EndTurn"; if (data != null && data.getString("reason") != null) { reason = data.getString("reason"); } sink.next(buildChatFinishedMessage(agentContext, reason)); - compeleted.set(true); + completed.set(true); sink.complete(); + if (sseSubscriptionAtomicReference.get() != null) { + sseSubscriptionAtomicReference.get().cancel(); + } return; } if ("sessionPromptEnd".equals(jsonObject.getString("messageType"))) { checkUnfinishedToolCall(agentContext, componentExecutingDtoMap); checkAndCompleteFinalResultFile(agentContext, componentExecutingDtoMap, finalText, sink); - if (sseSubscriptionAtomicReference.get() != null) { - sseSubscriptionAtomicReference.get().cancel(); - } if (data == null) { - compeleted.set(true); + completed.set(true); sink.complete(); + if (sseSubscriptionAtomicReference.get() != null) { + sseSubscriptionAtomicReference.get().cancel(); + } return; } if ("error".equals(event.type) || "error".equals(jsonObject.getString("sub_type")) || "error".equals(jsonObject.getString("subType"))) { @@ -593,7 +650,9 @@ public class SandboxAgentClient { callMessage.setText("\n```\n" + I18nUtil.systemMessage(agentContext.getUser().getLangMap(), "Backend.Sandbox.Error.ModelExecutionError") + "\n```"); agentStop(agentContext.getConversationId()); // 异常会话无法恢复时清除之前关联的会话 - if (conversation != null && agentContext.getSandboxSessionCreatedConsumer() != null) { + if (agentContext.isUseSate()) { + agentContext.getSessionState().remove("sandboxSessionId"); + } else if (conversation != null && agentContext.getSandboxSessionCreatedConsumer() != null) { conversation.setSandboxSessionId(""); conversation.setSandboxServerId(sandboxServer.getServerId()); agentContext.getSandboxSessionCreatedConsumer().accept(conversation); @@ -602,10 +661,16 @@ public class SandboxAgentClient { sink.next(callMessage); sink.error(new AgentException("ERROR", message)); } else if ("cancelled".equals(event.type)) { - sink.error(new AgentException("0001", data.containsKey("error_message") ? data.getString("error_message") : "cancelled")); - } else { - compeleted.set(true); + //sink.error(new AgentException("0001", data.containsKey("error_message") ? data.getString("error_message") : "cancelled")); + agentContext.setInterrupted(true); + completed.set(true); sink.complete(); + } else { + completed.set(true); + sink.complete(); + } + if (sseSubscriptionAtomicReference.get() != null) { + sseSubscriptionAtomicReference.get().cancel(); } return; } @@ -643,17 +708,37 @@ public class SandboxAgentClient { Consumer componentExecutingConsumer = agentContext.getComponentExecutingConsumer(); if (componentExecutingConsumer != null && data.getString("toolCallId") != null) { ComponentExecutingDto componentExecutingDto = buildComponentExecutingDto(data, componentExecutingDtoMap, agentContext.getUser().getLangMap()); - componentExecutingConsumer.accept(componentExecutingDto); + if (data.getString("title") != null && data.getString("title").contains("nuwax_ask_question")) { + Object input = data.get("rawInput"); + componentExecutingDto = buildExecutingEvent(I18nUtil.systemMessage(agentContext.getUser().getLangMap(), "Backend.Sandbox.Event.AskQuestion"), ComponentExecutingDto.SubEventTypeEnum.ASK_QUESTION, input, data.getString("toolCallId")); + componentExecutingConsumer.accept(componentExecutingDto); + } else { + componentExecutingConsumer.accept(componentExecutingDto); + } // 添加桌面打开事件 if (agentContext.getAgentConfig().getHideDesktop() == null || agentContext.getAgentConfig().getHideDesktop() != 1) { if (data.getString("title") != null && (data.getString("title").contains("mcp__chrome") || data.getString("title").contains("chrome-tools") || data.getString("title").contains("navigate_page")) && !subEventTypeEnumSet.contains(ComponentExecutingDto.SubEventTypeEnum.OPEN_DESKTOP)) { subEventTypeEnumSet.add(ComponentExecutingDto.SubEventTypeEnum.OPEN_DESKTOP); - componentExecutingDto = buildExecutingEvent(I18nUtil.systemMessage(agentContext.getUser().getLangMap(), "Backend.Sandbox.Event.OpenDesktop"), ComponentExecutingDto.SubEventTypeEnum.OPEN_DESKTOP, null); + componentExecutingDto = buildExecutingEvent(I18nUtil.systemMessage(agentContext.getUser().getLangMap(), "Backend.Sandbox.Event.OpenDesktop"), ComponentExecutingDto.SubEventTypeEnum.OPEN_DESKTOP, null, null); componentExecutingConsumer.accept(componentExecutingDto); } } } } + if (event.type.equals("acpRequestPermission") || "request_permission".equals(jsonObject.getString("subType"))) { + Consumer componentExecutingConsumer = agentContext.getComponentExecutingConsumer(); + String toolCallId = data.getString("tool_call_id"); + ComponentExecutingDto componentExecutingDto = buildExecutingEvent(I18nUtil.systemMessage(agentContext.getUser().getLangMap(), "Backend.Sandbox.Event.RequestPermission"), ComponentExecutingDto.SubEventTypeEnum.REQUEST_PERMISSION, data, toolCallId); + @SuppressWarnings("unchecked") + List> permissionRequests = (List>) agentContext.getVariableParams().computeIfAbsent("permissionRequests", k -> new ArrayList<>()); + permissionRequests.add(Map.of( + "toolCallId", toolCallId, + "agentId", agentContext.getAgentConfig().getId().toString(), + "useState", agentContext.isUseSate(), + "data", data + )); + componentExecutingConsumer.accept(componentExecutingDto); + } if (event.type.equals("plan") || "plan".equals(jsonObject.getString("subType"))) { Consumer componentExecutingConsumer = agentContext.getComponentExecutingConsumer(); if (componentExecutingConsumer != null) { @@ -668,13 +753,17 @@ public class SandboxAgentClient { @Override public void onError(Throwable error) { - sink.error(error); - log.error("subscribe error", error); + if (!completed.get()) { + sink.error(error); + log.error("subscribe error", error); + } else { + log.info("subscribe complete, onError"); + } } @Override public void onComplete() { - if (!compeleted.get()) { + if (!completed.get()) { sink.complete(); } } @@ -699,8 +788,43 @@ public class SandboxAgentClient { } private static AgentRequest.AgentServer buildAgentServer(AgentContext agentContext, SandboxServerConfig.SandboxServer sandboxServer, ModelConfigDto modelConfig) { + //需要审批的工具汇总 + List> toolApprovalRules = new ArrayList<>(); + List patterns = new ArrayList<>(); + toolApprovalRules.add(Map.of("action", "ask", "patterns", patterns)); + toolApprovalRules.add(Map.of("action", "allow", "patterns", List.of("*ask_question*"))); + agentContext.getAgentConfig().getAgentComponentConfigList().forEach(agentComponentConfigDto -> { + if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Mcp && agentComponentConfigDto.getBindConfig() instanceof McpBindConfigDto mcpBindConfig) { + if (YesOrNoEnum.Y.getKey().equals(mcpBindConfig.getCallApproval())) { + patterns.add("*" + mcpBindConfig.getToolName()); + } + } else if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Plugin && agentComponentConfigDto.getTargetConfig() instanceof PluginDto plugin + && agentComponentConfigDto.getBindConfig() instanceof PluginBindConfigDto pluginBindConfig) { + if (YesOrNoEnum.Y.getKey().equals(pluginBindConfig.getCallApproval())) { + String functionName = plugin.getFunctionName(); + if (StringUtils.isBlank(functionName)) { + functionName = "plugin" + plugin.getId(); + } + patterns.add("*" + functionName); + } + } else if (agentComponentConfigDto.getType() == AgentComponentConfig.Type.Workflow && agentComponentConfigDto.getTargetConfig() instanceof WorkflowConfigDto workflowConfig + && agentComponentConfigDto.getBindConfig() instanceof WorkflowBindConfigDto workflowBindConfig) { + if (YesOrNoEnum.Y.getKey().equals(workflowBindConfig.getCallApproval())) { + String functionName = workflowConfig.getFunctionName(); + if (StringUtils.isBlank(functionName)) { + functionName = "workflow" + workflowConfig.getId(); + } + patterns.add("*" + functionName); + } + } + }); + Map env = new HashMap<>(); //追加环境变量 + String logPath = "{PREFIX_WORKSPACE_DIR}/" + agentContext.getConversationId() + "/logs"; + if (agentContext.getConversation().getType() == Conversation.ConversationType.DevDebug) { + logPath = "{PREFIX_WORKSPACE_DIR}/" + agentContext.getAgentConfig().getDevAgentConversationId() + "/.logs"; + } TenantConfigDto tenantConfig = agentContext.getTenantConfig(); String siteUrl = tenantConfig.getSiteUrl(); siteUrl = siteUrl.endsWith("/") ? siteUrl.substring(0, siteUrl.length() - 1) : siteUrl; @@ -713,11 +837,89 @@ public class SandboxAgentClient { if (agentContext.getVariableParams().containsKey("SANDBOX_ACCESS_KEY")) { env.put("SANDBOX_ACCESS_KEY", agentContext.getVariableParams().get("SANDBOX_ACCESS_KEY").toString()); } + //Agent开发项目时,将对应的环境变量添加到容器中 + ConversationDto conversation = agentContext.getConversation(); + if (conversation.getDevTargetType() != null) { + if (conversation.getDevTargetType().equalsIgnoreCase("Agent")) { + env.put("DEV_AGENT_ID", String.valueOf(conversation.getDevTargetId())); + } + if (conversation.getDevTargetType().equalsIgnoreCase("Skill")) { + env.put("DEV_SKILL_ID", String.valueOf(conversation.getDevTargetId())); + } + if (conversation.getDevTargetType().equalsIgnoreCase("Plugin")) { + env.put("DEV_PLUGIN_ID", String.valueOf(conversation.getDevTargetId())); + } + env.put("DEV_SPACE_ID", String.valueOf(conversation.getDevSpaceId())); + } + + Map extra = agentContext.getAgentConfig().getExtra(); + if (extra != null && extra.containsKey("createdFrom") && "VibeCoding".equalsIgnoreCase(extra.get("createdFrom").toString())) { + Map> platforms = null; + AgentPublishVersionDto latestVersion = null; + String workspacePrefix = "/home/user/"; + if (sandboxServer.getScope() == SandboxScopeEnum.USER) { + workspacePrefix = "{PREFIX_WORKSPACE_DIR}/"; + } + String cmd = workspacePrefix + agentContext.getAgentConfig().getDevAgentConversationId() + "/node_modules/.bin/tsx"; + String arg = workspacePrefix + agentContext.getAgentConfig().getDevAgentConversationId() + "/src/index.ts"; + if (agentContext.getConversation().getType() != Conversation.ConversationType.DevDebug) { + List publishVersion = agentContext.getAgentConfig().getPublishVersion(); + if (publishVersion == null || publishVersion.isEmpty()) { + throw new RuntimeException("Missing agent installation package"); + } + latestVersion = publishVersion.stream().filter(agentPublishVersionDto -> agentPublishVersionDto.getLatest() != null && agentPublishVersionDto.getLatest()).findFirst().orElse(publishVersion.get(0)); + platforms = new HashMap<>(); + for (AgentPublishVersionDto.PackageArtifact packageArtifact : latestVersion.getPackages()) { + platforms.put(packageArtifact.getPlatform(), Map.of("url", packageArtifact.getUrl())); + } + arg = workspacePrefix + "acp-agent/" + agentContext.getAgentConfig().getId() + "/" + latestVersion.getVersion() + "/dist/bundle.mjs"; + cmd = "node"; + } + + env.put("MAX_TOKENS", String.valueOf(modelConfig.getMaxTokens())); + env.put("MAX_CONTEXT_TOKENS", String.valueOf(modelConfig.getMaxContextTokens())); + env.put("API_PROTOCOL", modelConfig.getApiProtocol().name());//OpenAI、Anthropic + if (modelConfig.getApiProtocol() == ModelApiProtocolEnum.OpenAI) { + env.put("OPENAI_API_KEY", "{MODEL_PROVIDER_API_KEY}"); + env.put("OPENAI_BASE_URL", "{MODEL_PROVIDER_BASE_URL}"); + env.put("OPENAI_MODEL", modelConfig.getModel()); + } else if (modelConfig.getApiProtocol() == ModelApiProtocolEnum.Anthropic) { + env.put("ANTHROPIC_API_KEY", "{MODEL_PROVIDER_API_KEY}"); + if (sandboxServer.getScope() == SandboxScopeEnum.USER) { + env.put("ANTHROPIC_AUTH_TOKEN", "{MODEL_PROVIDER_API_KEY}"); + } + env.put("ANTHROPIC_BASE_URL", "{MODEL_PROVIDER_BASE_URL}"); + env.put("ANTHROPIC_MODEL", modelConfig.getModel()); + } + + if (modelConfig.getIsReasonModel() != null && modelConfig.getIsReasonModel() == 1) { + int maxThinkingTokens = modelConfig.getMaxTokens() == null ? 2048 : modelConfig.getMaxTokens() / 2; + env.put("MAX_THINKING_TOKENS", maxThinkingTokens > 4096 ? "4096" : Integer.toString(maxThinkingTokens)); + env.put("DISABLE_THINKING", "0"); + } else { + env.put("DISABLE_THINKING", "1"); + } + env.put("LOG_LEVEL", agentContext.getConversation().getType() == Conversation.ConversationType.DevDebug ? "debug" : "info"); + env.put("LOG_DIR", logPath); + + return AgentRequest.AgentServer.builder() + .agent_id(agentContext.getAgentConfig().getId().toString()) + .command(cmd) + .args(List.of(arg)) + .env(env) + .agent_mode(agentContext.getAgentMode()) + .version(latestVersion == null ? null : latestVersion.getVersion()) + .platforms(platforms) + .tool_approval_rules(toolApprovalRules) + .build(); + } + // 不支持claudecode的模型直接使用opencode if (!isClaudeCodeSupport(modelConfig)) { - env.put("OPENCODE_LOG_DIR", "/app/container-logs"); + env.put("OPENCODE_LOG_DIR", logPath); env.put("OPENCODE_MAX_TOKENS", String.valueOf(modelConfig.getMaxTokens())); env.put("OPENCODE_MAX_CONTEXT_TOKENS", String.valueOf(modelConfig.getMaxContextTokens())); + env.put("OPENCODE_PERMISSION", "{\"bash\":\"ask\",\"edit\":\"ask\",\"question\":\"deny\"}"); if (modelConfig.getApiProtocol() == ModelApiProtocolEnum.OpenAI) { env.put("OPENAI_API_KEY", "{MODEL_PROVIDER_API_KEY}"); env.put("OPENAI_BASE_URL", "{MODEL_PROVIDER_BASE_URL}"); @@ -748,7 +950,14 @@ public class SandboxAgentClient { env.put("CLAUDE_CODE_DISABLE_THINKING", "1"); } } - return AgentRequest.AgentServer.builder().agent_id("claude-code-acp-ts").command("claude-code-acp-ts").args(List.of()).env(env).build(); + return AgentRequest.AgentServer.builder() + .agent_id("claude-code-acp-ts") + .command("claude-code-acp-ts") + .args(List.of()) + .env(env) + .agent_mode(agentContext.getAgentMode()) + .tool_approval_rules(toolApprovalRules) + .build(); } private static boolean isClaudeCodeSupport(ModelConfigDto modelConfig) { @@ -768,7 +977,8 @@ public class SandboxAgentClient { agentContext.getContextMessages().forEach(message -> { String text = message.getText(); if (message.getMessageType() == MessageType.USER) { - text = message.getText().replaceAll("[\\s\\S]*?", "").trim(); + text = message.getText().replaceAll("[\\s\\S]*?", "") + .replaceAll("[\\s\\S]*?", "").trim(); if (text.contains("") && text.contains("")) { text = text.replace("", "").replace("", "").trim(); } @@ -801,14 +1011,19 @@ public class SandboxAgentClient { if (finalComponentExecutingDto == null) { return; } + // 检查finalText中是否包含 标签 String finalTextString = finalText.toString(); if (finalTextString.contains("") && finalTextString.contains("")) { // 已经有结果文件输出,不再追加 return; } + if (agentContext.getConversation().getType() == Conversation.ConversationType.Development) { + return; + } String resultFileText = """ - + + {description} {file} @@ -912,6 +1127,9 @@ public class SandboxAgentClient { Map contextServers = new HashMap<>(); //添加chrome-tools contextServers.put("chrome-tools", chromeMcp); + if (YesOrNoEnum.Y.getKey().equals(agentContext.getAgentConfig().getEnableAskQuestion())) { + contextServers.put("ask-question", askQuestionMcp); + } //添加mcp List mcpComponentConfigs = agentContext.getAgentConfig().getAgentComponentConfigList().stream().filter(agentComponentConfig -> agentComponentConfig.getType() == AgentComponentConfig.Type.Mcp).toList(); @@ -1256,7 +1474,7 @@ public class SandboxAgentClient { return title; } - private static ComponentExecutingDto buildExecutingEvent(String title, ComponentExecutingDto.SubEventTypeEnum subEventType, Object input) { + private static ComponentExecutingDto buildExecutingEvent(String title, ComponentExecutingDto.SubEventTypeEnum subEventType, Object input, String toolCallId) { ComponentExecuteResult componentExecuteResult = new ComponentExecuteResult(); componentExecuteResult.setStartTime(System.currentTimeMillis()); componentExecuteResult.setEndTime(componentExecuteResult.getStartTime()); @@ -1264,7 +1482,7 @@ public class SandboxAgentClient { componentExecuteResult.setName(title); componentExecuteResult.setType(ComponentTypeEnum.Event); componentExecuteResult.setInput(input); - componentExecuteResult.setExecuteId(UUID.randomUUID().toString().replace("-", "")); + componentExecuteResult.setExecuteId(toolCallId == null ? UUID.randomUUID().toString().replace("-", "") : toolCallId); ComponentExecutingDto componentExecutingDto = new ComponentExecutingDto(); componentExecutingDto.setTargetId(-1L); componentExecutingDto.setName(title); @@ -1327,12 +1545,47 @@ public class SandboxAgentClient { SimpleJvmHashCache.putHash(agentContext.getRequestId(), "modelExecuteInfos", modelExecuteInfoMap, 600); } - public boolean chatCancel(String conversationId) { - SandboxServerConfig.SandboxServer sandboxServer = sandboxServerConfigService.selectServer(Long.parseLong(conversationId)); - if (sandboxServer == null || sandboxServer.getCurrentConversation() == null || sandboxServer.getCurrentConversation().getSandboxSessionId() == null) { - return false; + private String rewriteComputerBaseUrl(ConversationDto conversation, String apiUrl) { + if (conversation.getType() == Conversation.ConversationType.DevDebug) { + return apiUrl.replace("/computer/", "/devcomputer/"); } - HttpRequest request = HttpRequest.newBuilder().uri(URI.create(sandboxServer.getServerAgentUrl() + "/computer/agent/session/cancel?project_id=" + conversationId + "&session_id=" + sandboxServer.getCurrentConversation().getSandboxSessionId() + "&user_id=" + sandboxServer.getCurrentConversation().getUserId())) + return apiUrl; + } + + public void chatCancel(String conversationId) { + SandboxServerConfig.SandboxServer sandboxServer = sandboxServerConfigService.selectServer(Long.parseLong(conversationId)); + if (sandboxServer == null || sandboxServer.getCurrentConversation() == null) { + return; + } + Map sessionMap = createSessionMap(sandboxServer); + if (!sessionMap.isEmpty()) { + sessionMap.forEach((key, value) -> chatCancel0(sandboxServer, key, value)); + return; + } + if (sandboxServer.getCurrentConversation().getSandboxSessionId() == null) { + return; + } + chatCancel0(sandboxServer, conversationId, sandboxServer.getCurrentConversation().getSandboxSessionId()); + } + + //针对AgentGroup和AgentFlow下的多个会话进行处理 + private Map createSessionMap(SandboxServerConfig.SandboxServer sandboxServer) { + Map variables = sandboxServer.getCurrentConversation().getVariables(); + Map sesssionMap = new HashMap<>(); + if (variables != null) { + variables.forEach((key, value) -> { + if (key.startsWith("_agent") && value instanceof Map && ((Map) value).containsKey("sandboxSessionId")) { + sesssionMap.put(key.replace("_agent", sandboxServer.getCurrentConversation().getId() + "_"), (String) ((Map) value).get("sandboxSessionId")); + } + }); + } + return sesssionMap; + } + + private void chatCancel0(SandboxServerConfig.SandboxServer sandboxServer, String projectId, String sandboxSessionId) { + ConversationDto conversation = sandboxServer.getCurrentConversation(); + String apiUrl = sandboxServer.getServerAgentUrl() + "/computer/agent/session/cancel?project_id=" + projectId + "&session_id=" + sandboxSessionId + "&user_id=" + conversation.getUserId(); + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(rewriteComputerBaseUrl(conversation, apiUrl))) .header("Content-Type", "application/json") .header("x-api-key", sandboxServer.getServerApiKey() == null ? "" : sandboxServer.getServerApiKey()) .timeout(Duration.ofSeconds(60)).POST(HttpRequest.BodyPublishers.noBody()).build(); @@ -1340,23 +1593,45 @@ public class SandboxAgentClient { try { send = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); log.info("chat cancel response: {}", send.body()); - return true; } catch (Throwable e) { // ignore log.error("chat cancel error", e); } - return false; } public void agentStop(String conversationId) { - SandboxServerConfig.SandboxServer sandboxServer = sandboxServerConfigService.selectServer(Long.parseLong(conversationId)); - if (sandboxServer == null || sandboxServer.getCurrentConversation() == null || sandboxServer.getCurrentConversation().getSandboxSessionId() == null) { + SandboxServerConfig.SandboxServer sandboxServer; + try { + sandboxServer = sandboxServerConfigService.selectServer(Long.parseLong(conversationId)); + } catch (Exception e) { + // ignore + log.error("agentStop error", e); return; } - HttpRequest request = HttpRequest.newBuilder().uri(URI.create(sandboxServer.getServerAgentUrl() + "/computer/agent/stop")) + if (sandboxServer == null || sandboxServer.getCurrentConversation() == null) { + return; + } + Map sessionMap = createSessionMap(sandboxServer); + if (!sessionMap.isEmpty()) { + sessionMap.forEach((key, value) -> agentStop0(sandboxServer, key, value)); + return; + } + try { + agentStop0(sandboxServer, conversationId, sandboxServer.getCurrentConversation().getSandboxSessionId()); + } catch (Exception e) { + // ignore + } + } + + public void agentStop0(SandboxServerConfig.SandboxServer sandboxServer, String projectId, String sandboxSessionId) { + if (sandboxServer == null || sandboxServer.getCurrentConversation() == null || sandboxSessionId == null) { + return; + } + String apiUrl = sandboxServer.getServerAgentUrl() + "/computer/agent/stop"; + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(rewriteComputerBaseUrl(sandboxServer.getCurrentConversation(), apiUrl))) .header("Content-Type", "application/json") .header("x-api-key", sandboxServer.getServerApiKey() == null ? "" : sandboxServer.getServerApiKey()) - .timeout(Duration.ofSeconds(60)).POST(HttpRequest.BodyPublishers.ofString(JSON.toJSONString(Map.of("user_id", sandboxServer.getCurrentConversation().getUserId().toString(), "session_id", sandboxServer.getCurrentConversation().getSandboxSessionId(), "project_id", conversationId)))).build(); + .timeout(Duration.ofSeconds(60)).POST(HttpRequest.BodyPublishers.ofString(JSON.toJSONString(Map.of("user_id", sandboxServer.getCurrentConversation().getUserId().toString(), "session_id", sandboxSessionId, "project_id", projectId)))).build(); HttpResponse send; try { send = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); @@ -1367,9 +1642,74 @@ public class SandboxAgentClient { } } + public void notifyResolved(String conversationId, String toolCallId, Map outcome) { + SandboxServerConfig.SandboxServer sandboxServer = sandboxServerConfigService.selectServer(Long.parseLong(conversationId)); + if (sandboxServer == null || sandboxServer.getCurrentConversation() == null) { + return; + } + String projectId = conversationId; + AtomicReference sessionId = new AtomicReference<>(); + if (sandboxServer.getCurrentConversation().getSandboxSessionId() != null) { + sessionId.set(sandboxServer.getCurrentConversation().getSandboxSessionId()); + } + Map variables = sandboxServer.getCurrentConversation().getVariables(); + if (variables != null) { + @SuppressWarnings("unchecked") + List> permissionRequests = (List>) variables.computeIfAbsent("permissionRequests", k -> new ArrayList<>()); + for (Map permissionRequest : permissionRequests) { + if (permissionRequest.get("toolCallId").equals(toolCallId) && permissionRequest.get("useState") != null && (Boolean) permissionRequest.get("useState")) { + projectId = conversationId + "_" + permissionRequest.get("agentId"); + String agentId = permissionRequest.get("agentId").toString(); + variables.forEach((key, value) -> { + if (key.startsWith("_agent" + agentId) && value instanceof Map && ((Map) value).containsKey("sandboxSessionId")) { + sessionId.set((String) ((Map) value).get("sandboxSessionId")); + } + }); + } + } + } + if (sessionId.get() == null) { + return; + } + Map requestBody = new HashMap<>(); + requestBody.put("user_id", sandboxServer.getCurrentConversation().getUserId().toString()); + requestBody.put("project_id", projectId); + Map permissionResolveRequest = new HashMap<>(); + Map requestPermissionResponse = new HashMap<>(); + requestPermissionResponse.put("outcome", outcome); + permissionResolveRequest.put("request_permission_response", requestPermissionResponse); + permissionResolveRequest.put("session_id", sessionId.get()); + permissionResolveRequest.put("tool_call_id", toolCallId); + requestBody.put("permission_resolve_request", permissionResolveRequest); + + String apiUrl = sandboxServer.getServerAgentUrl() + "/computer/notify-resolved"; + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(rewriteComputerBaseUrl(sandboxServer.getCurrentConversation(), apiUrl))) + .header("Content-Type", "application/json") + .header("x-api-key", sandboxServer.getServerApiKey() == null ? "" : sandboxServer.getServerApiKey()) + .timeout(Duration.ofSeconds(30)) + .POST(HttpRequest.BodyPublishers.ofString(JSON.toJSONString(requestBody))) + .build(); + try { + HttpResponse send = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + String body = send.body(); + log.info("notify resolved response: {}", body); + if (JSON.isValid(body)) { + JSONObject res = JSON.parseObject(body); + if (!"0000".equals(res.getString("code"))) { + throw new BizException(res.getString("message") != null ? res.getString("message") : "Permission failed"); + } + } + } catch (BizException e) { + throw e; + } catch (Throwable e) { + log.error("notify resolved error", e); + throw new BizException("Permission failed: " + e.getMessage()); + } + } + public boolean checkAgentIfAlive(String conversationId) { SandboxServerConfig.SandboxServer sandboxServer = sandboxServerConfigService.selectServer(Long.parseLong(conversationId)); - if (sandboxServer == null || sandboxServer.getCurrentConversation() == null || sandboxServer.getCurrentConversation().getSandboxSessionId() == null) { + if (sandboxServer == null || sandboxServer.getCurrentConversation() == null) { return false; } AgentContext agentContext = new AgentContext(); @@ -1379,7 +1719,10 @@ public class SandboxAgentClient { } public static SseSubscription subscribe(SandboxServerConfig.SandboxServer sandboxServer, String url, SseEventHandler eventHandler) { - HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).header("Accept", "text/event-stream").header("Cache-Control", "no-cache") + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "text/event-stream") + .header("Cache-Control", "no-cache") .header("x-api-key", sandboxServer.getServerApiKey() == null ? "" : sandboxServer.getServerApiKey()) .GET().build(); StringBuilder eventBuilder = new StringBuilder(); @@ -1511,11 +1854,18 @@ public class SandboxAgentClient { AtomicReference subscription, Disposable heartbeatDisposable) { public void cancel() { + if (future != null) { + try { + future.cancel(true); + } catch (Exception e) { + log.warn("Cancel SSE subscription failed", e); + // Ignore + } + } if (subscription.get() != null) { subscription.get().cancel(); } heartbeatDisposable.dispose(); -// future.cancel(true); } } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/dto/AgentRequest.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/dto/AgentRequest.java index 5fb47378..3a9be68d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/dto/AgentRequest.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/agent/dto/AgentRequest.java @@ -18,6 +18,7 @@ public class AgentRequest { private String user_id; private String session_id; private String project_id; + private String agent_work_dir; private String request_id; private List attachments; private List data_source_attachments; @@ -107,6 +108,10 @@ public class AgentRequest { private List args; private Map env; private Metadata metadata; + private String agent_mode; + private Map> platforms; + private List> tool_approval_rules; + private String version; } @Builder diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/knowledge/KnowledgeBaseSearcher.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/knowledge/KnowledgeBaseSearcher.java index 88f59d0e..138baf63 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/knowledge/KnowledgeBaseSearcher.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/knowledge/KnowledgeBaseSearcher.java @@ -10,15 +10,19 @@ import com.xspaceagi.knowledge.sdk.response.KnowledgeFullTextSearchResponseVo; import com.xspaceagi.knowledge.sdk.response.KnowledgeFullTextSearchResultVo; import com.xspaceagi.knowledge.sdk.response.KnowledgeQaResponseVo; import com.xspaceagi.knowledge.sdk.response.KnowledgeQaVo; +import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeDocumentSearchRpcService; import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeFullTextSearchRpcService; import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeQaSearchRpcService; +import com.xspaceagi.knowledge.sdk.vo.DocumentAddRequestVo; import com.xspaceagi.system.spec.tenant.thread.TenantFunctions; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -31,6 +35,9 @@ public class KnowledgeBaseSearcher extends BaseComponent { private IKnowledgeFullTextSearchRpcService knowledgeFullTextSearchRpcService; + @Resource + private IKnowledgeDocumentSearchRpcService iKnowledgeDocumentSearchRpcService; + private IFileAccessService iFileAccessService; @Autowired @@ -59,6 +66,7 @@ public class KnowledgeBaseSearcher extends BaseComponent { } else { queryList.add(QueryText.builder().text(searchContext.getQuery()).searchStrategy(searchContext.getSearchStrategy()).build()); } + AtomicReference throwableAtomicReference = new AtomicReference<>(); List results = new ArrayList<>(); //使用多线程并行调用queryList queryList.parallelStream().forEach(query -> { @@ -78,6 +86,7 @@ public class KnowledgeBaseSearcher extends BaseComponent { } catch (Exception e) { // 忽略 log.error("query error: {}", e.getMessage()); + throwableAtomicReference.set(e); } }); } else if (query.getSearchStrategy() == SearchStrategyEnum.FULL_TEXT) { @@ -94,6 +103,7 @@ public class KnowledgeBaseSearcher extends BaseComponent { } catch (Exception e) { // 忽略 log.error("search error: {}", e.getMessage()); + throwableAtomicReference.set(e); } } }); @@ -147,6 +157,10 @@ public class KnowledgeBaseSearcher extends BaseComponent { qaVo.setAnswer(completeFileUrlWithAk(qaVo.getAnswer())); qaVo.setRawTxt(completeFileUrlWithAk(qaVo.getRawTxt())); }); + if (finalQaVoList.isEmpty() && throwableAtomicReference.get() != null) { + sink.error(throwableAtomicReference.get()); + return; + } sink.success(finalQaVoList); } catch (Throwable e) { log.error("search error", e); @@ -169,6 +183,10 @@ public class KnowledgeBaseSearcher extends BaseComponent { return text; } + public Long documentAdd(DocumentAddRequestVo documentAddRequestVo) { + return iKnowledgeDocumentSearchRpcService.documentAdd(documentAddRequestVo); + } + // 预编译一次,性能更好 private static final Pattern URL_PATTERN = Pattern.compile("https?://[\\w.-]+(?:\\.[\\w.-]+)+[/\\w .-?&%=]*", Pattern.CASE_INSENSITIVE); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/JdkClientHttpRequest.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/JdkClientHttpRequest.java index 3e0942ce..557d84be 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/JdkClientHttpRequest.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/JdkClientHttpRequest.java @@ -67,7 +67,7 @@ class JdkClientHttpRequest extends AbstractClientHttpRequest { this.uri = uri; this.bufferFactory = bufferFactory; this.builder = HttpRequest.newBuilder(uri); - this.builder.timeout(Duration.ofMinutes(3));//1分钟没有任何响应则超时 + this.builder.timeout(Duration.ofMinutes(5)); } @Override diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/JsonExtractor.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/JsonExtractor.java new file mode 100644 index 00000000..b6d74d5d --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/JsonExtractor.java @@ -0,0 +1,86 @@ +package com.xspaceagi.agent.core.infra.component.model; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 从混合文本中精确提取 JSON 对象/数组。 + * 使用 Jackson 流式解析器校验边界,比手工状态机更健壮。 + */ +public class JsonExtractor { + + private static final JsonFactory FACTORY = new JsonFactory(); + + public static List extract(String text) { + List results = new ArrayList<>(); + if (text == null || text.isEmpty()) return results; + + // 跳过属性名中的 { [ (如 "prompt":"{\"x\":1}" 这种不用提取) + boolean inTopString = false; + + int i = 0; + while (i < text.length()) { + char c = text.charAt(i); + + // 跟踪是否在顶层字符串值中 + if (c == '"') { + inTopString = !inTopString; + i++; + continue; + } + + if (inTopString) { + i++; + continue; + } + + if (c == '{' || c == '[') { + int end = tryParseJson(text, i); + if (end > i) { + results.add(text.substring(i, end + 1)); + i = end + 1; + continue; + } + } + i++; + } + return results; + } + + public static String extractLast(String text) { + List list = extract(text); + return list.isEmpty() ? null : list.get(list.size() - 1); + } + + /** + * 从 start 位置尝试用 Jackson 解析一段完整 JSON。 + * 成功返回结束下标(含),失败返回 -1。 + */ + private static int tryParseJson(String text, int start) { + try (JsonParser parser = FACTORY.createParser(text.substring(start))) { + JsonToken token = parser.nextToken(); + if (token == null) return -1; + + // 递归跳过整个 token(对象/数组会自动跳过所有子元素) + parser.skipChildren(); + + // parser.currentLocation() 给出的是解码后的字符偏移, + // token 结束位置 = start + 当前字节偏移 → 转为 char 偏移 + long byteOffset = parser.currentLocation().getCharOffset(); + int end = start + (int) byteOffset - 1; + if (end <= start) return -1; + + // 简单校验:结束字符必须是 } 或 ] + char lastChar = text.charAt(end); + return (lastChar == '}' || lastChar == ']') ? end : -1; + + } catch (IOException e) { + return -1; + } + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/ModelClientFactory.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/ModelClientFactory.java index cfafc954..d981c925 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/ModelClientFactory.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/ModelClientFactory.java @@ -105,7 +105,7 @@ public class ModelClientFactory { return webClientBuilder.clone(); } - public ChatClient createChatClient(ModelConfigDto model) { + public ChatClient createChatClient(ModelConfigDto model, List functionCallbacks) { ModelContext modelContext = new ModelContext(); String traceId = UUID.randomUUID().toString().replace("-", ""); modelContext.setTraceContext(TraceContext.builder() @@ -122,7 +122,7 @@ public class ModelClientFactory { modelContext.setRequestId(traceId); modelContext.setConversationId(traceId); modelContext.setModelConfig(model); - return createChatClient(modelContext, model, List.of()); + return createChatClient(modelContext, model, functionCallbacks); } public EmbeddingModel createEmbeddingModel(ModelConfigDto model) { @@ -186,7 +186,16 @@ public class ModelClientFactory { apiInfo.setKey(frontendModelDto.getApiKey()); apiInfo.setUrl(frontendModelDto.getBaseUrl()); - + boolean forceToolChoice = false; + if (functionCallbacks != null) { + forceToolChoice = functionCallbacks.stream().anyMatch(callback -> callback.getToolDefinition().name().contains("json_output")); + if (forceToolChoice) { + isReasoningModel = false; + } +// if (model.getTypes() != null && model.getTypes().contains(ModelCapabilityEnum.Reasoning)) { +// forceToolChoice = false; +// } + } if (model.getApiProtocol() == ModelApiProtocolEnum.OpenAI) { var promptOptions = OpenAiChatOptions.builder() .model(model.getModel()) @@ -194,7 +203,8 @@ public class ModelClientFactory { .maxTokens(model.getMaxTokens()) .temperature(temperature) .topP(topP) - .extraBody(buildExtraBody(model)) + .extraBody(buildExtraBody(model, isReasoningModel)) + .toolChoice(forceToolChoice ? OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder.function("json_output") : null) .build(); OpenAiApi api = new OpenAiApi(apiInfo.getUrl(), new SimpleApiKey(apiInfo.getKey()), CollectionUtils.toMultiValueMap(Map.of()), "/chat/completions", "/embeddings", restClientBuilder.clone(), cloneWebClientBuilder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); @@ -225,6 +235,7 @@ public class ModelClientFactory { .temperature(temperature) .topP(topP) .maxTokens(model.getMaxTokens()) + .toolChoice(forceToolChoice ? new AnthropicApi.ToolChoiceAny("any", true) : null) .thinking(new AnthropicApi.ChatCompletionRequest.ThinkingConfig(isReasoningModel ? AnthropicApi.ThinkingType.ENABLED : AnthropicApi.ThinkingType.DISABLED, budget)) .build(); AnthropicApi api = AnthropicApi.builder() @@ -247,16 +258,15 @@ public class ModelClientFactory { return builder.build(); } - private Map buildExtraBody(ModelConfigDto model) { + private Map buildExtraBody(ModelConfigDto model, boolean isReasoningModel) { boolean thinkingModel = (model.getTypes() != null && model.getTypes().contains(ModelCapabilityEnum.Reasoning)); if (!thinkingModel) { return null; } - boolean thinking = model.getIsReasonModel() != null && model.getIsReasonModel() == 1; if ("alibaba-cn".equals(model.getPid()) || "alibaba-coding-plan-cn".equals(model.getPid()) || "qiming".equals(model.getPid())) { - return Map.of("enable_thinking", thinking); + return Map.of("enable_thinking", isReasoningModel, "thinking", Map.of("type", isReasoningModel ? "enabled" : "disabled")); } - return Map.of("thinking", Map.of("type", thinking ? "enabled" : "disabled")); + return Map.of("thinking", Map.of("type", isReasoningModel ? "enabled" : "disabled")); } private static String completeBaseUrl(String baseUrl) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/ModelInvoker.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/ModelInvoker.java index ff7e4690..f6a79593 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/ModelInvoker.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/ModelInvoker.java @@ -435,24 +435,6 @@ public class ModelInvoker extends BaseComponent { } if (modelContext.getModelCallResult().getFirstResponseTime() == null) { modelContext.getModelCallResult().setFirstResponseTime(System.currentTimeMillis()); - if (StringUtils.isBlank(text) && !isReasoning) { - // deepseek function call exception finishReason=TOOL_CALLS - Object finishReason = assistantMessage.getMetadata().get("finishReason"); - if (finishReason != null && finishReason.toString().equals("TOOL_CALLS") - && CollectionUtils.isEmpty(assistantMessage.getToolCalls())) { - CallMessage callMessage = new CallMessage(); - callMessage.setText(text); - callMessage.setType(MessageTypeEnum.CHAT); - callMessage.setRole(ChatMessageDto.Role.ASSISTANT); - callMessage.setId(messageId); - callMessage.setFinished(false); - callMessage.setText("The model is not working properly😒 please resend your message~"); - sink.tryEmitNext(callMessage); - modelContext.getAgentContext().getAgentExecuteResult().setSuccess(false); - modelContext.getAgentContext().getAgentExecuteResult().setError(callMessage.getText()); - } - return; - } } modelContext.setHasReasoningContent(isReasoning); @@ -524,12 +506,14 @@ public class ModelInvoker extends BaseComponent { private void handleFinish(ModelContext modelContext, Sinks.Many sink, String messageId, String text, String finishReason) { if (modelContext.getModelCallConfig().getOutputType() == OutputTypeEnum.JSON) { - String text0 = getJSONText(text == null ? "" : text); - if (!JSON.isValid(text0) && modelContext.getRetryCount() < 3) { - modelContext.setRetryCount(modelContext.getRetryCount() + 1); - log.warn("The JSON format returned by the model is incorrect, please check the prompt. Returned content: {}", text); - streamCall(modelContext, sink); - return; + if (!JSON.isValid(text)) { + String text0 = getJSONText(text == null ? "" : text); + if (!JSON.isValid(text0) && modelContext.getRetryCount() < 3) { + modelContext.setRetryCount(modelContext.getRetryCount() + 1); + log.warn("The JSON format returned by the model is incorrect, please check the prompt. Returned content: {}", text); + streamCall(modelContext, sink); + return; + } } } setModelCallResult(modelContext, text); @@ -720,6 +704,7 @@ public class ModelInvoker extends BaseComponent { } public static String getJSONText(String text) { + String originalText = text; if (text.trim().startsWith("")) { text = text.replaceAll("[\\s\\S]*?", "").trim(); } @@ -737,16 +722,35 @@ public class ModelInvoker extends BaseComponent { // Trim again to remove any potential whitespace text = text.trim(); } + if (!JSON.isValid(text)) { + text = JsonExtractor.extractLast(originalText); + } return text; } private List buildFunctionCallbacks(ModelContext modelContext) { + ToolCallback jsonOutputToolCallback = null; + ModelCallConfigDto modelCallConfig = modelContext.getModelCallConfig(); + if (modelCallConfig.getOutputType() == OutputTypeEnum.JSON && modelContext.getModelConfig().getFunctionCall() == ModelFunctionCallEnum.StreamCallSupported) { + Map inputSchema = buildFunctionParams(modelCallConfig.getOutputArgs()); + jsonOutputToolCallback = FunctionToolCallback + .builder("json_output", (input, context) -> JSON.toJSONString(input)) + .description("Generate JSON based on the context. When this tool appears, it indicates that the user intends to invoke it to extract JSON content; please invoke it finally") + .inputSchema(ModelOptionsUtils.toJsonString(inputSchema)) + .inputType(HashMap.class) + .toolMetadata(DefaultToolMetadata.builder().returnDirect(true).build()) + .toolCallResultConverter((res, returnType) -> (res instanceof String) ? (String) res : JSON.toJSONString(res)) + .build(); + } if (modelContext.getModelCallConfig().getComponentConfigs() == null) { + if (jsonOutputToolCallback != null) { + return Collections.singletonList(jsonOutputToolCallback); + } return new ArrayList<>(); } // Remove duplicates Set functionNames = new HashSet<>(); - return modelContext.getModelCallConfig().getComponentConfigs().stream().map(componentConfig -> { + List toolCallbacks = modelContext.getModelCallConfig().getComponentConfigs().stream().map(componentConfig -> { if (componentConfig.getType() == null) { return null; } @@ -821,6 +825,10 @@ public class ModelInvoker extends BaseComponent { .toolCallResultConverter((res, returnType) -> (res instanceof String) ? (String) res : JSON.toJSONString(res)) .build(); }).filter(Objects::nonNull).collect(Collectors.toList()); + if (jsonOutputToolCallback != null) { + toolCallbacks.add(jsonOutputToolCallback); + } + return toolCallbacks; } private Object componentExecute(ModelContext modelContext, ComponentConfig componentConfig, Object input) { @@ -1090,7 +1098,10 @@ public class ModelInvoker extends BaseComponent { if (agentContext == null) { return "execution failed"; } - Object message = ((Map) input).get("user_message"); + Object message = ((Map) input).get("message"); + if (message == null) { + message = ((Map) input).get("user_message"); + } if (message == null) { message = modelContext.getAgentContext().getMessage(); } @@ -1126,7 +1137,7 @@ public class ModelInvoker extends BaseComponent { return new SystemMessage(Prompts.TIME_PROMPT.replace("${time}", new Date().toString())); } - return new SystemMessage(Prompts.TIME_PROMPT.replace("${time}", new Date().toString()) + modelContext.getModelCallConfig().getSystemPrompt()); + return new SystemMessage(modelContext.getModelCallConfig().getSystemPrompt()); } private static UserMessage buildUserMessage(ModelContext modelContext) { @@ -1226,7 +1237,7 @@ public class ModelInvoker extends BaseComponent { return prompt; } List componentConfigs = new ArrayList<>(componentConfigs0); - componentConfigs.removeIf(c -> c.getType() == null || c.getTargetId() == null || (c.getType() != ComponentTypeEnum.Plugin && c.getType() != ComponentTypeEnum.Workflow && c.getType() != ComponentTypeEnum.Mcp)); + componentConfigs.removeIf(c -> c.getType() == null || c.getTargetId() == null || (c.getType() != ComponentTypeEnum.Plugin && c.getType() != ComponentTypeEnum.Workflow && c.getType() != ComponentTypeEnum.Mcp && c.getType() != ComponentTypeEnum.Agent)); if (componentConfigs.isEmpty() || StringUtils.isBlank(prompt)) { return prompt; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/dto/ComponentExecutingDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/dto/ComponentExecutingDto.java index f2bd9226..39d33a42 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/dto/ComponentExecutingDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/model/dto/ComponentExecutingDto.java @@ -114,6 +114,10 @@ public class ComponentExecutingDto implements Serializable { // 打开桌面 OPEN_DESKTOP, // 打开文件 - OPEN_FILE + OPEN_FILE, + // request_permission + REQUEST_PERMISSION, + //ASK USER + ASK_QUESTION, } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/PluginExecutor.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/PluginExecutor.java index efd1e92b..eba35780 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/PluginExecutor.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/PluginExecutor.java @@ -16,6 +16,7 @@ import com.xspaceagi.eco.market.sdk.request.ClientSecretRequest; import com.xspaceagi.log.sdk.service.ILogRpcService; import com.xspaceagi.log.sdk.vo.LogDocument; import com.xspaceagi.system.application.dto.UserDto; +import com.xspaceagi.system.sdk.common.TraceContext; import com.xspaceagi.system.spec.enums.ErrorCodeEnum; import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; @@ -148,6 +149,55 @@ public class PluginExecutor { logDocument.setResultCode("0000"); logDocument.setResultMsg("Success"); logDocument.setRequestEndTime(System.currentTimeMillis()); + + Object result1 = result.getResult(); + if (result1 instanceof Map) { + Object o = ((Map) result1).get("usage"); + if (o instanceof Map) { + TraceContext.TokenUsage tokenUsage = new TraceContext.TokenUsage(); + Object o1 = ((Map) o).get("inputTokens"); + if (o1 instanceof Number) { + tokenUsage.inputTokens = ((Number) o1).longValue(); + } + + o1 = ((Map) o).get("input_tokens"); + if (o1 instanceof Number) { + tokenUsage.inputTokens = ((Number) o1).longValue(); + } + + Object o2 = ((Map) o).get("outputTokens"); + if (o2 instanceof Number) { + tokenUsage.outputTokens = ((Number) o2).longValue(); + pluginContext.getTraceContext().setTokenUsage(tokenUsage); + } + + o2 = ((Map) o).get("output_tokens"); + if (o2 instanceof Number) { + tokenUsage.outputTokens = ((Number) o2).longValue(); + pluginContext.getTraceContext().setTokenUsage(tokenUsage); + } + + Object o3 = ((Map) o).get("completionTokens"); + if (o3 instanceof Number) { + tokenUsage.outputTokens = ((Number) o3).longValue(); + pluginContext.getTraceContext().setTokenUsage(tokenUsage); + } + + o3 = ((Map) o).get("completion_tokens"); + if (o3 instanceof Number) { + tokenUsage.outputTokens = ((Number) o3).longValue(); + pluginContext.getTraceContext().setTokenUsage(tokenUsage); + } + + TraceContext.DurationUsage durationUsage = new TraceContext.DurationUsage(); + Object o4 = ((Map) o).get("duration"); + if (o4 instanceof Number) { + durationUsage.duration = ((Number) o4).longValue(); + pluginContext.getTraceContext().setDurationUsage(durationUsage); + } + } + } + iLogRpcService.pushTraceLog(pluginContext.getTraceContext()); } catch (Exception e) { // Ignore diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/handler/CodePluginHandler.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/handler/CodePluginHandler.java index 7aee568c..5d678550 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/handler/CodePluginHandler.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/handler/CodePluginHandler.java @@ -77,6 +77,9 @@ public class CodePluginHandler extends AbstractPluginHandler { outputMap.putAll(resultMap); codePluginConfigDto.setOutputArgs(new ArrayList<>()); } + if (resultMap != null && resultMap.containsKey("usage")) { + outputMap.put("usage", resultMap.get("usage")); + } resetRequireToFalse(codePluginConfigDto.getOutputArgs()); for (Arg outputArg : codePluginConfigDto.getOutputArgs()) { if ((outputArg.getDataType() == DataTypeEnum.Object || outputArg.getDataType().name().startsWith("Array")) && CollectionUtils.isEmpty(outputArg.getSubArgs())) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/handler/HttpPluginHandler.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/handler/HttpPluginHandler.java index 372fe603..40403cda 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/handler/HttpPluginHandler.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/plugin/handler/HttpPluginHandler.java @@ -1,6 +1,7 @@ package com.xspaceagi.agent.core.infra.component.plugin.handler; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; import com.xspaceagi.agent.core.adapter.dto.config.Arg; import com.xspaceagi.agent.core.adapter.dto.config.plugin.HttpPluginConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.workflow.HttpNodeConfigDto; @@ -89,11 +90,20 @@ public class HttpPluginHandler extends AbstractPluginHandler { HttpResponse httpResponse = httpClient.getLastResponse(); Map outputMap = new LinkedHashMap<>(); - if (result != null && JSON.isValid(result)) { + if (JSON.isValid(result)) { if (result.startsWith("[")) { result = "{\"ROOT_ARRAY\":" + result + "}"; } - Map resultMap = JSON.parseObject(result); + JSONObject resultMap = JSON.parseObject(result); + //检查是否有usage字段 + JSONObject usage = resultMap.getJSONObject("usage"); + if (usage == null && resultMap.get("data") instanceof JSONObject) { + JSONObject data = resultMap.getJSONObject("data"); + if (data != null && data.get("usage") instanceof JSONObject && null != data.getJSONObject("usage")) { + outputMap.put("usage", data.getJSONObject("usage")); + } + } + //null时为解析参数时传递 if (httpPluginConfigDto.getOutputArgs() == null) { outputMap.putAll(resultMap); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/ExecuteStatusMap.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/ExecuteStatusMap.java deleted file mode 100644 index 31c356b1..00000000 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/ExecuteStatusMap.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.xspaceagi.agent.core.infra.component.workflow; - -import com.xspaceagi.agent.core.infra.component.workflow.enums.NodeExecuteStatus; - -import java.util.LinkedHashMap; -import java.util.function.Consumer; - -public class ExecuteStatusMap extends LinkedHashMap { - - private Consumer consumer; - - @Override - public NodeExecuteStatus put(String key, NodeExecuteStatus value) { - NodeExecuteStatus executeStatus = super.put(key, value); - if (consumer != null) { - consumer.accept(key); - } - return executeStatus; - } - - public void setConsumer(Consumer consumer) { - this.consumer = consumer; - } -} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/WorkflowContext.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/WorkflowContext.java index b372018e..989c598e 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/WorkflowContext.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/WorkflowContext.java @@ -2,6 +2,7 @@ package com.xspaceagi.agent.core.infra.component.workflow; import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; import com.xspaceagi.agent.core.infra.component.agent.AgentContext; +import com.xspaceagi.agent.core.infra.component.workflow.dto.FlowChatMessage; import com.xspaceagi.agent.core.infra.component.workflow.dto.LoopNodeExecutingDto; import com.xspaceagi.agent.core.infra.component.workflow.dto.NodeExecuteResult; import com.xspaceagi.agent.core.infra.component.workflow.dto.NodeExecutingDto; @@ -9,6 +10,7 @@ import com.xspaceagi.agent.core.infra.component.workflow.enums.NodeExecuteStatus import com.xspaceagi.system.sdk.common.TraceContext; import lombok.Data; import lombok.ToString; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import java.io.Serializable; @@ -19,6 +21,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +@Slf4j @Data @ToString public class WorkflowContext implements Serializable { @@ -57,6 +60,8 @@ public class WorkflowContext implements Serializable { private Consumer nodeExecutingConsumer; + private Consumer flowChatMessageConsumer; + //结束节点最终输出的内容(可选) private String endNodeContent; @@ -80,7 +85,10 @@ public class WorkflowContext implements Serializable { private String from; + private Map resultStorage; + private WorkflowContextServiceHolder workflowContextServiceHolder; + private boolean restored; public Map>> getNodeExecuteInputMap() { if (nodeExecuteInputMap == null) { @@ -109,10 +117,42 @@ public class WorkflowContext implements Serializable { return executingLoopNodeMap == null ? executingLoopNodeMap = Collections.synchronizedMap(new LinkedHashMap<>()) : executingLoopNodeMap; } + public Map getResultStorage() { + if (resultStorage == null) { + resultStorage = new ConcurrentHashMap<>(); + } + return resultStorage; + } + + public void saveResultStorage() { + if (workflowContextServiceHolder != null) { + workflowContextServiceHolder.getRedisUtil().hashPutAll("workflow_result_storage:" + requestId, resultStorage); + workflowContextServiceHolder.getRedisUtil().expire("workflow_result_storage:" + requestId, 86400); + log.debug("saveResultStorage, workflowId {}, result {}", workflowConfig.getId(), resultStorage); + } + } + + public void restoreResultStorage() { + if (workflowContextServiceHolder != null && !restored) { + synchronized (this) { + if (restored) { + return; + } + restored = true; + Map resultStorage = workflowContextServiceHolder.getRedisUtil().hashGetAll("workflow_result_storage:" + requestId); + if (resultStorage != null) { + this.getResultStorage().putAll(resultStorage); + } + } + log.debug("restoreResultStorage, workflowId {}, result {}", workflowConfig.getId(), resultStorage); + } + } + public AgentContext getCopiedAgentContext() { AgentContext agentContext0 = new AgentContext(); BeanUtils.copyProperties(this.agentContext, agentContext0); agentContext0.setContextMessages(null); return agentContext0; } + } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/WorkflowExecutor.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/WorkflowExecutor.java index c6d04406..f906dada 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/WorkflowExecutor.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/WorkflowExecutor.java @@ -223,6 +223,24 @@ public class WorkflowExecutor { logDocument.setResultCode("0000"); logDocument.setResultMsg("Success"); logDocument.setRequestEndTime(System.currentTimeMillis()); + + if (result instanceof Map) { + Object o = ((Map) result).get("usage"); + if (o instanceof Map) { + TraceContext.TokenUsage tokenUsage = new TraceContext.TokenUsage(); + Object o3 = ((Map) o).get("completionTokens"); + if (o3 instanceof Number) { + tokenUsage.outputTokens = ((Number) o3).longValue(); + } + TraceContext.DurationUsage durationUsage = new TraceContext.DurationUsage(); + Object o4 = ((Map) o).get("duration"); + if (o4 instanceof Number) { + durationUsage.duration = ((Number) o4).longValue(); + workflowContext.getTraceContext().setDurationUsage(durationUsage); + } + } + } + workflowContext.getWorkflowContextServiceHolder().getILogRpcService().pushTraceLog(workflowContext.getTraceContext()); } catch (Exception e) { // Ignore diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/dto/FlowChatMessage.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/dto/FlowChatMessage.java new file mode 100644 index 00000000..61ea2129 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/dto/FlowChatMessage.java @@ -0,0 +1,10 @@ +package com.xspaceagi.agent.core.infra.component.workflow.dto; + +import com.xspaceagi.agent.core.infra.component.agent.AgentContext; +import com.xspaceagi.agent.core.infra.component.model.dto.CallMessage; +import lombok.Data; + +@Data +public class FlowChatMessage extends CallMessage { + private AgentContext agentContext; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/ConditionNodeHandler.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/ConditionNodeHandler.java index 8743d090..36399d41 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/ConditionNodeHandler.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/ConditionNodeHandler.java @@ -64,9 +64,9 @@ public class ConditionNodeHandler extends AbstractNodeHandler { return Mono.just(Map.of()); } - private boolean eval(Object firstValue, Object secondValue, ConditionNodeConfigDto.CompareTypeEnum compareType) { + public static boolean eval(Object firstValue, Object secondValue, ConditionNodeConfigDto.CompareTypeEnum compareType) { String firstValueStr = firstValue == null ? null : firstValue.toString(); - if (firstValue != null && (firstValue instanceof List) && ((List) firstValue).size() > 0) { + if ((firstValue instanceof List) && !((List) firstValue).isEmpty()) { Object val = ((List) firstValue).get(0); firstValueStr = val == null ? null : val.toString(); } @@ -135,7 +135,7 @@ public class ConditionNodeHandler extends AbstractNodeHandler { return false; } - private int compareValues(Object firstValue, Object secondValue) { + private static int compareValues(Object firstValue, Object secondValue) { if (firstValue == null || secondValue == null) { return -1; } @@ -146,7 +146,7 @@ public class ConditionNodeHandler extends AbstractNodeHandler { } } - private int compareLength(Object firstValue, Object secondValue) { + private static int compareLength(Object firstValue, Object secondValue) { if (firstValue == null || secondValue == null) { return -1; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/HandlerFactory.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/HandlerFactory.java index 200512c0..f6ac13ec 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/HandlerFactory.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/HandlerFactory.java @@ -23,6 +23,7 @@ public class HandlerFactory { case Output -> new WrappedNodeHandler(new ProcessOutputNodeHandler()); case Code -> new WrappedNodeHandler(new CodeNodeHandler()); case Knowledge -> new WrappedNodeHandler(new KnowledgeBaseNodeHandler()); + case KnowledgeInsert -> new WrappedNodeHandler(new KnowledgeInsertNodeHandler()); case VariableAggregation -> new VariableAggregationNodeHandler(); case Variable -> new VariableNodeHandler(); case LongTermMemory -> new LongMemoryNodeHandler(); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/IntentRecognitionNodeHandler.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/IntentRecognitionNodeHandler.java index 1518ea57..49b07e4b 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/IntentRecognitionNodeHandler.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/IntentRecognitionNodeHandler.java @@ -38,9 +38,12 @@ public class IntentRecognitionNodeHandler extends AbstractNodeHandler { Map intentOption = new HashMap<>(); intentOption.put("classificationId", classificationIndex.getAndIncrement()); intentOption.put("intent", intentConfigDto.getIntent()); + if (intentConfigDto.getName() != null) { + intentOption.put("name", intentConfigDto.getName()); + } list.add(intentOption); }, ArrayList::addAll); - StringBuilder stringBuilder = new StringBuilder(INTENT_RECOGNITION_SYSTEM_PROMPT).append("\n"); + StringBuilder stringBuilder = new StringBuilder(); if (StringUtils.isNotBlank(intentRecognitionNodeConfigDto.getExtraPrompt())) { stringBuilder.append(intentRecognitionNodeConfigDto.getExtraPrompt()).append("\n"); } @@ -54,8 +57,8 @@ public class IntentRecognitionNodeHandler extends AbstractNodeHandler { } stringBuilder.append("Recognition parameters:\n").append(JSON.toJSONString(params)).append("\n"); stringBuilder.append("Intent options:\n").append(JSON.toJSONString(intentOptions)); - modelCallConfigDto.setSystemPrompt(stringBuilder.toString()); - modelCallConfigDto.setUserPrompt(""); + modelCallConfigDto.setSystemPrompt(StringUtils.isBlank(intentRecognitionNodeConfigDto.getSystemPrompt()) ? INTENT_RECOGNITION_SYSTEM_PROMPT : intentRecognitionNodeConfigDto.getSystemPrompt()); + modelCallConfigDto.setUserPrompt(stringBuilder.toString()); modelCallConfigDto.setChatRound(0); modelCallConfigDto.setStreamCall(true); modelCallConfigDto.setOutputType(OutputTypeEnum.JSON); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/KnowledgeInsertNodeHandler.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/KnowledgeInsertNodeHandler.java new file mode 100644 index 00000000..5e9bf367 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/KnowledgeInsertNodeHandler.java @@ -0,0 +1,56 @@ +package com.xspaceagi.agent.core.infra.component.workflow.handler; + +import com.xspaceagi.agent.core.adapter.dto.config.workflow.KnowledgeInsertNodeConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowNodeDto; +import com.xspaceagi.agent.core.infra.component.workflow.WorkflowContext; +import com.xspaceagi.knowledge.sdk.enums.SegmentEnum; +import com.xspaceagi.knowledge.sdk.vo.DocumentAddRequestVo; +import com.xspaceagi.system.application.dto.UserDto; +import com.xspaceagi.system.spec.common.RequestContext; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +public class KnowledgeInsertNodeHandler extends AbstractNodeHandler { + + @Override + protected Object executeNode(WorkflowContext workflowContext, WorkflowNodeDto node) { + KnowledgeInsertNodeConfigDto knowledgeInsertNodeConfigDto = (KnowledgeInsertNodeConfigDto) node.getNodeConfig(); + Map params = extraBindValueMap(workflowContext, node, knowledgeInsertNodeConfigDto.getInputArgs()); + DocumentAddRequestVo documentAddRequestVo = new DocumentAddRequestVo(); + documentAddRequestVo.setKbId(knowledgeInsertNodeConfigDto.getKnowledgeBaseId()); + documentAddRequestVo.setName(params.get("title") == null ? "" : params.get("title").toString()); + documentAddRequestVo.setFileContent(params.get("content") == null ? "" : params.get("content").toString()); + documentAddRequestVo.setDocUrl(params.get("docUrl") == null ? "" : params.get("docUrl").toString()); + try { + documentAddRequestVo.setSegment(params.get("segment") == null ? SegmentEnum.WORDS : SegmentEnum.valueOf(params.get("segment").toString())); + documentAddRequestVo.setWords(params.get("words") == null ? null : Integer.parseInt(params.get("words").toString())); + } catch (Exception e) { + documentAddRequestVo.setSegment(SegmentEnum.WORDS); + documentAddRequestVo.setWords(800); + } + if (StringUtils.isBlank(documentAddRequestVo.getFileContent()) && StringUtils.isBlank(documentAddRequestVo.getDocUrl())) { + throw new IllegalArgumentException("Choose either the document content or the document URL"); + } + Long docId; + boolean isInRequestContext = RequestContext.get() != null; + try { + UserDto user = workflowContext.getAgentContext().getUser(); + if (!isInRequestContext) { + RequestContext.setThreadTenantId(user.getTenantId()); + } + documentAddRequestVo.setUserId(user.getId()); + documentAddRequestVo.setUserName(user.getUserName()); + docId = workflowContext.getWorkflowContextServiceHolder().getKnowledgeBaseSearcher().documentAdd(documentAddRequestVo); + } finally { + if (!isInRequestContext) { + RequestContext.remove(); + } + } + Map output = new HashMap<>(); + output.put("success", true); + output.put("docId", docId); + return output; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/StartNodeHandler.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/StartNodeHandler.java index 07b772fe..caa53c0d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/StartNodeHandler.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/StartNodeHandler.java @@ -19,6 +19,9 @@ public class StartNodeHandler extends AbstractNodeHandler { Map startNodeOutput = new HashMap<>(); startNodeOutput.putAll(workflowContext.getAgentContext().getVariableParams()); List inputArgs = node.getNodeConfig().getInputArgs(); + if (inputArgs == null) { + inputArgs = new ArrayList<>(); + } Map inputMap = new LinkedHashMap<>(); for (Arg inputArg : inputArgs) { if (StringUtils.isNotBlank(workflowContext.getAgentContext().getMessage()) && inputArg.getName().equals(SystemArgNameEnum.AGENT_USER_MSG.name())) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/WrappedNodeHandler.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/WrappedNodeHandler.java index 67fdd781..ca3c4a24 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/WrappedNodeHandler.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/component/workflow/handler/WrappedNodeHandler.java @@ -26,7 +26,8 @@ public class WrappedNodeHandler implements NodeHandler { public Mono execute(WorkflowContext workflowContext, WorkflowNodeDto node) { try { if (workflowContext.isUseResultCache()) { - Object value = workflowContext.getWorkflowContextServiceHolder().getRedisUtil().get(generateKey(workflowContext, node)); + workflowContext.restoreResultStorage(); + Object value = workflowContext.getResultStorage().get(generateKey(workflowContext, node)); if (value != null && JSON.isValid(value.toString())) { NodeResult nodeResult = JSON.parseObject(value.toString(), NodeResult.class); if (nodeResult != null) { @@ -51,7 +52,9 @@ public class WrappedNodeHandler implements NodeHandler { nodeResult.setResult(result); nodeResult.setInput(workflowContext.getNodeExecuteInputMap().get(node.getId().toString())); nodeResult.setUnreachableNextNodeIds(node.getUnreachableNextNodeIds()); - workflowContext.getWorkflowContextServiceHolder().getRedisUtil().set(generateKey(workflowContext, node), JSONObject.toJSONString(nodeResult), 86400); + workflowContext.getResultStorage().put(generateKey(workflowContext, node), JSONObject.toJSONString(nodeResult)); + } else { + workflowContext.saveResultStorage(); } } catch (Exception e) { log.error("redis set error", e); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/ResourceGroupMapper.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/ResourceGroupMapper.java new file mode 100644 index 00000000..a2485801 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/ResourceGroupMapper.java @@ -0,0 +1,9 @@ +package com.xspaceagi.agent.core.infra.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroup; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ResourceGroupMapper extends BaseMapper { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/ResourceGroupRelationMapper.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/ResourceGroupRelationMapper.java new file mode 100644 index 00000000..af9cbc6c --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/ResourceGroupRelationMapper.java @@ -0,0 +1,9 @@ +package com.xspaceagi.agent.core.infra.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ResourceGroupRelationMapper extends BaseMapper { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/TargetRecommendMapper.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/TargetRecommendMapper.java new file mode 100644 index 00000000..8c85f744 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/TargetRecommendMapper.java @@ -0,0 +1,9 @@ +package com.xspaceagi.agent.core.infra.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TargetRecommendMapper extends BaseMapper { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/proxy/ComputerProxyRequestHandler.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/proxy/ComputerProxyRequestHandler.java index e0124076..d16b93cb 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/proxy/ComputerProxyRequestHandler.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/proxy/ComputerProxyRequestHandler.java @@ -4,13 +4,17 @@ import com.xspaceagi.agent.core.adapter.dto.ConversationDto; import com.xspaceagi.agent.core.infra.rpc.SandboxServerConfigService; import com.xspaceagi.agent.core.infra.rpc.UserShareRpcService; import com.xspaceagi.agent.core.infra.rpc.dto.SandboxServerConfig; +import com.xspaceagi.custompage.sdk.ICustomPageRpcService; +import com.xspaceagi.custompage.sdk.dto.CustomPageDto; import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum; import com.xspaceagi.system.application.dto.TenantConfigDto; import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.service.AuthService; import com.xspaceagi.system.application.service.TenantConfigApplicationService; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; import com.xspaceagi.system.sdk.service.dto.UserShareDto; import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.tenant.thread.TenantFunctions; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -42,11 +46,14 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { private static final Pattern DESKTOP_PATTERN = Pattern.compile("/computer/desktop/(\\d+)/(.*)"); private static final Pattern AUDIO_PATTERN = Pattern.compile("/computer/audio/(\\d+)/(.*)"); private static final Pattern IME_PATTERN = Pattern.compile("/computer/ime/(\\d+)/(.*)"); + private static final Pattern TERMINAL_PATTERN = Pattern.compile("/computer/terminal/(\\d+)/(.*)"); private final SandboxServerConfigService sandboxServerConfigService; private final UserShareRpcService userShareRpcService; private final AuthService authService; private final TenantConfigApplicationService tenantConfigApplicationService; + private final SpacePermissionService spacePermissionService; + private final ICustomPageRpcService iCustomPageRpcService; private final Bootstrap httpClientBootstrap; private final Bootstrap httpsClientBootstrap; private final int pendingMax; @@ -63,6 +70,8 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { UserShareRpcService userShareRpcService, AuthService authService, TenantConfigApplicationService tenantConfigApplicationService, + ICustomPageRpcService iCustomPageRpcService, + SpacePermissionService spacePermissionService, Bootstrap httpClientBootstrap, Bootstrap httpsClientBootstrap, int pendingMax) { @@ -70,6 +79,8 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { this.userShareRpcService = userShareRpcService; this.authService = authService; this.tenantConfigApplicationService = tenantConfigApplicationService; + this.iCustomPageRpcService = iCustomPageRpcService; + this.spacePermissionService = spacePermissionService; this.httpClientBootstrap = httpClientBootstrap; this.httpsClientBootstrap = httpsClientBootstrap; this.pendingMax = pendingMax; @@ -86,35 +97,43 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { writeError(ctx, HttpResponseStatus.BAD_REQUEST, "cId is missing or format error"); return; } - + Long tenantId; + Long computerUserId = null; + Long projectUserId = null; + Long projectSpaceId = null; + Long projectSandboxId = null; + String targetUrl = null; // Get target server information based on cId - SandboxServerConfig.SandboxServer sandboxServer; + SandboxServerConfig.SandboxServer sandboxServer = null; try { sandboxServer = sandboxServerConfigService.selectServer(cId); + targetUrl = sandboxServer.getServerVncUrl(); + ConversationDto currentConversation = sandboxServer.getCurrentConversation(); + if (currentConversation == null) { + ReferenceCountUtil.release(msg); + writeError(ctx, HttpResponseStatus.BAD_GATEWAY, "Current session not bound to sandbox server"); + return; + } + // Get creator ID of current session + computerUserId = currentConversation.getUserId(); + + // Set RequestContext, used for multi-tenant user_share table query + tenantId = currentConversation.getTenantId(); } catch (Exception e) { - log.warn("Failed to get sandbox server, cId={}", cId, e); - ReferenceCountUtil.release(msg); - writeError(ctx, HttpResponseStatus.BAD_GATEWAY, "Failed to get sandbox server: " + e.getMessage()); - return; + //兜底检查是不是网页应用 + CustomPageDto customPageDto = TenantFunctions.callWithIgnoreCheck(() -> iCustomPageRpcService.queryDetail(cId)); + if (customPageDto == null) { + log.warn("Failed to get sandbox server, cId={}", cId, e); + ReferenceCountUtil.release(msg); + writeError(ctx, HttpResponseStatus.BAD_REQUEST, "Failed to get sandbox server: " + e.getMessage()); + return; + } + tenantId = customPageDto.getTenantId(); + projectSpaceId = customPageDto.getSpaceId(); + projectUserId = customPageDto.getCreatorId(); + projectSandboxId = customPageDto.getSandboxId(); } - String targetUrl = sandboxServer.getServerVncUrl(); - if (StringUtils.isBlank(targetUrl)) { - ReferenceCountUtil.release(msg); - writeError(ctx, HttpResponseStatus.BAD_GATEWAY, "Sandbox server VNC address not configured"); - return; - } - ConversationDto currentConversation = sandboxServer.getCurrentConversation(); - if (currentConversation == null) { - ReferenceCountUtil.release(msg); - writeError(ctx, HttpResponseStatus.BAD_GATEWAY, "Current session not bound to sandbox server"); - return; - } - // Get creator ID of current session - Long computerUserId = currentConversation.getUserId(); - - // Set RequestContext, used for multi-tenant user_share table query - Long tenantId = currentConversation.getTenantId(); RequestContext requestContext = null; try { if (tenantId != null) { @@ -125,6 +144,24 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { RequestContext.set(requestContext); } + if (computerUserId == null) { + if (projectSandboxId == null) { + sandboxServer = new SandboxServerConfig.SandboxServer(); + sandboxServer.setScope(SandboxScopeEnum.GLOBAL); + targetUrl = sandboxServerConfigService.getDefaultPageAppVncUrl(); + } else { + sandboxServer = sandboxServerConfigService.selectServer((TenantConfigDto) RequestContext.get().getTenantConfig(), projectUserId, projectSandboxId.toString()); + targetUrl = sandboxServer.getServerVncUrl(); + } + computerUserId = projectUserId; + } + + if (StringUtils.isBlank(targetUrl)) { + ReferenceCountUtil.release(msg); + writeError(ctx, HttpResponseStatus.BAD_REQUEST, "Sandbox server VNC address not configured"); + return; + } + // Get shareKey from URL parameter or Cookie String shareKey = getShareKeyFromUri(uri); String skCookieKey = "vnc_sk_" + computerUserId + "="; @@ -146,12 +183,17 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { ctx.channel().attr(ComputerProxyServerContainer.SHARE_EXPIRE).set(userShare.getExpire()); ctx.channel().attr(ComputerProxyServerContainer.SK_COOKIE_KEY).set(skCookieKey); } else {// Non-shared link - if (!allow(computerUserId, request, ctx)) { + if (!allow(computerUserId, projectSpaceId, request, ctx)) { ReferenceCountUtil.release(msg); writeError(ctx, HttpResponseStatus.FORBIDDEN, "No permission to access current user resource"); return; } } + } catch (Exception e) { + log.warn("Failed to get sandbox server, cId={}", cId, e); + ReferenceCountUtil.release(msg); + writeError(ctx, HttpResponseStatus.BAD_REQUEST, "Failed to get sandbox server: " + e.getMessage()); + return; } finally { // Clean up RequestContext if (requestContext != null) { @@ -169,7 +211,7 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { } this.targetOrigin = ComputerProxyServerContainer.buildOrigin(targetScheme, this.targetHost, this.targetPort); - uri = rewriteUri(uri, computerUserId, sandboxServer); + uri = rewriteUri(uri, computerUserId, sandboxServer, projectSpaceId != null); request.setUri(uri); request.headers().set(HttpHeaderNames.HOST, this.targetHost + ":" + this.targetPort); @@ -325,6 +367,15 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { return null; } } + matcher = TERMINAL_PATTERN.matcher(uri); + if (matcher.matches()) { + String cId = matcher.group(1); + try { + return Long.valueOf(cId); + } catch (Exception e) { + return null; + } + } return null; } @@ -334,7 +385,7 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { * /computer/audio/{cId}/xxx -> /computer/audio/{userId}/{cId}/xxx * /computer/ime/{cId}/xxx -> /computer/ime/{userId}/{cId}/xxx */ - private String rewriteUri(String uri, Long userId, SandboxServerConfig.SandboxServer sandboxServer) { + private String rewriteUri(String uri, Long userId, SandboxServerConfig.SandboxServer sandboxServer, boolean isPageApp) { // Handle desktop path Matcher matcher = DESKTOP_PATTERN.matcher(uri); if (matcher.matches()) { @@ -378,13 +429,26 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { log.info("Computer proxy rewrite uri: {} -> {}", uri, newUri); return newUri; } + + matcher = TERMINAL_PATTERN.matcher(uri); + if (matcher.matches()) { + String cId = matcher.group(1); + String newUri; + if (isPageApp) { + newUri = "/web/ttyd/" + userId + "/" + cId + "/ws"; + } else { + newUri = "/computer/ttyd/" + userId + "/" + cId + "/ws"; + } + log.info("Computer proxy rewrite uri: {} -> {}", uri, newUri); + return newUri; + } return uri; } /** * User permission validation */ - private boolean allow(Long vncUserId, HttpRequest request, ChannelHandlerContext ctx) { + private boolean allow(Long vncUserId, Long projectSpaceId, HttpRequest request, ChannelHandlerContext ctx) { if (vncUserId == null) { log.warn("Failed to get VNC userId"); return false; @@ -394,7 +458,17 @@ public class ComputerProxyRequestHandler extends ChannelInboundHandlerAdapter { log.info("Failed to get login userId. vncUserId={}", vncUserId); return false; } - boolean allow = vncUserId.equals(loginUserId); + boolean allow; + if (projectSpaceId != null) { + try { + spacePermissionService.checkSpaceUserPermission(projectSpaceId, loginUserId); + allow = true; + } catch (Exception e) { + allow = false; + } + } else { + allow = vncUserId.equals(loginUserId); + } log.info("Computer proxy allow={}: {} -> {}", allow, vncUserId, loginUserId); return allow; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/proxy/ComputerProxyServerContainer.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/proxy/ComputerProxyServerContainer.java index b661c64f..8462875f 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/proxy/ComputerProxyServerContainer.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/proxy/ComputerProxyServerContainer.java @@ -2,23 +2,26 @@ package com.xspaceagi.agent.core.infra.proxy; import com.xspaceagi.agent.core.infra.rpc.SandboxServerConfigService; import com.xspaceagi.agent.core.infra.rpc.UserShareRpcService; +import com.xspaceagi.custompage.sdk.ICustomPageRpcService; +import com.xspaceagi.system.application.service.AuthService; import com.xspaceagi.system.application.service.TenantConfigApplicationService; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpServerCodec; -import io.netty.handler.timeout.IdleStateHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.AttributeKey; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; @@ -27,7 +30,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import com.xspaceagi.system.application.service.AuthService; import javax.net.ssl.SSLException; @@ -69,6 +71,12 @@ public class ComputerProxyServerContainer { @Resource private TenantConfigApplicationService tenantConfigApplicationService; + @Resource + private ICustomPageRpcService iCustomPageRpcService; + + @Resource + private SpacePermissionService spacePermissionService; + // 连接超时(毫秒),超过该时间还未连上目标,就判定超时失败并返回 502,不再等待 private int connectTimeoutMs = 5000; @@ -101,7 +109,7 @@ public class ComputerProxyServerContainer { pipeline.addLast("httpServerCodec", new HttpServerCodec()); pipeline.addLast("computerProxyRequestHandler", new ComputerProxyRequestHandler(sandboxServerConfigService, userShareRpcService, authService, - tenantConfigApplicationService, httpClientBootstrap, httpsClientBootstrap, pendingMax)); + tenantConfigApplicationService, iCustomPageRpcService, spacePermissionService, httpClientBootstrap, httpsClientBootstrap, pendingMax)); } }); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/ResourceGroupRelationRepositoryImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/ResourceGroupRelationRepositoryImpl.java new file mode 100644 index 00000000..a9cec6b0 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/ResourceGroupRelationRepositoryImpl.java @@ -0,0 +1,11 @@ +package com.xspaceagi.agent.core.infra.repository; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xspaceagi.agent.core.adapter.repository.ResourceGroupRelationRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; +import com.xspaceagi.agent.core.infra.dao.mapper.ResourceGroupRelationMapper; +import org.springframework.stereotype.Service; + +@Service +public class ResourceGroupRelationRepositoryImpl extends ServiceImpl implements ResourceGroupRelationRepository { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/ResourceGroupRepositoryImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/ResourceGroupRepositoryImpl.java new file mode 100644 index 00000000..97a9fc1b --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/ResourceGroupRepositoryImpl.java @@ -0,0 +1,11 @@ +package com.xspaceagi.agent.core.infra.repository; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xspaceagi.agent.core.adapter.repository.ResourceGroupRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroup; +import com.xspaceagi.agent.core.infra.dao.mapper.ResourceGroupMapper; +import org.springframework.stereotype.Service; + +@Service +public class ResourceGroupRepositoryImpl extends ServiceImpl implements ResourceGroupRepository { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/TargetRecommendRepositoryImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/TargetRecommendRepositoryImpl.java new file mode 100644 index 00000000..75d2beca --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/TargetRecommendRepositoryImpl.java @@ -0,0 +1,11 @@ +package com.xspaceagi.agent.core.infra.repository; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xspaceagi.agent.core.adapter.repository.TargetRecommendRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; +import com.xspaceagi.agent.core.infra.dao.mapper.TargetRecommendMapper; +import org.springframework.stereotype.Service; + +@Service +public class TargetRecommendRepositoryImpl extends ServiceImpl implements TargetRecommendRepository { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ComputerFileClient.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ComputerFileClient.java index 64d8a33c..83aa69b9 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ComputerFileClient.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ComputerFileClient.java @@ -61,13 +61,18 @@ public class ComputerFileClient { /** * 获取文件列表 + * + * @param customTargetDir 目标目录,非空时直接作为查询目录,为空则由服务端按 proxyPath 处理 */ - public Map getFileList(Long userId, Long cId, String proxyPath) { - String url = UriComponentsBuilder.fromHttpUrl(getBaseUrl(cId) + "/computer/get-file-list") + public Map getFileList(Long userId, Long cId, String proxyPath, String customTargetDir) { + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(getBaseUrl(cId) + "/computer/get-file-list") .queryParam("userId", userId) .queryParam("cId", cId) - .queryParam("proxyPath", proxyPath) - .toUriString(); + .queryParam("proxyPath", proxyPath); + if (customTargetDir != null && !customTargetDir.isBlank()) { + builder.queryParam("customTargetDir", customTargetDir); + } + String url = builder.toUriString(); log.info("[computer-client] userId={} cId={} 调用获取文件列表接口, url={}", userId, cId, url); RestTemplate restTemplate = new RestTemplate(); @@ -84,15 +89,30 @@ public class ComputerFileClient { } /** - * 获取静态文件(流式返回) + * 构建静态文件 URL + * 始终用 targetPrefix + relativePath 拼接路径,customTargetDir 非空时作为 query param 传给 file-server */ - public Flux getStaticFile(Long cId, String targetPrefix, String relativePath, String logId) { - // 使用 UriComponentsBuilder 来正确处理路径编码 - String[] prefixSegments = Arrays.stream(targetPrefix.split("/")).filter(segment -> !segment.isEmpty()).toArray(String[]::new); - String[] relativeSegments = Arrays.stream(relativePath.split("/")).filter(segment -> !segment.isEmpty()).toArray(String[]::new); + private String buildStaticFileUrl(Long cId, String targetPrefix, String relativePath, String customTargetDir) { + String base = getBaseUrl(cId); + String[] prefixSegments = Arrays.stream(targetPrefix.split("/")).filter(s -> !s.isEmpty()).toArray(String[]::new); + String[] relativeSegments = Arrays.stream(relativePath.split("/")).filter(s -> !s.isEmpty()).toArray(String[]::new); + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(base) + .pathSegment(prefixSegments) + .pathSegment(relativeSegments); + if (customTargetDir != null && !customTargetDir.isBlank()) { + builder.queryParam("customTargetDir", customTargetDir); + } + return builder.toUriString(); + } - String url = UriComponentsBuilder.fromHttpUrl(getBaseUrl(cId)).pathSegment(prefixSegments).pathSegment(relativeSegments).toUriString(); - log.info("[computer-client] logId={} 调用获取静态文件接口, url={}, targetPrefix={}, relativePath={}", logId, url, targetPrefix, relativePath); + /** + * 获取静态文件(流式返回) + * + * @param customTargetDir 目标目录,非空时直接使用,为空则用 targetPrefix + relativePath + */ + public Flux getStaticFile(Long cId, String targetPrefix, String relativePath, String logId, String customTargetDir) { + String url = buildStaticFileUrl(cId, targetPrefix, relativePath, customTargetDir); + log.info("[computer-client] logId={} 调用获取静态文件接口, url={}, targetPrefix={}, relativePath={}, customTargetDir={}", logId, url, targetPrefix, relativePath, customTargetDir); return webClient.get() .uri(url) @@ -110,13 +130,12 @@ public class ComputerFileClient { /** * 获取静态文件(流式返回,保留状态码与响应头,支持断点续传) + * + * @param customTargetDir 目标目录,非空时直接使用,为空则用 targetPrefix + relativePath */ - public ResponseEntity> getStaticFile(Long cId, String targetPrefix, String relativePath, String logId, String rangeHeader) { - String[] prefixSegments = Arrays.stream(targetPrefix.split("/")).filter(segment -> !segment.isEmpty()).toArray(String[]::new); - String[] relativeSegments = Arrays.stream(relativePath.split("/")).filter(segment -> !segment.isEmpty()).toArray(String[]::new); - - String url = UriComponentsBuilder.fromHttpUrl(getBaseUrl(cId)).pathSegment(prefixSegments).pathSegment(relativeSegments).toUriString(); - log.info("[computer-client] logId={} 调用获取静态文件接口(含响应头), url={}, targetPrefix={}, relativePath={}, range={}", logId, url, targetPrefix, relativePath, rangeHeader); + public ResponseEntity> getStaticFile(Long cId, String targetPrefix, String relativePath, String logId, String rangeHeader, String customTargetDir) { + String url = buildStaticFileUrl(cId, targetPrefix, relativePath, customTargetDir); + log.info("[computer-client] logId={} 调用获取静态文件接口(含响应头), url={}, targetPrefix={}, relativePath={}, range={}, customTargetDir={}", logId, url, targetPrefix, relativePath, rangeHeader, customTargetDir); try { ResponseEntity> response = webClient.get() @@ -151,7 +170,7 @@ public class ComputerFileClient { public byte[] getStaticFileAsBytes(Long cId, String staticPrefix, String relativePath, String logId) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { - getStaticFile(cId, staticPrefix, relativePath, logId).doOnNext(dataBuffer -> { + getStaticFile(cId, staticPrefix, relativePath, logId, null).doOnNext(dataBuffer -> { try { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); @@ -175,7 +194,7 @@ public class ComputerFileClient { /** * 更新文件列表 */ - public Map filesUpdate(Long userId, Long cId, List files) { + public Map filesUpdate(Long userId, Long cId, List files, String customTargetDir) { String url = getBaseUrl(cId) + "/computer/files-update"; log.info("[computer-client] userId={} cId={} 更新文件列表, url={}, filesCount={}", userId, cId, url, files != null ? files.size() : 0); @@ -183,6 +202,9 @@ public class ComputerFileClient { requestBody.put("userId", userId); requestBody.put("cId", cId); requestBody.put("files", files); + if (customTargetDir != null && !customTargetDir.isBlank()) { + requestBody.put("customTargetDir", customTargetDir); + } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -204,7 +226,7 @@ public class ComputerFileClient { /** * 上传文件 */ - public Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file) { + public Map uploadFile(Long userId, Long cId, String filePath, MultipartFile file, String customTargetDir) { String url = getBaseUrl(cId) + "/computer/upload-file"; log.info("[computer-client] userId={} cId={} 上传用户文件, url={}, filePath={}", userId, cId, url, filePath); @@ -218,6 +240,9 @@ public class ComputerFileClient { body.add("userId", String.valueOf(userId)); body.add("cId", String.valueOf(cId)); body.add("filePath", filePath); + if (customTargetDir != null && !customTargetDir.isBlank()) { + body.add("customTargetDir", customTargetDir); + } HttpEntity> requestEntity = new HttpEntity<>(body, headers); @@ -237,7 +262,7 @@ public class ComputerFileClient { /** * 批量上传文件 */ - public Map uploadFiles(Long userId, Long cId, List filePaths, List files) { + public Map uploadFiles(Long userId, Long cId, List filePaths, List files, String customTargetDir) { String url = getBaseUrl(cId) + "/computer/upload-files"; log.info("[computer-client] userId={} cId={} 批量上传用户文件, url={}, fileCount={}, filePathsCount={}", userId, cId, url, files != null ? files.size() : 0, filePaths != null ? filePaths.size() : 0); @@ -248,6 +273,9 @@ public class ComputerFileClient { LinkedMultiValueMap body = new LinkedMultiValueMap<>(); body.add("userId", String.valueOf(userId)); body.add("cId", String.valueOf(cId)); + if (customTargetDir != null && !customTargetDir.isBlank()) { + body.add("customTargetDir", customTargetDir); + } // 确保 filePaths 始终作为数组传递,即使只有一个值 // 使用循环 add 方法确保每个值都作为数组元素传递 @@ -277,15 +305,53 @@ public class ComputerFileClient { } } + /** + * 导入项目(zip 替换工作空间) + */ + public Map importProject(Long userId, Long cId, MultipartFile file, String customTargetDir) { + String url = getBaseUrl(cId) + "/computer/import-project"; + log.info("[computer-client] userId={} cId={} 导入项目, url={}, fileName={}", userId, cId, url, file.getOriginalFilename()); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + LinkedMultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", file.getResource()); + body.add("userId", String.valueOf(userId)); + body.add("cId", String.valueOf(cId)); + if (customTargetDir != null && !customTargetDir.isBlank()) { + body.add("customTargetDir", customTargetDir); + } + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, + new ParameterizedTypeReference>() { + }); + Map responseBody = entity.getBody(); + log.info("[computer-client] userId={} cId={} 导入项目, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[computer-client] userId={} cId={} 调用导入项目接口失败, status={}, responseBody={}", userId, cId, + e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + /** * 下载全部文件(zip 流式返回) */ - public Flux downloadAllFiles(Long userId, Long cId, String logId) { - String url = UriComponentsBuilder + public Flux downloadAllFiles(Long userId, Long cId, String logId, String customTargetDir) { + UriComponentsBuilder builder = UriComponentsBuilder .fromHttpUrl(getBaseUrl(cId) + "/computer/download-all-files") .queryParam("userId", userId) - .queryParam("cId", cId) - .toUriString(); + .queryParam("cId", cId); + if (customTargetDir != null && !customTargetDir.isBlank()) { + builder.queryParam("customTargetDir", customTargetDir); + } + String url = builder.toUriString(); log.info("[computer-client] logId={} 调用下载全部文件接口, url={}", logId, url); return webClient.get() @@ -306,6 +372,30 @@ public class ComputerFileClient { }); } + /** + * 获取沙盒日志 + */ + public Map getLogs(Long userId, Long cId, int tailLines) { + String url = UriComponentsBuilder.fromHttpUrl(getBaseUrl(cId) + "/computer/get-logs") + .queryParam("userId", userId) + .queryParam("cId", cId) + .queryParam("tailLines", tailLines) + .toUriString(); + log.info("[computer-client] userId={} cId={} 获取沙盒日志, url={}", userId, cId, url); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference>() { + }); + Map body = entity.getBody(); + log.info("[computer-client] userId={} cId={} 获取沙盒日志, 已响应", userId, cId); + return body; + } catch (HttpClientErrorException e) { + log.warn("[computer-client] userId={} cId={} 获取沙盒日志失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + // 捕获4xx错误,尝试解析响应体 private Map parseClientErr(String logId, HttpClientErrorException e) { Map resultMap = new HashMap<>(); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ComputerPodClient.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ComputerPodClient.java index 7117c3db..0abf3aee 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ComputerPodClient.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ComputerPodClient.java @@ -1,12 +1,21 @@ package com.xspaceagi.agent.core.infra.rpc; import com.fasterxml.jackson.databind.ObjectMapper; +import com.xspaceagi.agent.core.adapter.application.ConversationApplicationService; +import com.xspaceagi.agent.core.adapter.dto.ConversationDto; import com.xspaceagi.agent.core.infra.rpc.dto.SandboxServerConfig; +import com.xspaceagi.custompage.sdk.ICustomPageRpcService; +import com.xspaceagi.custompage.sdk.dto.CustomPageDto; +import com.xspaceagi.sandbox.sdk.server.ISandboxConfigRpcService; +import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigRpcDto; +import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigValue; import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.*; import org.springframework.stereotype.Component; @@ -23,19 +32,75 @@ public class ComputerPodClient { @Resource private SandboxServerConfigService sandboxServerConfigService; + @Resource + private ConversationApplicationService conversationApplicationService; + + @Resource + private ICustomPageRpcService iCustomPageRpcService; + + @Resource + private ISandboxConfigRpcService iSandboxConfigRpcService; + + @Resource + private SpacePermissionService spacePermissionService; + + @Value("${custom-page.ai-agent.base-url:}") + private String configuredBaseUrl; + /** * 启动容器 */ public Map ensurePod(Long cId, Long userId) { - SandboxServerConfig.SandboxServer sandboxServer = selectSandboxServer(cId); - String url = sandboxServer.getServerAgentUrl() + "/computer/pod/ensure"; + ConversationDto conversationByCid = conversationApplicationService.getConversationByCid(cId); + String baseUrl; + SandboxServerConfig.SandboxServer sandboxServer; + Map extraBody = new HashMap<>(); + if (conversationByCid == null) { + // 同一个ID兼容适配网页应用 + CustomPageDto customPageDto = iCustomPageRpcService.queryDetail(cId); + if (customPageDto == null) { + throw new BizException("Incomplete data"); + } + spacePermissionService.checkSpaceUserPermission(customPageDto.getSpaceId()); + if (customPageDto.getSandboxId() == null) { + baseUrl = configuredBaseUrl; + sandboxServer = new SandboxServerConfig.SandboxServer(); + sandboxServer.setServerAgentUrl(baseUrl); + } else { + try { + SandboxConfigRpcDto sandboxConfigRpcDto = iSandboxConfigRpcService.selectAppDevelopmentSandbox(customPageDto.getTenantId(), userId, customPageDto.getSpaceId(), customPageDto.getProjectId(), customPageDto.getSandboxId()); + SandboxConfigValue configValue = sandboxConfigRpcDto.getConfigValue(); + baseUrl = configValue.getHostWithScheme() + ":" + configValue.getAgentPort(); + sandboxServer = new SandboxServerConfig.SandboxServer(); + sandboxServer.setServerAgentUrl(baseUrl); + String isolationType = sandboxConfigRpcDto.getIsolation() == null ? null : sandboxConfigRpcDto.getIsolation().name(); + String podId = sandboxConfigRpcDto.getIsolationKey(); + extraBody.put("tenant_id", customPageDto.getTenantId()); + extraBody.put("space_id", customPageDto.getSpaceId()); + if (isolationType != null) { + extraBody.put("isolation_type", isolationType); + } + if (podId != null) { + extraBody.put("pod_id", podId); + } + } catch (Exception e) { + log.warn("[workspace-client] selectAppDevelopmentSandbox failed cId={}", cId, e); + throw new BizException("The sandbox bound to the project does not exist or has been taken offline."); + } + } + extraBody.put("service_type", "web-agent-runner"); + } else { + sandboxServer = selectSandboxServer(cId); + baseUrl = sandboxServer.getServerAgentUrl(); + } + String url = baseUrl + "/computer/pod/ensure"; log.info("[ComputerPodClient] ensurePod userId={} cId={} , url={}", userId, cId, url); Map body = new HashMap<>(); body.put("project_id", String.valueOf(cId)); body.put("user_id", String.valueOf(userId)); body.put("resource_limits", null); - + body.putAll(extraBody); return doPost(url, body, sandboxServer, userId + "_" + cId + "_ensure"); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/CustomPageRpcService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/CustomPageRpcService.java index 5a97a849..3ae6842c 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/CustomPageRpcService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/CustomPageRpcService.java @@ -1,20 +1,18 @@ package com.xspaceagi.agent.core.infra.rpc; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Service; - import com.xspaceagi.agent.core.adapter.dto.config.Arg; import com.xspaceagi.agent.core.adapter.dto.config.PageArgConfig; import com.xspaceagi.agent.core.infra.rpc.dto.PageDto; import com.xspaceagi.agent.core.spec.enums.DataTypeEnum; import com.xspaceagi.custompage.sdk.ICustomPageRpcService; import com.xspaceagi.custompage.sdk.dto.CustomPageDto; - import jakarta.annotation.Resource; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; @Service public class CustomPageRpcService { @@ -85,7 +83,7 @@ public class CustomPageRpcService { pageDto.setPageArgConfigs(new ArrayList<>()); } List homeIndex = pageDto.getPageArgConfigs().stream().filter(arg -> arg.getPageUri().equals("/") || arg.getPageUri().equals("/index.html")).collect(Collectors.toList()); - if (homeIndex.size() == 0) { + if (homeIndex.isEmpty()) { PageArgConfig pageArgConfig = new PageArgConfig(); pageArgConfig.setBasePath(pageDto.getBasePath()); pageArgConfig.setPageId(customPageDto.getProjectId()); @@ -172,7 +170,7 @@ public class CustomPageRpcService { pageDto.setPageArgConfigs(new ArrayList<>()); } List homeIndex = pageDto.getPageArgConfigs().stream().filter(arg -> arg.getPageUri().equals("/") || arg.getPageUri().equals("/index.html")).collect(Collectors.toList()); - if (homeIndex.size() == 0) { + if (homeIndex.isEmpty()) { PageArgConfig pageArgConfig = new PageArgConfig(); pageArgConfig.setBasePath(pageDto.getBasePath()); pageArgConfig.setPageId(customPageDto.getProjectId()); @@ -191,4 +189,8 @@ public class CustomPageRpcService { }); return pageDto; } + + public void bindDataSource(Long userId, Long projectId, String type, Long dataSourceId) { + iCustomPageRpcService.bindDataSource(userId, projectId, type, dataSourceId); + } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/GitRpcClient.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/GitRpcClient.java new file mode 100644 index 00000000..b662ab27 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/GitRpcClient.java @@ -0,0 +1,714 @@ +package com.xspaceagi.agent.core.infra.rpc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xspaceagi.agent.core.infra.rpc.dto.SandboxServerConfig; +import com.xspaceagi.custompage.sdk.ICustomPageRpcService; +import com.xspaceagi.custompage.sdk.dto.CustomPageDto; +import com.xspaceagi.sandbox.sdk.server.ISandboxConfigRpcService; +import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigRpcDto; +import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigValue; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class GitRpcClient { + + @Value("${custom-page.build-server.base-url:}") + private String configuredBaseUrl; + + @Resource + private SandboxServerConfigService sandboxServerConfigService; + @Resource + private ISandboxConfigRpcService iSandboxConfigRpcService; + @Resource + private ICustomPageRpcService iCustomPageRpcService; + + // ======================== 1. init ======================== + + public Map initTaskAgent(Long cId, Long userId) { + Map params = new HashMap<>(); + params.put("workspaceType", "taskAgent"); + params.put("cId", String.valueOf(cId)); + params.put("userId", String.valueOf(userId)); + return doGet(getBaseUrlForTaskAgent(cId), "/git/init", params); + } + + public Map initPageApp(Long projectId) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map params = new HashMap<>(); + params.put("workspaceType", "pageApp"); + params.put("projectId", String.valueOf(projectId)); + params.put("tenantId", String.valueOf(ctx.tenantId)); + params.put("spaceId", String.valueOf(ctx.spaceId)); + if (ctx.isolationType != null) { + params.put("isolationType", ctx.isolationType); + } + return doGet(ctx.baseUrl, "/git/init", params); + } + + // ======================== 2. status ======================== + + public Map statusTaskAgent(Long cId, Long userId) { + Map params = new HashMap<>(); + params.put("workspaceType", "taskAgent"); + params.put("cId", String.valueOf(cId)); + params.put("userId", String.valueOf(userId)); + return doGet(getBaseUrlForTaskAgent(cId), "/git/status", params); + } + + public Map statusPageApp(Long projectId) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map params = new HashMap<>(); + params.put("workspaceType", "pageApp"); + params.put("projectId", String.valueOf(projectId)); + params.put("tenantId", String.valueOf(ctx.tenantId)); + params.put("spaceId", String.valueOf(ctx.spaceId)); + if (ctx.isolationType != null) { + params.put("isolationType", ctx.isolationType); + } + return doGet(ctx.baseUrl, "/git/status", params); + } + + // ======================== 3. commit ======================== + + public Map commitTaskAgent(Long cId, Long userId, String message, + List files, String authorName, String authorEmail) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("message", message); + body.put("files", files); + body.put("authorName", authorName); + body.put("authorEmail", authorEmail); + return doPost(getBaseUrlForTaskAgent(cId), "/git/commit", body); + } + + public Map commitPageApp(Long projectId, String message, + List files, String authorName, String authorEmail) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("message", message); + body.put("files", files); + body.put("authorName", authorName); + body.put("authorEmail", authorEmail); + return doPost(ctx.baseUrl, "/git/commit", body); + } + + // ======================== 4. add ======================== + + public Map addTaskAgent(Long cId, Long userId, List files) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("files", files); + return doPost(getBaseUrlForTaskAgent(cId), "/git/add", body); + } + + public Map addPageApp(Long projectId, List files) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("files", files); + return doPost(ctx.baseUrl, "/git/add", body); + } + + // ======================== 5. unstage ======================== + + public Map unstageTaskAgent(Long cId, Long userId, List files) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("files", files); + return doPost(getBaseUrlForTaskAgent(cId), "/git/unstage", body); + } + + public Map unstagePageApp(Long projectId, List files) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("files", files); + return doPost(ctx.baseUrl, "/git/unstage", body); + } + + // ======================== 5. discard ======================== + + public Map discardTaskAgent(Long cId, Long userId, List files) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("files", files); + return doPost(getBaseUrlForTaskAgent(cId), "/git/discard", body); + } + + public Map discardPageApp(Long projectId, List files) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("files", files); + return doPost(ctx.baseUrl, "/git/discard", body); + } + + // ======================== 6. log ======================== + + public Map logTaskAgent(Long cId, Long userId, Integer page, Integer pageSize, String branch, String filePath) { + int ps = (pageSize != null && pageSize > 0) ? pageSize : 50; + int pg = (page != null && page > 0) ? page : 1; + int skip = (pg - 1) * ps; + + Map params = new HashMap<>(); + params.put("workspaceType", "taskAgent"); + params.put("cId", String.valueOf(cId)); + params.put("userId", String.valueOf(userId)); + params.put("maxCount", String.valueOf(ps)); + if (skip > 0) { + params.put("skip", String.valueOf(skip)); + } + if (branch != null) { + params.put("branch", branch); + } + if (filePath != null) { + params.put("filePath", filePath); + } + return doGet(getBaseUrlForTaskAgent(cId), "/git/log", params); + } + + public Map logPageApp(Long projectId, Integer page, Integer pageSize, String branch, String filePath) { + int ps = (pageSize != null && pageSize > 0) ? pageSize : 50; + int pg = (page != null && page > 0) ? page : 1; + int skip = (pg - 1) * ps; + + PageAppContext ctx = resolvePageAppContext(projectId); + Map params = new HashMap<>(); + params.put("workspaceType", "pageApp"); + params.put("projectId", String.valueOf(projectId)); + params.put("tenantId", String.valueOf(ctx.tenantId)); + params.put("spaceId", String.valueOf(ctx.spaceId)); + if (ctx.isolationType != null) { + params.put("isolationType", ctx.isolationType); + } + params.put("maxCount", String.valueOf(ps)); + if (skip > 0) { + params.put("skip", String.valueOf(skip)); + } + if (branch != null) { + params.put("branch", branch); + } + if (filePath != null) { + params.put("filePath", filePath); + } + return doGet(ctx.baseUrl, "/git/log", params); + } + + // ======================== 7. diff ======================== + + public Map diffTaskAgent(Long cId, Long userId, String source, String from, String to, List paths) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("source", source); + body.put("from", from); + body.put("to", to); + body.put("paths", paths); + return doPost(getBaseUrlForTaskAgent(cId), "/git/diff", body); + } + + public Map diffPageApp(Long projectId, String source, String from, String to, List paths) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("source", source); + body.put("from", from); + body.put("to", to); + body.put("paths", paths); + return doPost(ctx.baseUrl, "/git/diff", body); + } + + // ======================== 7.5 file-content ======================== + + public Map fileContentTaskAgent(Long cId, Long userId, String ref, String filePath) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("ref", ref); + body.put("filePath", filePath); + return doPost(getBaseUrlForTaskAgent(cId), "/git/file-content", body); + } + + public Map fileContentPageApp(Long projectId, String ref, String filePath) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("ref", ref); + body.put("filePath", filePath); + return doPost(ctx.baseUrl, "/git/file-content", body); + } + + // ======================== 8. reset ======================== + + public Map resetTaskAgent(Long cId, Long userId, String target, String mode) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("target", target); + body.put("mode", mode); + return doPost(getBaseUrlForTaskAgent(cId), "/git/reset", body); + } + + public Map resetPageApp(Long projectId, String target, String mode) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("target", target); + body.put("mode", mode); + return doPost(ctx.baseUrl, "/git/reset", body); + } + + // ======================== 9. checkout ======================== + + public Map checkoutTaskAgent(Long cId, Long userId, String target) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("target", target); + return doPost(getBaseUrlForTaskAgent(cId), "/git/checkout", body); + } + + public Map checkoutPageApp(Long projectId, String target) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("target", target); + return doPost(ctx.baseUrl, "/git/checkout", body); + } + + // ======================== 10. revert ======================== + + public Map revertTaskAgent(Long cId, Long userId, String target, String message, + String authorName, String authorEmail) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("target", target); + body.put("message", message); + body.put("authorName", authorName); + body.put("authorEmail", authorEmail); + return doPost(getBaseUrlForTaskAgent(cId), "/git/revert", body); + } + + public Map revertPageApp(Long projectId, String target, String message, + String authorName, String authorEmail) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("target", target); + body.put("message", message); + body.put("authorName", authorName); + body.put("authorEmail", authorEmail); + return doPost(ctx.baseUrl, "/git/revert", body); + } + + // ======================== 11. tags ======================== + + public Map tagsTaskAgent(Long cId, Long userId) { + Map params = new HashMap<>(); + params.put("workspaceType", "taskAgent"); + params.put("cId", String.valueOf(cId)); + params.put("userId", String.valueOf(userId)); + return doGet(getBaseUrlForTaskAgent(cId), "/git/tags", params); + } + + public Map tagsPageApp(Long projectId) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map params = new HashMap<>(); + params.put("workspaceType", "pageApp"); + params.put("projectId", String.valueOf(projectId)); + params.put("tenantId", String.valueOf(ctx.tenantId)); + params.put("spaceId", String.valueOf(ctx.spaceId)); + if (ctx.isolationType != null) { + params.put("isolationType", ctx.isolationType); + } + return doGet(ctx.baseUrl, "/git/tags", params); + } + + // ======================== 12. tag-create ======================== + + public Map tagCreateTaskAgent(Long cId, Long userId, String tagName, String message) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("tagName", tagName); + body.put("message", message); + return doPost(getBaseUrlForTaskAgent(cId), "/git/tag-create", body); + } + + public Map tagCreatePageApp(Long projectId, String tagName, String message) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("tagName", tagName); + body.put("message", message); + return doPost(ctx.baseUrl, "/git/tag-create", body); + } + + // ======================== 13. tag-delete ======================== + + public Map tagDeleteTaskAgent(Long cId, Long userId, String tagName) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("tagName", tagName); + return doPost(getBaseUrlForTaskAgent(cId), "/git/tag-delete", body); + } + + public Map tagDeletePageApp(Long projectId, String tagName) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("tagName", tagName); + return doPost(ctx.baseUrl, "/git/tag-delete", body); + } + + // ======================== 14. branches ======================== + + public Map branchesTaskAgent(Long cId, Long userId) { + Map params = new HashMap<>(); + params.put("workspaceType", "taskAgent"); + params.put("cId", String.valueOf(cId)); + params.put("userId", String.valueOf(userId)); + return doGet(getBaseUrlForTaskAgent(cId), "/git/branches", params); + } + + public Map branchesPageApp(Long projectId) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map params = new HashMap<>(); + params.put("workspaceType", "pageApp"); + params.put("projectId", String.valueOf(projectId)); + params.put("tenantId", String.valueOf(ctx.tenantId)); + params.put("spaceId", String.valueOf(ctx.spaceId)); + if (ctx.isolationType != null) { + params.put("isolationType", ctx.isolationType); + } + return doGet(ctx.baseUrl, "/git/branches", params); + } + + // ======================== 15. branch-create ======================== + + public Map branchCreateTaskAgent(Long cId, Long userId, String branchName, String startPoint) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("branchName", branchName); + body.put("startPoint", startPoint); + return doPost(getBaseUrlForTaskAgent(cId), "/git/branch-create", body); + } + + public Map branchCreatePageApp(Long projectId, String branchName, String startPoint) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("branchName", branchName); + body.put("startPoint", startPoint); + return doPost(ctx.baseUrl, "/git/branch-create", body); + } + + // ======================== 16. branch-switch ======================== + + public Map branchSwitchTaskAgent(Long cId, Long userId, String branchName) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("branchName", branchName); + return doPost(getBaseUrlForTaskAgent(cId), "/git/branch-switch", body); + } + + public Map branchSwitchPageApp(Long projectId, String branchName) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("branchName", branchName); + return doPost(ctx.baseUrl, "/git/branch-switch", body); + } + + // ======================== 17. branch-delete ======================== + + public Map branchDeleteTaskAgent(Long cId, Long userId, String branchName, Boolean force) { + Map body = new HashMap<>(); + body.put("workspaceType", "taskAgent"); + body.put("cId", cId); + body.put("userId", userId); + body.put("branchName", branchName); + if (force != null) { + body.put("force", force); + } + return doPost(getBaseUrlForTaskAgent(cId), "/git/branch-delete", body); + } + + public Map branchDeletePageApp(Long projectId, String branchName, Boolean force) { + PageAppContext ctx = resolvePageAppContext(projectId); + Map body = new HashMap<>(); + body.put("workspaceType", "pageApp"); + body.put("projectId", projectId); + body.put("tenantId", ctx.tenantId); + body.put("spaceId", ctx.spaceId); + if (ctx.isolationType != null) { + body.put("isolationType", ctx.isolationType); + } + body.put("branchName", branchName); + if (force != null) { + body.put("force", force); + } + return doPost(ctx.baseUrl, "/git/branch-delete", body); + } + + // ======================== 内部类 ======================== + + private static class PageAppContext { + final String baseUrl; + final Long tenantId; + final Long spaceId; + final String isolationType; + + PageAppContext(String baseUrl, Long tenantId, Long spaceId, String isolationType) { + this.baseUrl = baseUrl; + this.tenantId = tenantId; + this.spaceId = spaceId; + this.isolationType = isolationType; + } + } + + // ======================== taskAgent URL 解析 ======================== + + private String getBaseUrlForTaskAgent(Long cId) { + SandboxServerConfig.SandboxServer sandboxServer; + try { + sandboxServer = sandboxServerConfigService.selectServer(cId); + } catch (Exception e) { + log.warn("[GitRpcClient] selectServer failed cId={}", cId, e); + throw BizException.of(BizExceptionCodeEnum.agentDependencyServiceError); + } + if (sandboxServer == null) { + throw BizException.of(BizExceptionCodeEnum.agentSandboxNotFound); + } + return sandboxServer.getServerFileUrl() + "/api"; + } + + // ======================== pageApp URL 解析 ======================== + + private PageAppContext resolvePageAppContext(Long projectId) { + CustomPageDto pageDto = iCustomPageRpcService.queryDetail(projectId); + if (pageDto == null) { + throw BizException.of(BizExceptionCodeEnum.customPageProjectConfigNotFound); + } + Long spaceId = pageDto.getSpaceId(); + Long sandboxId = pageDto.getSandboxId(); + Long tenantId = pageDto.getTenantId(); + + RequestContext requestContext = RequestContext.get(); + Long userId = requestContext == null ? null : requestContext.getUserId(); + if (tenantId == null && requestContext != null) { + tenantId = requestContext.getTenantId(); + } + String baseUrl = null; + String isolationType = null; + + if (sandboxId != null) { + SandboxConfigRpcDto sandboxConfig = iSandboxConfigRpcService.selectAppDevelopmentSandbox(tenantId, userId, spaceId, projectId, sandboxId); + if (sandboxConfig == null || sandboxConfig.getConfigValue() == null) { + throw BizException.of(BizExceptionCodeEnum.agentSandboxNotFound); + } + SandboxConfigValue configValue = sandboxConfig.getConfigValue(); + if (configValue.getHostWithScheme() == null || configValue.getHostWithScheme().isBlank() + || configValue.getFileServerPort() <= 0) { + throw BizException.of(BizExceptionCodeEnum.agentSandboxConfigError); + } + baseUrl = configValue.getHostWithScheme() + ":" + configValue.getFileServerPort() + "/api"; + isolationType = sandboxConfig.getIsolation() == null ? null : sandboxConfig.getIsolation().name(); + } + if (baseUrl == null || baseUrl.isBlank()) { + baseUrl = configuredBaseUrl; + } + + return new PageAppContext(baseUrl, tenantId, spaceId, isolationType); + } + + // ======================== HTTP 辅助方法 ======================== + + private Map doGet(String baseUrl, String path, Map params) { + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl + path); + if (params != null) { + params.forEach(builder::queryParam); + } + String url = builder.toUriString(); + log.info("[git-client] GET url={}", url); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange( + url, HttpMethod.GET, null, new ParameterizedTypeReference<>() { + }); + return entity.getBody(); + } catch (HttpClientErrorException e) { + log.warn("[git-client] GET failed, status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(path, e); + } + } + + private Map doPost(String baseUrl, String path, Map body) { + String url = baseUrl + path; + log.info("[git-client] POST url={}", url); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange( + url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<>() { + }); + return entity.getBody(); + } catch (HttpClientErrorException e) { + log.warn("[git-client] POST failed, status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(path, e); + } + } + + private Map parseClientErr(String logId, HttpClientErrorException e) { + Map resultMap = new HashMap<>(); + resultMap.put("success", false); + resultMap.put("message", e.getMessage()); + try { + String responseBody = e.getResponseBodyAsString(); + if (responseBody != null && !responseBody.isEmpty()) { + ObjectMapper objectMapper = new ObjectMapper(); + @SuppressWarnings("unchecked") + Map errorResponse = objectMapper.readValue(responseBody, Map.class); + if (errorResponse.containsKey("code")) { + resultMap.put("code", errorResponse.get("code")); + } + if (errorResponse.containsKey("message")) { + resultMap.put("message", errorResponse.get("message")); + } else if (errorResponse.containsKey("error")) { + Object errorObj = errorResponse.get("error"); + if (errorObj instanceof Map) { + @SuppressWarnings("unchecked") + Map errorMap = (Map) errorObj; + if (errorMap.containsKey("message")) { + resultMap.put("message", errorMap.get("message")); + } + } + } + } + } catch (Exception parseException) { + log.error("[git-client] logId={} 解析错误响应体失败", logId, parseException); + } + return resultMap; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/MarketClientRpcService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/MarketClientRpcService.java index 28e6ff75..a5b34c2a 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/MarketClientRpcService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/MarketClientRpcService.java @@ -5,25 +5,34 @@ import com.xspaceagi.eco.market.sdk.request.ClientSecretRequest; import com.xspaceagi.eco.market.sdk.service.IEcoMarketRpcService; import com.xspaceagi.system.spec.cache.SimpleJvmHashCache; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +@Slf4j @Service public class MarketClientRpcService { @Resource private IEcoMarketRpcService ecoMarketRpcService; - public ClientSecretResponse queryClientSecret(ClientSecretRequest request) { Object clientSecret = SimpleJvmHashCache.getHash("client_secret", request.getTenantId().toString()); if (clientSecret != null) { return (ClientSecretResponse) clientSecret; } - ClientSecretResponse clientSecretResponse = ecoMarketRpcService.queryClientSecret(request); - if (clientSecretResponse != null){ + ClientSecretResponse clientSecretResponse = null; + try { + clientSecretResponse = ecoMarketRpcService.queryClientSecret(request); + } catch (Exception e) { + log.warn("Exception while querying client secret", e); + return null; + } + if (clientSecretResponse != null) { SimpleJvmHashCache.putHash("client_secret", request.getTenantId().toString(), clientSecretResponse, 7200); return clientSecretResponse; } - return null; + clientSecretResponse = new ClientSecretResponse(); + clientSecretResponse.setClientSecret("Unregistered"); + return clientSecretResponse; } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ResourcePricingRpcService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ResourcePricingRpcService.java index 71c8c3f4..8e8f1fee 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ResourcePricingRpcService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/ResourcePricingRpcService.java @@ -1,6 +1,8 @@ package com.xspaceagi.agent.core.infra.rpc; +import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.workflow.*; +import com.xspaceagi.agent.core.adapter.repository.entity.AgentComponentConfig; import com.xspaceagi.agent.core.adapter.repository.entity.Published; import com.xspaceagi.agent.core.adapter.repository.entity.WorkflowNodeConfig; import com.xspaceagi.pricing.sdk.dto.*; @@ -88,11 +90,40 @@ public class ResourcePricingRpcService { return iPricingRpcService.estimatePrice(tenantId, userId, estimateTargets); } + public void completeAgentEstimate(AgentConfigDto agentConfigDto, List estimateTargets, List estimateSkillTargets, Set workflowIds) { + agentConfigDto.getAgentComponentConfigList().forEach(componentConfigDto -> { + if (componentConfigDto.getType() == AgentComponentConfig.Type.Skill) { + estimateSkillTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.SKILL).targetId(componentConfigDto.getTargetId().toString()).build()); + } + if (componentConfigDto.getType() == AgentComponentConfig.Type.Plugin) { + estimateTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.PLUGIN).targetId(componentConfigDto.getTargetId().toString()).build()); + } + if (componentConfigDto.getType() == AgentComponentConfig.Type.Mcp) { + estimateTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.MCP).targetId(componentConfigDto.getTargetId().toString()).build()); + } + if (componentConfigDto.getType() == AgentComponentConfig.Type.Workflow) { + estimateTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.WORKFLOW).targetId(componentConfigDto.getTargetId().toString()).build()); + Object targetConfig = componentConfigDto.getTargetConfig(); + if (targetConfig instanceof WorkflowConfigDto) { + completeWorkflowEstimateTargets(estimateTargets, (WorkflowConfigDto) targetConfig, workflowIds, estimateSkillTargets); + } + } + }); + } + + private void completeWorkflowEstimateTargets(List estimateTargets, WorkflowConfigDto targetConfig, Set workflowIds, List estimateSkillTargets) { + targetConfig.getNodes().forEach(node -> completeWorkflowNodeEstimateTargets(estimateTargets, node, workflowIds, estimateSkillTargets)); + } + public void completeWorkflowEstimateTargets(List estimateTargets, WorkflowConfigDto targetConfig, Set workflowIds) { targetConfig.getNodes().forEach(node -> completeWorkflowNodeEstimateTargets(estimateTargets, node, workflowIds)); } public void completeWorkflowNodeEstimateTargets(List estimateTargets, WorkflowNodeDto node, Set workflowIds) { + completeWorkflowNodeEstimateTargets(estimateTargets, node, workflowIds, null); + } + + private void completeWorkflowNodeEstimateTargets(List estimateTargets, WorkflowNodeDto node, Set workflowIds, List estimateSkillTargets) { if (node.getType() == WorkflowNodeConfig.NodeType.Workflow) { WorkflowAsNodeConfigDto workflowAsNodeConfigDto = (WorkflowAsNodeConfigDto) node.getNodeConfig(); if (workflowAsNodeConfigDto.getWorkflowConfig() != null && !workflowIds.contains(workflowAsNodeConfigDto.getWorkflowId())) { @@ -119,5 +150,11 @@ public class ResourcePricingRpcService { }); } } + if (node.getType() == WorkflowNodeConfig.NodeType.Agent) { + AgentNodeConfigDto nodeConfigDto = (AgentNodeConfigDto) node.getNodeConfig(); + if (nodeConfigDto.getAgentConfigDto() != null) { + completeAgentEstimate(nodeConfigDto.getAgentConfigDto(), estimateTargets, estimateSkillTargets, workflowIds); + } + } } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/SandboxServerConfigService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/SandboxServerConfigService.java index e495fc1f..8a7b2193 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/SandboxServerConfigService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/SandboxServerConfigService.java @@ -23,9 +23,11 @@ import com.xspaceagi.system.spec.utils.RedisUtil; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import lombok.Data; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.Assert; @@ -66,6 +68,10 @@ public class SandboxServerConfigService extends AbstractTaskExecuteService { @Resource private ISandboxConfigRpcService iSandboxConfigRpcService; + @Getter + @Value("${custom-page.docker-proxy.base-url:}") + private String defaultPageAppVncUrl; + private final HttpClient httpClient = java.net.http.HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); private final Map> serverStatusMap = new ConcurrentHashMap<>(); @@ -83,7 +89,6 @@ public class SandboxServerConfigService extends AbstractTaskExecuteService { .build()); } - // Get sandbox server based on cId (automatically get tenantId from session) public SandboxServerConfig.SandboxServer selectServer(Long cid) { return TenantFunctions.callWithIgnoreCheck(() -> { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/SkillFileClient.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/SkillFileClient.java deleted file mode 100644 index 4d8d0fb6..00000000 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/SkillFileClient.java +++ /dev/null @@ -1,222 +0,0 @@ -package com.xspaceagi.agent.core.infra.rpc; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.xspaceagi.agent.core.infra.rpc.dto.SandboxServerConfig; -import com.xspaceagi.system.spec.exception.BizException; -import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.*; -import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.multipart.MultipartFile; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Slf4j -@Component -public class SkillFileClient { - - @Resource - private SandboxServerConfigService sandboxServerConfigService; - - private String getBaseUrl(Long cId) { - SandboxServerConfig.SandboxServer sandboxServer = null; - try { - sandboxServer = sandboxServerConfigService.selectServer(cId); - } catch (BizException e) { - log.warn("[Skill-client] selectServer failed cId={}", cId, e); - throw e; - } catch (Exception e) { - log.warn("[Skill-client] selectServer failed cId={}", cId, e); - throw new BizException(e.getMessage()); - } - if (sandboxServer == null) { - throw BizException.of(BizExceptionCodeEnum.agentSandboxNotFound); - } - String serverUrl = sandboxServer.getServerFileUrl(); - if (serverUrl == null) { - throw BizException.of(BizExceptionCodeEnum.agentSandboxNotFound); - } - return serverUrl + "/api"; - } - - /** - * 创建工作空间 - */ - public Map createWorkSpace(Long userId, Long cId, MultipartFile file) { - String url = getBaseUrl(cId) + "/computer/create-workspace"; - log.info("[Skill-client] userId={} cId={} , url={}", userId, cId, url); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - - // 创建MultiValueMap来存储文件和其他参数 - LinkedMultiValueMap body = new LinkedMultiValueMap<>(); - body.add("userId", String.valueOf(userId)); - body.add("cId", String.valueOf(cId)); - if (file != null) { - body.add("file", file.getResource()); - } - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - - RestTemplate restTemplate = new RestTemplate(); - try { - ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { - }); - Map responseBody = entity.getBody(); - log.info("[Skill-client] userId={} cId={} 创建工作空间, 响应结果={}", userId, cId, responseBody); - return responseBody; - } catch (HttpClientErrorException e) { - log.warn("[Skill-client] userId={} cId={} 创建工作空间失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); - return parseClientErr(userId + "_" + cId, e); - } - } - - /** - * 创建工作空间v2 - */ - public Map createWorkSpaceV2(Long userId, Long cId, MultipartFile file, List skillUrls) { - String url = getBaseUrl(cId) + "/computer/create-workspace-v2"; - log.info("[Skill-client] userId={} cId={} , url={}", userId, cId, url); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - - // 创建MultiValueMap来存储文件和其他参数 - LinkedMultiValueMap body = new LinkedMultiValueMap<>(); - body.add("userId", String.valueOf(userId)); - body.add("cId", String.valueOf(cId)); - if (file != null) { - body.add("file", file.getResource()); - } - if (skillUrls != null && !skillUrls.isEmpty()) { - body.add("skillUrls", skillUrls); - } - - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - - RestTemplate restTemplate = new RestTemplate(); - try { - ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { - }); - Map responseBody = entity.getBody(); - log.info("[Skill-client] userId={} cId={} 创建工作空间v2, 响应结果={}", userId, cId, responseBody); - return responseBody; - } catch (HttpClientErrorException e) { - log.warn("[Skill-client] userId={} cId={} 创建工作空间v2失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); - return parseClientErr(userId + "_" + cId, e); - } - } - - /** - * 推送skill到空间 - * zip 结构与 create-workspace 一致,包含 skills/ 目录 - */ - public Map pushSkillsToWorkspace(Long userId, Long cId, MultipartFile zipFile) { - if (zipFile == null) { - throw new IllegalArgumentException("No skill files to push"); - } - - String url = getBaseUrl(cId) + "/computer/push-skills-to-workspace"; - log.info("[Skill-client] userId={} cId={}, url={}", userId, cId, url); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - - LinkedMultiValueMap body = new LinkedMultiValueMap<>(); - body.add("userId", String.valueOf(userId)); - body.add("cId", String.valueOf(cId)); - body.add("file", zipFile.getResource()); - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - - RestTemplate restTemplate = new RestTemplate(); - try { - ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { - }); - Map responseBody = entity.getBody(); - log.info("[Skill-client] userId={} cId={} 推送技能文件, 响应结果={}", userId, cId, responseBody); - return responseBody; - } catch (HttpClientErrorException e) { - log.warn("[Skill-client] userId={} cId={} 调用推送技能文件失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); - return parseClientErr(userId + "_" + cId, e); - } - } - - /** - * 推送skill到空间 - * zip 结构与 create-workspace 一致,包含 skills/ 目录 - */ - public Map pushSkillsToWorkspaceV2(Long userId, Long cId, MultipartFile zipFile, List skillUrls) { - if (zipFile == null && (skillUrls == null || skillUrls.isEmpty())) { - throw new IllegalArgumentException("No skill files to push"); - } - - String url = getBaseUrl(cId) + "/computer/push-skills-to-workspace-v2"; - log.info("[Skill-client] userId={} cId={}, url={}", userId, cId, url); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - - LinkedMultiValueMap body = new LinkedMultiValueMap<>(); - body.add("userId", String.valueOf(userId)); - body.add("cId", String.valueOf(cId)); - if (zipFile != null) { - body.add("file", zipFile.getResource()); - } - if (skillUrls != null && !skillUrls.isEmpty()) { - body.add("skillUrls", skillUrls); - } - - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - - RestTemplate restTemplate = new RestTemplate(); - try { - ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { - }); - Map responseBody = entity.getBody(); - log.info("[Skill-client] userId={} cId={} 推送技能文件v2, 响应结果={}", userId, cId, responseBody); - return responseBody; - } catch (HttpClientErrorException e) { - log.warn("[Skill-client] userId={} cId={} 调用推送技能文件v2失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); - return parseClientErr(userId + "_" + cId, e); - } - } - - // 捕获4xx错误,尝试解析响应体 - private Map parseClientErr(String logId, HttpClientErrorException e) { - Map resultMap = new HashMap<>(); - resultMap.put("success", false); - resultMap.put("message", e.getMessage()); - try { - String responseBody = e.getResponseBodyAsString(); - if (responseBody != null && !responseBody.isEmpty()) { - ObjectMapper objectMapper = new ObjectMapper(); - @SuppressWarnings("unchecked") Map errorResponse = objectMapper.readValue(responseBody, Map.class); - if (errorResponse.containsKey("code")) { - resultMap.put("code", errorResponse.get("code")); - } - if (errorResponse.containsKey("message")) { - resultMap.put("message", errorResponse.get("message")); - } else if (errorResponse.containsKey("error")) { - Object errorObj = errorResponse.get("error"); - if (errorObj instanceof Map) { - @SuppressWarnings("unchecked") Map errorMap = (Map) errorObj; - if (errorMap.containsKey("message")) { - resultMap.put("message", errorMap.get("message")); - } - } - } - } - } catch (Exception parseException) { - log.error("[Skill-client] logId={} 解析错误响应体失败", logId, parseException); - } - return resultMap; - } - -} \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/WorkspaceRpcClient.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/WorkspaceRpcClient.java new file mode 100644 index 00000000..aea7024a --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/rpc/WorkspaceRpcClient.java @@ -0,0 +1,456 @@ +package com.xspaceagi.agent.core.infra.rpc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xspaceagi.agent.core.infra.rpc.dto.SandboxServerConfig; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class WorkspaceRpcClient { + + @Resource + private SandboxServerConfigService sandboxServerConfigService; + + private String getBaseUrl(Long cId) { + SandboxServerConfig.SandboxServer sandboxServer = null; + try { + sandboxServer = sandboxServerConfigService.selectServer(cId); + } catch (BizException e) { + log.warn("[workspace-client] selectServer failed cId={}", cId, e); + throw e; + } catch (Exception e) { + log.warn("[workspace-client] selectServer failed cId={}", cId, e); + throw new BizException(e.getMessage()); + } + if (sandboxServer == null) { + throw BizException.of(BizExceptionCodeEnum.agentSandboxNotFound); + } + String serverUrl = sandboxServer.getServerFileUrl(); + if (serverUrl == null) { + throw BizException.of(BizExceptionCodeEnum.agentSandboxNotFound); + } + return serverUrl + "/api"; + } + + /** + * 创建工作空间 + */ + public Map createWorkSpace(Long userId, Long cId, MultipartFile file) { + String url = getBaseUrl(cId) + "/computer/create-workspace"; + log.info("[workspace-client] userId={} cId={} , url={}", userId, cId, url); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + // 创建MultiValueMap来存储文件和其他参数 + LinkedMultiValueMap body = new LinkedMultiValueMap<>(); + body.add("userId", String.valueOf(userId)); + body.add("cId", String.valueOf(cId)); + if (file != null) { + body.add("file", file.getResource()); + } + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { + }); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 创建工作空间, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 创建工作空间失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + /** + * 创建工作空间v2 + */ + public Map createWorkSpaceV2(Long userId, Long cId, MultipartFile file, List skillUrls, String mcpServersConfig, String permissionsConfig, String hooksConfig, String hookScripts) { + String url = getBaseUrl(cId) + "/computer/create-workspace-v2"; + log.info("[workspace-client] userId={}, cId={}, url={}", userId, cId, url); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + // 创建MultiValueMap来存储文件和其他参数 + LinkedMultiValueMap body = new LinkedMultiValueMap<>(); + body.add("userId", String.valueOf(userId)); + body.add("cId", String.valueOf(cId)); + if (file != null) { + body.add("file", file.getResource()); + } + if (skillUrls != null && !skillUrls.isEmpty()) { + body.add("skillUrls", skillUrls); + } + if (mcpServersConfig != null && !mcpServersConfig.isBlank()) { + body.add("mcpServersConfig", mcpServersConfig); + } + if (permissionsConfig != null && !permissionsConfig.isBlank()) { + body.add("permissionsConfig", permissionsConfig); + } + if (hooksConfig != null && !hooksConfig.isBlank()) { + body.add("hooksConfig", hooksConfig); + } + if (hookScripts != null && !hookScripts.isBlank()) { + body.add("hookScripts", hookScripts); + } + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { + }); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 创建工作空间v2, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 创建工作空间v2失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + /** + * 推送skill到空间 + * zip 结构与 create-workspace 一致,包含 skills/ 目录 + */ + public Map pushSkillsToWorkspace(Long userId, Long cId, MultipartFile zipFile) { + if (zipFile == null) { + throw new IllegalArgumentException("No skill files to push"); + } + + String url = getBaseUrl(cId) + "/computer/push-skills-to-workspace"; + log.info("[workspace-client] userId={} cId={}, url={}", userId, cId, url); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + LinkedMultiValueMap body = new LinkedMultiValueMap<>(); + body.add("userId", String.valueOf(userId)); + body.add("cId", String.valueOf(cId)); + body.add("file", zipFile.getResource()); + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { + }); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 推送技能文件, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 调用推送技能文件失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + /** + * 推送skill到空间 + * zip 结构与 create-workspace 一致,包含 skills/ 目录 + */ + public Map pushSkillsToWorkspaceV2(Long userId, Long cId, MultipartFile zipFile, List skillUrls) { + if (zipFile == null && (skillUrls == null || skillUrls.isEmpty())) { + throw new IllegalArgumentException("No skill files to push"); + } + + String url = getBaseUrl(cId) + "/computer/push-skills-to-workspace-v2"; + log.info("[workspace-client] userId={} cId={}, url={}", userId, cId, url); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + LinkedMultiValueMap body = new LinkedMultiValueMap<>(); + body.add("userId", String.valueOf(userId)); + body.add("cId", String.valueOf(cId)); + if (zipFile != null) { + body.add("file", zipFile.getResource()); + } + if (skillUrls != null && !skillUrls.isEmpty()) { + body.add("skillUrls", skillUrls); + } + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { + }); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 推送技能文件v2, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 调用推送技能文件v2失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + /** + * 初始化项目模板 + */ + public Map initProjectTemplate(Long userId, Long cId, MultipartFile file, Boolean enableGit) { + String url = getBaseUrl(cId) + "/computer/init-project-template"; + log.info("[workspace-client] userId={} cId={}, url={}", userId, cId, url); + if (file == null) { + throw new IllegalArgumentException("No template file to init"); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + LinkedMultiValueMap body = new LinkedMultiValueMap<>(); + body.add("userId", String.valueOf(userId)); + body.add("cId", String.valueOf(cId)); + body.add("file", file.getResource()); + body.add("enableGit", String.valueOf(Boolean.TRUE.equals(enableGit))); + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { + }); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 初始化项目模板, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 初始化项目模板失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + /** + * 安装项目依赖 + */ + public Map installProject(Long userId, Long cId, String programmingLanguage) { + String url = getBaseUrl(cId) + "/computer/install-project"; + log.info("[workspace-client] userId={} cId={}, url={}, programmingLanguage={}", userId, cId, url, programmingLanguage); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map requestBody = new HashMap<>(); + requestBody.put("userId", userId); + requestBody.put("cId", cId); + requestBody.put("programmingLanguage", programmingLanguage); + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { + }); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 安装项目依赖, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 安装项目依赖失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + /** + * 在沙箱工作空间中执行命令 + */ + public Map executeCommand(Long userId, Long cId, String command) { + String url = getBaseUrl(cId) + "/computer/execute-command"; + log.info("[workspace-client] userId={} cId={}, url={}, command={}", userId, cId, url, command); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map requestBody = new HashMap<>(); + requestBody.put("userId", userId); + requestBody.put("cId", cId); + requestBody.put("command", command); + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { + }); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 执行命令完成, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 执行命令失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + /** + * 删除工作空间目录 + */ + public Map deleteWorkspace(Long userId, Long cId) { + String url = getBaseUrl(cId) + "/computer/delete-workspace"; + log.info("[workspace-client] userId={} cId={}, url={}", userId, cId, url); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map requestBody = new HashMap<>(); + requestBody.put("userId", userId); + requestBody.put("cId", cId); + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() { + }); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 删除工作空间, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 删除工作空间失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + /** + * 打包工作空间文件为 zip(二进制流) + * @param userId 用户ID + * @param cId 会话ID + * @param excludeDirs 排除目录列表,null 表示使用 file-server 端默认列表 + * @return zip 字节数组 + */ + public byte[] zipWorkspace(Long userId, Long cId, List excludeDirs) { + String url = getBaseUrl(cId) + "/computer/zip-workspace"; + log.info("[workspace-client] userId={} cId={}, url={}", userId, cId, url); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM)); + + Map requestBody = new HashMap<>(); + requestBody.put("userId", userId); + requestBody.put("cId", cId); + if (excludeDirs != null) { + requestBody.put("excludeDirs", excludeDirs); + } + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, byte[].class); + byte[] zipBytes = entity.getBody(); + int size = zipBytes != null ? zipBytes.length : 0; + log.info("[workspace-client] userId={} cId={} 打包工作空间完成, zipSize={}", userId, cId, size); + return zipBytes; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 打包工作空间失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + throw new BizException("Failed to zip workspace: " + e.getMessage()); + } + } + + /** + * 构建 agent 打包产物 + * @param userId 用户ID + * @param cId 会话ID + * @param agentId agent ID + * @param version 版本号 + * @return 响应体,包含 success 和 artifacts 数组 + */ + public Map buildAgentPackage(Long userId, Long cId, Long agentId, String version) { + String url = getBaseUrl(cId) + "/computer/build-agent-package"; + log.info("[workspace-client] userId={} cId={}, url={}, agentId={}, version={}", userId, cId, url, agentId, version); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map requestBody = new HashMap<>(); + requestBody.put("userId", userId); + requestBody.put("cId", cId); + requestBody.put("agentId", agentId); + requestBody.put("version", version); + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, + new ParameterizedTypeReference>() {}); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 构建 agent 产物完成, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 构建 agent 产物失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + /** + * 清理沙箱构建产物 + * @param userId 用户ID + * @param cId 会话ID + * @return 响应体,包含 success 和 cleaned + */ + public Map cleanupBuildArtifacts(Long userId, Long cId) { + String url = getBaseUrl(cId) + "/computer/cleanup-build-artifacts"; + log.info("[workspace-client] userId={} cId={}, url={}", userId, cId, url); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map requestBody = new HashMap<>(); + requestBody.put("userId", userId); + requestBody.put("cId", cId); + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + RestTemplate restTemplate = new RestTemplate(); + try { + ResponseEntity> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, + new ParameterizedTypeReference>() {}); + Map responseBody = entity.getBody(); + log.info("[workspace-client] userId={} cId={} 清理构建产物完成, 响应结果={}", userId, cId, responseBody); + return responseBody; + } catch (HttpClientErrorException e) { + log.warn("[workspace-client] userId={} cId={} 清理构建产物失败, status={}, responseBody={}", userId, cId, e.getStatusCode(), e.getResponseBodyAsString()); + return parseClientErr(userId + "_" + cId, e); + } + } + + // 捕获4xx错误,尝试解析响应体 + private Map parseClientErr(String logId, HttpClientErrorException e) { + Map resultMap = new HashMap<>(); + resultMap.put("success", false); + resultMap.put("message", e.getMessage()); + try { + String responseBody = e.getResponseBodyAsString(); + if (StringUtils.isNotBlank(responseBody)) { + ObjectMapper objectMapper = new ObjectMapper(); + @SuppressWarnings("unchecked") Map errorResponse = objectMapper.readValue(responseBody, Map.class); + if (errorResponse.containsKey("code")) { + resultMap.put("code", errorResponse.get("code")); + } + if (errorResponse.containsKey("message")) { + resultMap.put("message", errorResponse.get("message")); + } else if (errorResponse.containsKey("error")) { + Object errorObj = errorResponse.get("error"); + if (errorObj instanceof Map) { + @SuppressWarnings("unchecked") Map errorMap = (Map) errorObj; + if (errorMap.containsKey("message")) { + resultMap.put("message", errorMap.get("message")); + } + } + } + } + } catch (Exception parseException) { + log.error("[workspace-client] logId={} 解析错误响应体失败", logId, parseException); + } + return resultMap; + } + +} \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/ChatMessagePubSubService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/ChatMessagePubSubService.java new file mode 100644 index 00000000..031c7f94 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/ChatMessagePubSubService.java @@ -0,0 +1,151 @@ +package com.xspaceagi.agent.core.infra.sub; + +import com.alibaba.fastjson2.JSON; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +@Slf4j +@Service +public class ChatMessagePubSubService { + + @Resource + private RedisMessageListenerContainer container; + + private final Map>> listeners = new ConcurrentHashMap<>(); + + @Resource + private RedisTemplate redisTemplate; + + // channel -> 消息缓冲区 + private final Map> buffer = new ConcurrentHashMap<>(); + + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + private volatile boolean subscribed = false; + + @PostConstruct + public void init() { + // 每200ms批量发送 + scheduler.scheduleAtFixedRate(this::flushAll, 1000, 200, TimeUnit.MILLISECONDS); + } + + @PreDestroy + public void destroy() { + scheduler.shutdown(); + } + + /** + * 动态订阅频道 + */ + public void subscribe(String conversationId, Consumer onMessage) { + if (!subscribed) { + synchronized (this) { + if (!subscribed) { + MessageListener listener = (message, pattern) -> { + Object deserialize = redisTemplate.getValueSerializer().deserialize(message.getBody()); + String topic = new String(message.getChannel()); + String cId = topic.split(":")[1]; + if (listeners.containsKey(cId) && deserialize != null) { + List> consumers = new ArrayList<>(listeners.get(cId)); + for (Consumer consumer : consumers) { + consumer.accept(deserialize.toString()); + } + } + }; + container.addMessageListener(listener, new PatternTopic(chatChannel())); + subscribed = true; + } + } + } + listeners.computeIfAbsent(conversationId, k -> Collections.synchronizedList(new ArrayList<>())) + .add(onMessage); + } + + /** + * 取消订阅 + */ + public synchronized void unsubscribe(String conversationId, Consumer listener) { + List> consumers = listeners.get(conversationId); + if (consumers != null) { + consumers.remove(listener); + if (consumers.isEmpty()) { + listeners.remove(conversationId); + } + } + } + + /** + * 消息不直接发送,先入缓冲区 + */ + public void publish(String conversationId, String message) { + buffer.computeIfAbsent(buildChatChannel(conversationId), k -> Collections.synchronizedList(new ArrayList<>())) + .add(message); + } + + /** + * 发送完成,清空缓冲区 + */ + public void finishPublish(String conversationId) { + flush(buildChatChannel(conversationId)); + buffer.remove(buildChatChannel(conversationId)); + redisTemplate.delete(buildChatChannel(conversationId) + ":messages"); + } + + public List getMessages(String conversationId) { + return redisTemplate.opsForList().range(buildChatChannel(conversationId) + ":messages", 0, -1); + } + + /** + * 批量发送指定频道 + */ + private void flush(String channel) { + List messages = buffer.get(channel); + if (messages == null || messages.isEmpty()) return; + + synchronized (messages) { + if (messages.isEmpty()) return; + + // 合并为 JSON 数组,一次 publish 搞定 + try { + redisTemplate.convertAndSend(channel, JSON.toJSONString(messages)); + redisTemplate.opsForList().rightPushAll(channel + ":messages", messages); + redisTemplate.expire(channel + ":messages", 1, TimeUnit.HOURS); + messages.clear(); + } catch (Exception e) { + log.error("批量发送失败", e); + } + } + } + + private String chatChannel() { + return "topic-chat-v1:*"; + } + + private String buildChatChannel(String conversationId) { + return "topic-chat-v1:" + conversationId; + } + + /** + * 定期 flush 所有频道 + */ + private void flushAll() { + buffer.keySet().forEach(this::flush); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/RedisPubSubConfig.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/RedisPubSubConfig.java new file mode 100644 index 00000000..dc876bcf --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/RedisPubSubConfig.java @@ -0,0 +1,31 @@ +package com.xspaceagi.agent.core.infra.sub; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +@Configuration +public class RedisPubSubConfig { + + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer( + RedisConnectionFactory connectionFactory) { + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + + // 可选:自定义线程池 + AtomicInteger i = new AtomicInteger(); + container.setTaskExecutor(Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "redis-sub-" + i.getAndIncrement()); + t.setDaemon(true); + return t; + })); + + return container; + } +} \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/RedisPublisher.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/RedisPublisher.java new file mode 100644 index 00000000..f30075ad --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/sub/RedisPublisher.java @@ -0,0 +1,20 @@ +package com.xspaceagi.agent.core.infra.sub; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +public class RedisPublisher { + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 发布消息到指定频道 + */ + public void publish(String channel, String message) { + redisTemplate.convertAndSend(channel, message); + } + +} \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/ISkillRpcService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/ISkillRpcService.java new file mode 100644 index 00000000..c41b9e3e --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/ISkillRpcService.java @@ -0,0 +1,22 @@ +package com.xspaceagi.agent.core.sdk; + +import com.xspaceagi.agent.core.spec.enums.UsageScenarioEnum; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +public interface ISkillRpcService { + + /** + * 导入技能(支持文件或URL导入) + * + * @param url 技能文件URL(与file二选一) + * @param file 上传的技能文件(与url二选一) + * @param targetSkillId 目标技能ID(导入到已有技能),为null时创建新技能 + * @param targetSpaceId 目标空间ID(创建新技能时必填) + * @param usageScenarios 使用场景列表 + * @return 技能ID + */ + Long importSkill(String url, MultipartFile file, Long targetSkillId, Long targetSpaceId, List usageScenarios); + +} \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/dto/PluginExecuteRequestDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/dto/PluginExecuteRequestDto.java index 4b8ea770..1743f1e3 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/dto/PluginExecuteRequestDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/dto/PluginExecuteRequestDto.java @@ -10,9 +10,9 @@ import java.util.Map; @Data public class PluginExecuteRequestDto implements Serializable { private String requestId; - private String config; + private Object config; // 用于传递通用智能体的绑定参数配置 - private String bindConfig; + private Object bindConfig; private Long spaceId; private Long userId; private Object user; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/dto/WorkflowExecuteRequestDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/dto/WorkflowExecuteRequestDto.java index a666376d..762d8874 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/dto/WorkflowExecuteRequestDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-sdk/src/main/java/com/xspaceagi/agent/core/sdk/dto/WorkflowExecuteRequestDto.java @@ -17,9 +17,9 @@ public class WorkflowExecuteRequestDto implements Serializable { private Long workflowId; private Long spaceId; private Map params; - private String config; + private Object config; // 用于传递通用智能体的绑定参数配置 - private String bindConfig; + private Object bindConfig; @Schema(description = "调用轨迹") private TraceContext traceContext; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/constant/Prompts.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/constant/Prompts.java index 85abcb3d..27cbeea4 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/constant/Prompts.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/constant/Prompts.java @@ -330,10 +330,6 @@ public class Prompts { """; - public static final String CURRENT_TIME = """ - {time} - """; - // 个人电脑助理提示词 public static final String PERSONAL_COMPUTER_ASSISTANT_PROMPT = """ ## Role diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/FileTypeUtils.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/FileTypeUtils.java index c6373ad0..9683fe58 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/FileTypeUtils.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/FileTypeUtils.java @@ -43,7 +43,7 @@ public class FileTypeUtils { return new MediaType("application", "json", StandardCharsets.UTF_8); } else if (lowerFileName.endsWith(".xml")) { return new MediaType("application", "xml", StandardCharsets.UTF_8); - } else if (lowerFileName.endsWith(".txt")) { + } else if (lowerFileName.endsWith(".txt") || lowerFileName.endsWith(".text")) { return new MediaType("text", "plain", StandardCharsets.UTF_8); } else if (lowerFileName.endsWith(".md") || lowerFileName.endsWith(".markdown") @@ -51,6 +51,86 @@ public class FileTypeUtils { || lowerFileName.endsWith(".mkd")) { return new MediaType("text", "markdown", StandardCharsets.UTF_8); } + // 代码文件 + else if (lowerFileName.endsWith(".py")) { + return new MediaType("text", "x-python", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".java")) { + return new MediaType("text", "x-java-source", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".ts")) { + return new MediaType("text", "typescript", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".tsx")) { + return new MediaType("text", "tsx", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".jsx")) { + return new MediaType("text", "jsx", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".go")) { + return new MediaType("text", "x-go", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".rs")) { + return new MediaType("text", "rust", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".c") || lowerFileName.endsWith(".h")) { + return new MediaType("text", "x-c", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".cpp") || lowerFileName.endsWith(".hpp") + || lowerFileName.endsWith(".cc") || lowerFileName.endsWith(".cxx")) { + return new MediaType("text", "x-c++", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".cs")) { + return new MediaType("text", "x-csharp", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".php")) { + return new MediaType("text", "x-php", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".rb")) { + return new MediaType("text", "x-ruby", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".swift")) { + return new MediaType("text", "x-swift", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".kt") || lowerFileName.endsWith(".kts")) { + return new MediaType("text", "x-kotlin", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".scala") || lowerFileName.endsWith(".sc")) { + return new MediaType("text", "x-scala", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".clj")) { + return new MediaType("text", "x-clojure", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".lua")) { + return new MediaType("text", "x-lua", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".sql")) { + return new MediaType("application", "sql", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".sh") || lowerFileName.endsWith(".bash") + || lowerFileName.endsWith(".zsh")) { + return new MediaType("application", "x-sh", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".fish")) { + return new MediaType("application", "x-fish", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".ps1")) { + return new MediaType("text", "x-powershell", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".bat") || lowerFileName.endsWith(".cmd")) { + return new MediaType("application", "x-bat", StandardCharsets.UTF_8); + } + // 配置文件 + else if (lowerFileName.endsWith(".yaml") || lowerFileName.endsWith(".yml")) { + return new MediaType("application", "x-yaml", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".toml")) { + return new MediaType("application", "toml", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".csv")) { + return new MediaType("text", "csv", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".tsv")) { + return new MediaType("text", "tab-separated-values", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".ini") || lowerFileName.endsWith(".conf") + || lowerFileName.endsWith(".config") || lowerFileName.endsWith(".properties") + || lowerFileName.endsWith(".env") || lowerFileName.endsWith(".gitignore") + || lowerFileName.endsWith(".gitattributes") || lowerFileName.endsWith(".editorconfig") + || lowerFileName.endsWith(".log")) { + return new MediaType("text", "plain", StandardCharsets.UTF_8); + } + // 样式与文档标记 + else if (lowerFileName.endsWith(".xhtml")) { + return new MediaType("application", "xhtml+xml", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".scss") || lowerFileName.endsWith(".sass")) { + return new MediaType("text", "x-scss", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".less")) { + return new MediaType("text", "x-less", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".styl")) { + return new MediaType("text", "x-stylus", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".rst")) { + return new MediaType("text", "x-rst", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".adoc") || lowerFileName.endsWith(".asciidoc")) { + return new MediaType("text", "asciidoc", StandardCharsets.UTF_8); + } else if (lowerFileName.endsWith(".diff") || lowerFileName.endsWith(".patch")) { + return new MediaType("text", "x-diff", StandardCharsets.UTF_8); + } // 其他类型 else if (lowerFileName.endsWith(".pdf")) { return MediaType.APPLICATION_PDF; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/SvgIconUtil.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/SvgIconUtil.java new file mode 100644 index 00000000..f8212f41 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/SvgIconUtil.java @@ -0,0 +1,377 @@ +package com.xspaceagi.agent.core.spec.utils; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 与 {@code IconGenerator} 默认 logo 输出尺寸保持一致(200x200 PNG)。 + */ +public final class SvgIconUtil { + + public static final int ICON_SIZE = 200; + + public static final double VIEW_BOX_SIZE = 100D; + + public static final String DEFAULT_VIEW_BOX = "0 0 100 100"; + + private static final double CONTENT_PADDING = 4D; + + /** 内容已占画布该比例时,不再额外缩放,避免对已合理的 AI 输出二次处理。 */ + private static final double MIN_FILL_RATIO_FOR_SKIP = 0.68D; + + /** 缩放倍率接近 1 时不包裹 transform,避免无意义的结构改动。 */ + private static final double MIN_SCALE_TO_APPLY = 1.12D; + + private static final Pattern TAG_PATTERN = Pattern.compile("<(circle|rect|ellipse|line|polyline|polygon|path|text)\\b[^>]*>", Pattern.CASE_INSENSITIVE); + + private static final Pattern VIEW_BOX_PATTERN = Pattern.compile("viewBox\\s*=\\s*[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE); + + private static final Pattern BACKGROUND_RECT_PATTERN = Pattern.compile("]*>", Pattern.CASE_INSENSITIVE); + + private static final Pattern NUMBER_PATTERN = Pattern.compile("[-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?"); + + private SvgIconUtil() { + } + + public static String normalize(String svgIcon) { + if (StringUtils.isBlank(svgIcon)) { + return svgIcon; + } + String svg = stripMarkdownFence(svgIcon.trim()); + int start = svg.indexOf("', start); + int closeStart = svg.toLowerCase().lastIndexOf(""); + if (openEnd < 0 || closeStart < 0 || closeStart <= openEnd) { + return svg; + } + + String openTag = svg.substring(start, openEnd + 1); + String content = svg.substring(openEnd + 1, closeStart).trim(); + ViewBox viewBox = parseViewBox(openTag); + ContentParts parts = splitBackground(content, viewBox); + Bounds bounds = mapBoundsToTargetViewBox(estimateBounds(parts.foreground(), viewBox), viewBox); + + StringBuilder normalized = new StringBuilder(); + normalized.append(""); + if (StringUtils.isNotBlank(parts.background())) { + normalized.append(normalizeBackgroundRect(parts.background())); + } + if (StringUtils.isNotBlank(parts.foreground())) { + normalized.append(wrapForeground(parts.foreground(), viewBox, bounds)); + } + normalized.append(""); + return normalized.toString(); + } + + private static String wrapForeground(String foreground, ViewBox viewBox, Bounds bounds) { + String wrapped = foreground; + if (!isStandardViewBox(viewBox)) { + wrapped = "" + wrapped + ""; + } + if (shouldApplyFitTransform(bounds)) { + wrapped = "" + wrapped + ""; + } + return wrapped; + } + + private static boolean shouldApplyFitTransform(Bounds bounds) { + double fillRatio = Math.max(bounds.width(), bounds.height()) / VIEW_BOX_SIZE; + if (fillRatio >= MIN_FILL_RATIO_FOR_SKIP) { + return false; + } + double available = VIEW_BOX_SIZE - CONTENT_PADDING * 2; + double scale = Math.min(available / bounds.width(), available / bounds.height()); + return scale >= MIN_SCALE_TO_APPLY; + } + + private static boolean isStandardViewBox(ViewBox viewBox) { + return Math.abs(viewBox.minX()) < 0.001D + && Math.abs(viewBox.minY()) < 0.001D + && Math.abs(viewBox.width() - VIEW_BOX_SIZE) < 0.001D + && Math.abs(viewBox.height() - VIEW_BOX_SIZE) < 0.001D; + } + + private static String buildViewBoxTransform(ViewBox viewBox) { + double scale = VIEW_BOX_SIZE / Math.max(viewBox.width(), viewBox.height()); + return "translate(" + format(-viewBox.minX() * scale) + " " + format(-viewBox.minY() * scale) + ") scale(" + format(scale) + ")"; + } + + private static Bounds mapBoundsToTargetViewBox(Bounds bounds, ViewBox viewBox) { + if (isStandardViewBox(viewBox)) { + return bounds; + } + double scale = VIEW_BOX_SIZE / Math.max(viewBox.width(), viewBox.height()); + Bounds mapped = new Bounds(); + mapped.includePoint((bounds.minX() - viewBox.minX()) * scale, (bounds.minY() - viewBox.minY()) * scale); + mapped.includePoint((bounds.maxX() - viewBox.minX()) * scale, (bounds.maxY() - viewBox.minY()) * scale); + return mapped; + } + + private static String normalizeBackgroundRect(String rectTag) { + StringBuilder rect = new StringBuilder(""); + return rect.toString(); + } + + private static void copyAttr(StringBuilder target, String tag, String name) { + String value = getAttrRaw(tag, name); + if (StringUtils.isNotBlank(value)) { + target.append(' ').append(name).append("=\"").append(value).append('"'); + } + } + + private static ContentParts splitBackground(String content, ViewBox viewBox) { + Matcher matcher = BACKGROUND_RECT_PATTERN.matcher(content); + if (!matcher.find()) { + return new ContentParts(null, content); + } + String rectTag = matcher.group(); + double x = getAttr(rectTag, "x", -1); + double y = getAttr(rectTag, "y", -1); + double width = getAttr(rectTag, "width", 0); + double height = getAttr(rectTag, "height", 0); + boolean fullCanvasBackground = x <= 1 && y <= 1 + && width >= viewBox.width() * 0.9 + && height >= viewBox.height() * 0.9; + if (!fullCanvasBackground) { + return new ContentParts(null, content); + } + String foreground = (content.substring(0, matcher.start()) + content.substring(matcher.end())).trim(); + return new ContentParts(rectTag, foreground); + } + + private static String stripMarkdownFence(String svg) { + if (!svg.startsWith("```")) { + return svg; + } + return svg.replaceFirst("```(?:svg)?\\s*", "").replaceFirst("```\\s*$", "").trim(); + } + + private static ViewBox parseViewBox(String openTag) { + Matcher matcher = VIEW_BOX_PATTERN.matcher(openTag); + if (matcher.find()) { + String[] parts = matcher.group(1).trim().split("[\\s,]+"); + if (parts.length == 4) { + return new ViewBox( + Double.parseDouble(parts[0]), + Double.parseDouble(parts[1]), + Double.parseDouble(parts[2]), + Double.parseDouble(parts[3])); + } + } + double width = getAttr(openTag, "width", VIEW_BOX_SIZE); + double height = getAttr(openTag, "height", VIEW_BOX_SIZE); + return new ViewBox(0, 0, width, height); + } + + private static Bounds estimateBounds(String content, ViewBox viewBox) { + if (StringUtils.isBlank(content)) { + return Bounds.fromViewBox(viewBox); + } + Bounds bounds = new Bounds(); + Matcher matcher = TAG_PATTERN.matcher(content); + while (matcher.find()) { + String tag = matcher.group(); + if (isDecorativeElement(tag)) { + continue; + } + String type = matcher.group(1).toLowerCase(); + switch (type) { + case "circle" -> { + double cx = getAttr(tag, "cx", viewBox.centerX()); + double cy = getAttr(tag, "cy", viewBox.centerY()); + double r = getAttr(tag, "r", 0); + bounds.include(cx - r, cy - r, cx + r, cy + r); + } + case "ellipse" -> { + double cx = getAttr(tag, "cx", viewBox.centerX()); + double cy = getAttr(tag, "cy", viewBox.centerY()); + double rx = getAttr(tag, "rx", 0); + double ry = getAttr(tag, "ry", 0); + bounds.include(cx - rx, cy - ry, cx + rx, cy + ry); + } + case "rect" -> { + double x = getAttr(tag, "x", 0); + double y = getAttr(tag, "y", 0); + double width = getAttr(tag, "width", 0); + double height = getAttr(tag, "height", 0); + bounds.include(x, y, x + width, y + height); + } + case "line" -> { + bounds.includePoint(getAttr(tag, "x1", 0), getAttr(tag, "y1", 0)); + bounds.includePoint(getAttr(tag, "x2", 0), getAttr(tag, "y2", 0)); + } + case "polyline", "polygon" -> includePoints(bounds, getAttrRaw(tag, "points")); + case "path" -> includePathNumbers(bounds, getAttrRaw(tag, "d")); + case "text" -> bounds.includePoint(getAttr(tag, "x", viewBox.centerX()), getAttr(tag, "y", viewBox.centerY())); + default -> { + } + } + } + if (!bounds.isValid()) { + return Bounds.fromViewBox(viewBox); + } + return bounds; + } + + private static boolean isDecorativeElement(String tag) { + String opacityRaw = getAttrRaw(tag, "opacity"); + if (StringUtils.isBlank(opacityRaw)) { + opacityRaw = getAttrRaw(tag, "fill-opacity"); + } + if (StringUtils.isBlank(opacityRaw)) { + return false; + } + try { + return Double.parseDouble(opacityRaw) < 0.5D; + } catch (NumberFormatException ignored) { + return false; + } + } + + private static void includePoints(Bounds bounds, String points) { + if (StringUtils.isBlank(points)) { + return; + } + String[] parts = points.trim().split("[\\s,]+"); + for (int i = 0; i + 1 < parts.length; i += 2) { + bounds.includePoint(Double.parseDouble(parts[i]), Double.parseDouble(parts[i + 1])); + } + } + + private static void includePathNumbers(Bounds bounds, String pathData) { + if (StringUtils.isBlank(pathData)) { + return; + } + List numbers = new ArrayList<>(); + Matcher matcher = NUMBER_PATTERN.matcher(pathData); + while (matcher.find()) { + numbers.add(Double.parseDouble(matcher.group())); + } + for (int i = 0; i + 1 < numbers.size(); i += 2) { + bounds.includePoint(numbers.get(i), numbers.get(i + 1)); + } + } + + private static String buildFitTransform(Bounds bounds) { + double available = VIEW_BOX_SIZE - CONTENT_PADDING * 2; + double scale = Math.min(available / bounds.width(), available / bounds.height()); + return "translate(" + format(VIEW_BOX_SIZE / 2) + " " + format(VIEW_BOX_SIZE / 2) + ") " + + "scale(" + format(scale) + ") " + + "translate(" + format(-bounds.centerX()) + " " + format(-bounds.centerY()) + ")"; + } + + private static double getAttr(String tag, String name, double defaultValue) { + String raw = getAttrRaw(tag, name); + if (StringUtils.isBlank(raw)) { + return defaultValue; + } + return Double.parseDouble(raw); + } + + private static String getAttrRaw(String tag, String name) { + Matcher matcher = Pattern.compile("\\b" + name + "\\s*=\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE).matcher(tag); + if (matcher.find()) { + return matcher.group(1); + } + matcher = Pattern.compile("\\b" + name + "\\s*=\\s*'([^']+)'", Pattern.CASE_INSENSITIVE).matcher(tag); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + private static String format(double value) { + if (Math.rint(value) == value) { + return String.valueOf((long) value); + } + return String.format("%.4f", value).replaceAll("0+$", "").replaceAll("\\.$", ""); + } + + private record ContentParts(String background, String foreground) { + } + + private record ViewBox(double minX, double minY, double width, double height) { + double centerX() { + return minX + width / 2; + } + + double centerY() { + return minY + height / 2; + } + } + + private static final class Bounds { + private double minX = Double.POSITIVE_INFINITY; + private double minY = Double.POSITIVE_INFINITY; + private double maxX = Double.NEGATIVE_INFINITY; + private double maxY = Double.NEGATIVE_INFINITY; + + void include(double x1, double y1, double x2, double y2) { + includePoint(x1, y1); + includePoint(x2, y2); + } + + void includePoint(double x, double y) { + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + + boolean isValid() { + return minX != Double.POSITIVE_INFINITY && width() > 0 && height() > 0; + } + + double minX() { + return minX; + } + + double minY() { + return minY; + } + + double maxX() { + return maxX; + } + + double maxY() { + return maxY; + } + + double width() { + return maxX - minX; + } + + double height() { + return maxY - minY; + } + + double centerX() { + return (minX + maxX) / 2; + } + + double centerY() { + return (minY + maxY) / 2; + } + + static Bounds fromViewBox(ViewBox viewBox) { + Bounds bounds = new Bounds(); + bounds.include(viewBox.minX(), viewBox.minY(), viewBox.minX() + viewBox.width(), viewBox.minY() + viewBox.height()); + return bounds; + } + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/ZipStringUtils.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/ZipStringUtils.java new file mode 100644 index 00000000..787ecce4 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-spec/src/main/java/com/xspaceagi/agent/core/spec/utils/ZipStringUtils.java @@ -0,0 +1,63 @@ +package com.xspaceagi.agent.core.spec.utils; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.zip.*; + +/** + * ZIP 字符串压缩/解压工具 + */ +public class ZipStringUtils { + + /** + * 压缩字符串为 byte 数组 + */ + public static byte[] compress(String input) throws IOException { + if (input == null || input.isEmpty()) { + return new byte[0]; + } + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ZipOutputStream zos = new ZipOutputStream(bos)) { + zos.putNextEntry(new ZipEntry("data")); + zos.write(input.getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + return bos.toByteArray(); + } + } + + /** + * 压缩字符串为 Base64 字符串(方便传输/存储) + */ + public static String compressToBase64(String input) throws IOException { + return Base64.getEncoder().encodeToString(compress(input)); + } + + /** + * 从 byte 数组解压为字符串 + */ + public static String decompress(byte[] data) throws IOException { + if (data == null || data.length == 0) { + return ""; + } + try (ByteArrayInputStream bis = new ByteArrayInputStream(data); + ZipInputStream zis = new ZipInputStream(bis)) { + zis.getNextEntry(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int len; + while ((len = zis.read(buf)) != -1) { + bos.write(buf, 0, len); + } + zis.closeEntry(); + return bos.toString(StandardCharsets.UTF_8); + } + } + + /** + * 从 Base64 字符串解压为原始字符串 + */ + public static String decompressFromBase64(String base64) throws IOException { + return decompress(Base64.getDecoder().decode(base64)); + } +} \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/AgentController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/AgentController.java index a6a2fe94..b71fb937 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/AgentController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/AgentController.java @@ -28,8 +28,8 @@ import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.service.SpaceApplicationService; import com.xspaceagi.system.application.service.SysUserPermissionCacheService; import com.xspaceagi.system.application.util.DefaultIconUrlUtil; -import com.xspaceagi.system.sdk.permission.SpacePermissionService; import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto; import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.common.RequestContext; @@ -116,6 +116,16 @@ public class AgentController { @Resource private IUserDataPermissionRpcService userDataPermissionRpcService; + @Resource + private AgentWorkspaceApplicationService agentWorkspaceApplicationService; + + @Operation(summary = "AI生成项目信息") + @RequestMapping(path = "/generate-info", method = RequestMethod.POST) + public ReqResult generateInfo(@RequestBody @Valid GenerateInfoReqDto req) { + GenerateInfoResultDto result = agentApplicationService.generateInfo(req); + return ReqResult.success(result); + } + // 方法内部做了资源鉴权,此处不用@RequireResource鉴权 @Operation(summary = "新增智能体接口") @RequestMapping(path = "/add", method = RequestMethod.POST) @@ -292,6 +302,14 @@ public class AgentController { throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentPrivateComputerDeleteForbidden); } agentApplicationService.delete(agentId); + + if (agentConfigDto.getDevAgentConversationId() != null) { + try { + agentWorkspaceApplicationService.deleteWorkspace(agentConfigDto.getCreatorId(), agentConfigDto.getDevAgentConversationId()); + } catch (Exception e) { + log.warn("Failed to delete sandbox workspace for agent, agentId={}, cId={}", agentId, agentConfigDto.getDevAgentConversationId(), e); + } + } return ReqResult.success(); } @@ -382,14 +400,14 @@ public class AgentController { boolean needCheckPermissionState = true; if (knowledgeIds != null && !knowledgeIds.isEmpty()) { for (Long knowledgeId : knowledgeIds) { - if(knowledgeId.equals(knowledgeConfigModel.getId())) { + if (knowledgeId.equals(knowledgeConfigModel.getId())) { needCheckPermissionState = false; break; } } } //新增特殊授权 - if(needCheckPermissionState) { + if (needCheckPermissionState) { spacePermissionService.checkSpaceUserPermission(knowledgeConfigModel.getSpaceId(), RequestContext.get().getUserId()); } @@ -420,6 +438,16 @@ public class AgentController { if (agentComponentConfigAddDto.getType() == AgentComponentConfig.Type.Skill) { skillApplicationService.checkSpaceSkillPermission(agentConfigDto.getSpaceId(), agentComponentConfigAddDto.getTargetId()); } + if (agentComponentConfigAddDto.getType() == AgentComponentConfig.Type.Agent) { + Assert.isTrue(!agentComponentConfigAddDto.getAgentId().equals(agentComponentConfigAddDto.getTargetId()), "不能将当前智能体加入自身组件"); + PublishedDto publishedAgent = publishApplicationService.queryPublished(Published.TargetType.Agent, agentComponentConfigAddDto.getTargetId()); + Assert.notNull(publishedAgent, "智能体不存在或已下架"); + PublishedPermissionDto publishedPermissionDto = publishApplicationService.hasPermission(Published.TargetType.Agent, agentComponentConfigAddDto.getTargetId()); + Assert.isTrue(publishedPermissionDto.isExecute(), "无智能体对话权限"); + componentConfigDto.setName(publishedAgent.getName()); + componentConfigDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(publishedAgent.getIcon(), publishedAgent.getName(), "agent")); + componentConfigDto.setDescription(publishedAgent.getDescription()); + } BeanUtils.copyProperties(agentComponentConfigAddDto, componentConfigDto); agentApplicationService.addComponentConfig(componentConfigDto); return ReqResult.success(componentConfigDto.getId()); @@ -752,6 +780,25 @@ public class AgentController { return ReqResult.success(); } + @RequireResource(AGENT_MODIFY) + @Operation(summary = "更新subagent配置") + @RequestMapping(path = "/component/hook/update", method = RequestMethod.POST) + public ReqResult updateComponentHookConfig(@RequestBody AgentComponentConfigUpdateDto componentConfigUpdateDto) { + //权限验证 + checkComponentPermission(componentConfigUpdateDto.getId(), AgentComponentConfig.Type.Hook); + componentConfigUpdateDto.getBindConfig().getHooks().forEach(hook -> { + Assert.notNull(hook.getName(), "hook name can not be null"); + Assert.notNull(hook.getConfig(), "hook config can not be null"); + Assert.notNull(hook.getType(), "hook type can not be null"); + Assert.notNull(hook.getEvent(), "hook event can not be null"); + }); + + AgentComponentConfigDto agentComponentConfigDto = new AgentComponentConfigDto(); + BeanUtils.copyProperties(componentConfigUpdateDto, agentComponentConfigDto); + agentApplicationService.updateComponentConfig(agentComponentConfigDto); + return ReqResult.success(); + } + @Operation(summary = "触发器定时任务时区数据") @RequestMapping(path = "/trigger/timeZone/data", method = RequestMethod.GET) public ReqResult getTriggerTimeZoneData() { @@ -818,10 +865,11 @@ public class AgentController { } private AgentConfigDto checkAgentPermission(Long agentId) { - AgentConfigDto agentDto = agentApplicationService.queryById(agentId); - if (agentDto == null) { + List agentDtos = agentApplicationService.queryListByIds(List.of(agentId)); + if (CollectionUtils.isEmpty(agentDtos)) { throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentNotFoundAlt); } + AgentConfigDto agentDto = agentDtos.get(0); spacePermissionService.checkSpaceUserPermission(agentDto.getSpaceId()); return agentDto; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/AgentWorkspaceController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/AgentWorkspaceController.java new file mode 100644 index 00000000..50d20749 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/AgentWorkspaceController.java @@ -0,0 +1,59 @@ +package com.xspaceagi.agent.web.ui.controller; + +import com.xspaceagi.agent.core.adapter.application.AgentWorkspaceApplicationService; +import com.xspaceagi.agent.core.adapter.dto.InitProjectTemplateDto; +import com.xspaceagi.agent.core.adapter.dto.AgentPublishVersionDto; +import com.xspaceagi.agent.core.adapter.dto.InstallProjectDto; +import com.xspaceagi.agent.core.adapter.dto.PackageAgentDto; +import com.xspaceagi.agent.web.ui.controller.base.BaseController; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.dto.ReqResult; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Tag(name = "Agent工作空间接口") +@RestController +@RequestMapping("/api/agent/workspace") +@Slf4j +public class AgentWorkspaceController extends BaseController { + + @Resource + private AgentWorkspaceApplicationService agentWorkspaceApplicationService; + + @Operation(summary = "安装项目依赖") + @PostMapping("/install-project") + public ReqResult installProject(@RequestBody InstallProjectDto dto) { + Assert.notNull(dto.getCId(), "Parameter cId is required."); + Assert.notNull(dto.getProgrammingLanguage(), "Parameter programmingLanguage is required."); + + dto.setUserId(RequestContext.get().getUserId()); + agentWorkspaceApplicationService.installProject(dto); + return ReqResult.success(); + } + + @PostMapping("/init-project-template") + public ReqResult initProjectTemplate(@RequestBody InitProjectTemplateDto dto) { + dto.setUserId(RequestContext.get().getUserId()); + agentWorkspaceApplicationService.initProjectTemplate(dto); + return ReqResult.success(); + } + + @PostMapping("/package") + public ReqResult> packageAgent(@RequestBody PackageAgentDto dto) { + dto.setUserId(RequestContext.get().getUserId()); + List result = agentWorkspaceApplicationService.packageAgent(dto); + return ReqResult.success(result); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComponentController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComponentController.java index 2470779f..97b9423b 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComponentController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComponentController.java @@ -4,21 +4,20 @@ import com.xspaceagi.agent.core.adapter.application.ComponentApplicationService; import com.xspaceagi.agent.core.adapter.dto.ComponentDto; import com.xspaceagi.agent.core.spec.enums.ComponentTypeEnum; import com.xspaceagi.agent.web.ui.controller.util.SpaceObjectPermissionUtil; +import com.xspaceagi.agent.web.ui.dto.ComponentQueryDto; +import com.xspaceagi.system.application.dto.SpaceUserDto; +import com.xspaceagi.system.application.service.SpaceApplicationService; +import com.xspaceagi.system.application.util.DefaultIconUrlUtil; import com.xspaceagi.system.sdk.permission.SpacePermissionService; import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.ReqResult; -import com.xspaceagi.system.application.dto.SpaceUserDto; -import com.xspaceagi.system.application.service.SpaceApplicationService; -import com.xspaceagi.system.application.util.DefaultIconUrlUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.stream.Collectors; @@ -45,16 +44,32 @@ public class ComponentController { @RequestMapping(path = "/list/{spaceId}", method = RequestMethod.GET) public ReqResult> list(@PathVariable Long spaceId) { spacePermissionService.checkSpaceUserPermission(spaceId); - List componentDtos = componentApplicationService.getComponentListBySpaceId(spaceId); + List componentDtos = componentApplicationService.getComponentListBySpaceId(spaceId, null, null); + completePermissions(componentDtos, spaceId); + return ReqResult.success(componentDtos); + } + + @RequireResource(COMPONENT_LIB_QUERY_LIST) + @Operation(summary = "条件查询组件列表接口") + @RequestMapping(path = "/list", method = RequestMethod.POST) + public ReqResult> queryList(@RequestBody ComponentQueryDto queryDto) { + Assert.notNull(queryDto, "queryDto is null"); + Assert.notNull(queryDto.getSpaceId(), "spaceId is null"); + spacePermissionService.checkSpaceUserPermission(queryDto.getSpaceId()); + List componentDtos = componentApplicationService.getComponentListBySpaceId(queryDto.getSpaceId(), queryDto.getGroupId(), queryDto.getTypes()); + completePermissions(componentDtos, queryDto.getSpaceId()); + return ReqResult.success(componentDtos); + } + + private void completePermissions(List componentDtos, Long spaceId) { SpaceUserDto spaceUserDto = spaceApplicationService.querySpaceUser(spaceId, RequestContext.get().getUserId()); componentDtos.forEach(componentDto -> { - List collect = SpaceObjectPermissionUtil.generatePermissionList(spaceUserDto, componentDto.getCreatorId()).stream().map(permission -> permission.name()).collect(Collectors.toList()); + List collect = SpaceObjectPermissionUtil.generatePermissionList(spaceUserDto, componentDto.getCreatorId()).stream().map(Enum::name).collect(Collectors.toList()); componentDto.setPermissions(collect); if (componentDto.getType() != ComponentTypeEnum.Model) { componentDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(componentDto.getIcon(), componentDto.getName(), componentDto.getType().name())); } }); - return ReqResult.success(componentDtos); } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComputerFileController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComputerFileController.java index 6a69cbdf..6502ccec 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComputerFileController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComputerFileController.java @@ -2,6 +2,7 @@ package com.xspaceagi.agent.web.ui.controller; import com.xspaceagi.agent.core.adapter.application.IComputerFileApplicationService; import com.xspaceagi.agent.web.ui.controller.base.BaseController; +import com.xspaceagi.agent.web.ui.controller.base.ConversationPermissionChecker; import com.xspaceagi.agent.web.ui.controller.dto.ComputerFileListRes; import com.xspaceagi.agent.web.ui.controller.dto.ComputerFilesUpdateReq; import com.xspaceagi.system.spec.common.UserContext; @@ -32,15 +33,19 @@ public class ComputerFileController extends BaseController { @Resource private IComputerFileApplicationService computerFileApplicationService; + @Resource + private ConversationPermissionChecker conversationPermissionChecker; @Operation(summary = "查询文件列表", description = "查询文件列表") @GetMapping("/static/file-list") - public ReqResult getFileList(@RequestParam("cId") Long cId) { - Long userId = getUser().getUserId(); - log.info("[Web] File list request, userId={}, cId={}", userId, cId); -// UserContext userContext = getUser(); + public ReqResult getFileList(@RequestParam("cId") Long cId, + @RequestParam(value = "customTargetDir", required = false) String customTargetDir) { + + Long conversationUserId = conversationPermissionChecker.check(cId, customTargetDir); + log.info("[Web] File list request, cId={}, conversationUserId={}, loginUserId={}", cId, conversationUserId, getUser().getUserId()); + String proxyPath = String.format("/api/computer/static/%s", cId); - Map result = computerFileApplicationService.getFileList(userId, cId, proxyPath, null); + Map result = computerFileApplicationService.getFileList(conversationUserId, cId, proxyPath, null, customTargetDir); if (result == null) { return ReqResult.error(I18nUtil.systemMessage("File.GetFileList.Error")); } @@ -69,11 +74,12 @@ public class ComputerFileController extends BaseController { @Operation(summary = "文件修改", description = "文件修改") @PostMapping(value = "/static/files-update", produces = MediaType.APPLICATION_JSON_VALUE) - public ReqResult> filesUpdate(@RequestBody ComputerFilesUpdateReq req) { - Long userId = getUser().getUserId(); - log.info("[Web] File update request, userId={}, cId={}", userId, req.getCId()); - UserContext userContext = getUser(); - Map result = computerFileApplicationService.filesUpdate(userId, req.getCId(), req.getFiles(), userContext); + public ReqResult> filesUpdate(@RequestBody ComputerFilesUpdateReq req, + @RequestParam(value = "customTargetDir", required = false) String customTargetDir) { + Long conversationUserId = conversationPermissionChecker.check(req.getCId(), customTargetDir); + log.info("[Web] File update request, cId={}, conversationUserId={}, loginUserId={}", req.getCId(), conversationUserId, getUser().getUserId()); + + Map result = computerFileApplicationService.filesUpdate(conversationUserId, req.getCId(), req.getFiles(), getUser(), customTargetDir); if (result == null) { return ReqResult.error("File modification failed."); } @@ -103,24 +109,25 @@ public class ComputerFileController extends BaseController { public ReqResult> uploadFile(@RequestParam("cId") Long cId, @RequestParam(value = "filePath", required = false) String filePath, @RequestParam(value = "filePaths", required = false) List filePaths, - @RequestParam("file") MultipartFile file) { - Long userId = getUser().getUserId(); - + @RequestParam("file") MultipartFile file, + @RequestParam(value = "customTargetDir", required = false) String customTargetDir) { + Long conversationUserId = conversationPermissionChecker.check(cId, customTargetDir); + // 兼容处理:优先使用 filePath,如果没有则从 filePaths 数组取第一个 String finalFilePath = filePath; if (finalFilePath == null || finalFilePath.trim().isEmpty()) { if (filePaths != null && !filePaths.isEmpty()) { finalFilePath = filePaths.get(0); - log.warn("[Web] Single-file upload received filePaths array; using first element, userId={}, cId={}, filePath={}", userId, cId, finalFilePath); + log.warn("[Web] Single-file upload received filePaths array; using first element, cId={}, conversationUserId={}, loginUserId={}, filePath={}", cId, conversationUserId, getUser().getUserId(), finalFilePath); } else { - log.error("[Web] Invalid upload params: both filePath and filePaths are empty, userId={}, cId={}", userId, cId); + log.error("[Web] Invalid upload params: both filePath and filePaths are empty, cId={}, conversationUserId={}, loginUserId={}", cId, conversationUserId, getUser().getUserId()); return ReqResult.error("Param 'filePath' cannot be left blank."); } } - log.info("[Web] File upload request, userId={}, cId={}, filePath={}", userId, cId, finalFilePath); + log.info("[Web] File upload request, cId={}, conversationUserId={}, loginUserId={}, filePath={}", cId, conversationUserId, getUser().getUserId(), finalFilePath); UserContext userContext = getUser(); - Map result = computerFileApplicationService.uploadFile(userId, cId, finalFilePath, file, userContext); + Map result = computerFileApplicationService.uploadFile(conversationUserId, cId, finalFilePath, file, userContext, customTargetDir); if (result == null) { return ReqResult.error("File upload failed."); } @@ -149,20 +156,22 @@ public class ComputerFileController extends BaseController { @PostMapping(value = "/static/upload-files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ReqResult> uploadFiles(@RequestParam("cId") Long cId, @RequestParam("filePaths") List filePaths, - @RequestParam("files") List files) { - Long userId = getUser().getUserId(); - + @RequestParam("files") List files, + @RequestParam(value = "customTargetDir", required = false) String customTargetDir) { + Long conversationUserId = conversationPermissionChecker.check(cId, customTargetDir); + if (filePaths == null || filePaths.isEmpty()) { - log.error("[Web] filePaths is empty, userId={}, cId={}", userId, cId); + log.error("[Web] filePaths is empty, cId={}, conversationUserId={}, loginUserId={}", cId, conversationUserId, getUser().getUserId()); return ReqResult.error("Param 'filePaths' cannot be left blank."); } - log.info("[Web] Batch file upload request, userId={}, cId={}, filePathsSize={}, filesSize={}, filePaths={}", userId, cId, + log.info("[Web] Batch file upload request, cId={}, conversationUserId={}, loginUserId={}, filePathsSize={}, filesSize={}, filePaths={}", + cId, conversationUserId, getUser().getUserId(), filePaths.size(), files != null ? files.size() : 0, filePaths); UserContext userContext = getUser(); - Map result = computerFileApplicationService.uploadFiles(userId, cId, filePaths, files, userContext); + Map result = computerFileApplicationService.uploadFiles(conversationUserId, cId, filePaths, files, userContext, customTargetDir); if (result == null) { return ReqResult.error("Batch file upload failed"); } @@ -187,12 +196,60 @@ public class ComputerFileController extends BaseController { return ReqResult.success(result); } + @Operation(summary = "导入项目", description = "上传 zip 包替换工作空间文件,保留 .git/.agents/.claude/.codex/.opencode/.tmp/.logs") + @PostMapping(value = "/static/import-project", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> importProject(@RequestParam("cId") Long cId, + @RequestParam("file") MultipartFile file, + @RequestParam(value = "customTargetDir", required = false) String customTargetDir) { + Long conversationUserId = conversationPermissionChecker.check(cId, customTargetDir); + + if (file == null || file.isEmpty()) { + log.error("[Web] Invalid import project params: file is empty, cId={}, loginUserId={}", cId, getUser().getUserId()); + return ReqResult.error("Param 'file' cannot be left blank."); + } + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".zip")) { + log.error("[Web] Invalid import project params: only zip is supported, cId={}, fileName={}", cId, originalFilename); + return ReqResult.error("Only zip files are supported."); + } + + log.info("[Web] Import project request, cId={}, conversationUserId={}, loginUserId={}, fileName={}", + cId, conversationUserId, getUser().getUserId(), originalFilename); + + UserContext userContext = getUser(); + Map result = computerFileApplicationService.importProject(conversationUserId, cId, file, userContext, customTargetDir); + if (result == null) { + return ReqResult.error("Project import failed."); + } + + Object successObj = result.get("success"); + Object codeObj = result.get("code"); + + boolean success; + if (successObj instanceof Boolean) { + success = (Boolean) successObj; + } else { + String code = codeObj != null ? codeObj.toString() : ReqResult.SUCCESS; + success = ReqResult.SUCCESS.equals(code); + } + + if (!success) { + String message = result.getOrDefault("message", "Project import failed.").toString(); + String code = codeObj != null ? codeObj.toString() : "9999"; + return ReqResult.create(code, message, null); + } + + return ReqResult.success(result); + } + @Operation(summary = "下载全部文件", description = "下载全部文件(zip 流式输出)") @GetMapping("/static/download-all-files") - public ResponseEntity downloadAllFiles(@RequestParam("cId") Long cId) { - Long userId = getUser().getUserId(); - log.info("[Web] Download all files request, userId={}, cId={}", userId, cId); - return computerFileApplicationService.downloadAllFiles(userId, cId, getUser()); + public ResponseEntity downloadAllFiles(@RequestParam("cId") Long cId, + @RequestParam(value = "customTargetDir", required = false) String customTargetDir) { + Long conversationUserId = conversationPermissionChecker.check(cId, customTargetDir); + log.info("[Web] Download all files request, cId={}, conversationUserId={}, loginUserId={}", cId, conversationUserId, getUser().getUserId()); + + return computerFileApplicationService.downloadAllFiles(conversationUserId, cId, getUser(), customTargetDir); } @Operation(summary = "静态文件访问", description = "静态文件访问,返回二进制流(图片、文件等)") @@ -204,7 +261,41 @@ public class ComputerFileController extends BaseController { log.error("[Web] Invalid cId, cId={}", cId); return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } - return computerFileApplicationService.getStaticFile(cId, request); + String customTargetDir = request.getParameter("customTargetDir"); + conversationPermissionChecker.checkCustomTargetDir(cId, customTargetDir); + + return computerFileApplicationService.getStaticFile(cId, request, customTargetDir); } + @Operation(summary = "获取沙盒日志", description = "获取沙盒最新日志,取最后修改的日志文件的最后N行") + @GetMapping("/logs") + public ReqResult> getLogs(@RequestParam("cId") Long cId, + @RequestParam(value = "tailLines", defaultValue = "200") int tailLines) { + Long conversationUserId = conversationPermissionChecker.check(cId); + log.info("[Web] Log request, cId={}, conversationUserId={}, loginUserId={}, tailLines={}", cId, conversationUserId, getUser().getUserId(), tailLines); + + Map result = computerFileApplicationService.getLogs(conversationUserId, cId, tailLines); + if (result == null) { + return ReqResult.error("Failed to get logs."); + } + + Object successObj = result.get("success"); + Object codeObj = result.get("code"); + + boolean success; + if (successObj instanceof Boolean) { + success = (Boolean) successObj; + } else { + String code = codeObj != null ? codeObj.toString() : ReqResult.SUCCESS; + success = ReqResult.SUCCESS.equals(code); + } + + if (!success) { + String message = result.getOrDefault("message", "Failed to get logs.").toString(); + String code = codeObj != null ? codeObj.toString() : "9999"; + return ReqResult.create(code, message, null); + } + + return ReqResult.success(result); + } } \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComputerPodController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComputerPodController.java index 6b042aef..d5619411 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComputerPodController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ComputerPodController.java @@ -1,9 +1,15 @@ package com.xspaceagi.agent.web.ui.controller; import com.xspaceagi.agent.core.adapter.application.ComputerPodApplicationService; +import com.xspaceagi.agent.core.adapter.application.ConversationApplicationService; import com.xspaceagi.agent.core.adapter.dto.ComputerPodResultDto; +import com.xspaceagi.agent.core.adapter.dto.ConversationDto; import com.xspaceagi.agent.core.infra.component.agent.SandboxAgentClient; +import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.ReqResult; +import com.xspaceagi.system.spec.enums.ErrorCodeEnum; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -20,6 +26,9 @@ public class ComputerPodController { @Resource private ComputerPodApplicationService computerPodApplicationService; + @Resource + private ConversationApplicationService conversationApplicationService; + @Resource private SandboxAgentClient sandboxAgentClient; @@ -30,6 +39,22 @@ public class ComputerPodController { return buildResponse(result); } + @Operation(summary = "重启容器(销毁后重建)") + @PostMapping(value = "/pod/restart", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult podRestart(@RequestParam("cId") Long cId) { + checkConversationPermission(cId); + ComputerPodResultDto result = computerPodApplicationService.restart(cId); + return buildResponse(result); + } + + @Operation(summary = "停止智能体") + @RequestMapping(path = "/agent/stop/{conversationId}", method = RequestMethod.POST) + public ReqResult stopAgent(@PathVariable Long conversationId) { + checkConversationPermission(conversationId); + sandboxAgentClient.agentStop(String.valueOf(conversationId)); + return ReqResult.success(); + } + @Operation(summary = "容器保活") @PostMapping(value = "/pod/keepalive", produces = MediaType.APPLICATION_JSON_VALUE) public ReqResult podKeepalive(@RequestParam("cId") Long cId) { @@ -37,13 +62,6 @@ public class ComputerPodController { return buildResponse(result); } - @Operation(summary = "重启容器(销毁后重建)") - @PostMapping(value = "/pod/restart", produces = MediaType.APPLICATION_JSON_VALUE) - public ReqResult podRestart(@RequestParam("cId") Long cId) { - ComputerPodResultDto result = computerPodApplicationService.restart(cId); - return buildResponse(result); - } - @Operation(summary = "查询容器 VNC 服务状态") @GetMapping(value = "/pod/vnc-status", produces = MediaType.APPLICATION_JSON_VALUE) public ReqResult podVncStatus(@RequestParam("cId") Long cId) { @@ -51,14 +69,18 @@ public class ComputerPodController { return buildResponse(result); } - @Operation(summary = "停止智能体") - @RequestMapping(path = "/agent/stop/{conversationId}", method = RequestMethod.POST) - public ReqResult stopAgent(@PathVariable Long conversationId) { - if (conversationId == null) { - return ReqResult.success(); + /** + * 校验会话是否属于当前登录用户,不属于则抛出无权限异常 + */ + private void checkConversationPermission(Long cId) { + if (cId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "cId"); + } + ConversationDto conversation = conversationApplicationService.getConversationByCid(cId); + if (conversation == null + || !RequestContext.get().getUserId().equals(conversation.getUserId())) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); } - sandboxAgentClient.agentStop(String.valueOf(conversationId)); - return ReqResult.success(); } private ReqResult buildResponse(ComputerPodResultDto result) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ConversationController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ConversationController.java index 9088774c..1e06e2c7 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ConversationController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ConversationController.java @@ -1,5 +1,7 @@ package com.xspaceagi.agent.web.ui.controller; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; import com.xspaceagi.agent.core.adapter.application.AgentApplicationService; import com.xspaceagi.agent.core.adapter.application.ConversationApplicationService; import com.xspaceagi.agent.core.adapter.application.ModelApplicationService; @@ -15,10 +17,12 @@ import com.xspaceagi.agent.core.infra.component.agent.SandboxAgentClient; import com.xspaceagi.agent.core.infra.component.model.ModelPageRequest; import com.xspaceagi.agent.core.infra.rpc.SandboxServerConfigService; import com.xspaceagi.agent.core.infra.rpc.UserShareRpcService; +import com.xspaceagi.agent.core.infra.sub.ChatMessagePubSubService; import com.xspaceagi.agent.core.spec.enums.MessageTypeEnum; import com.xspaceagi.agent.web.ui.controller.dto.ConversationMessageQueryDto; import com.xspaceagi.agent.web.ui.controller.dto.ConversationShareDto; import com.xspaceagi.agent.web.ui.controller.dto.ModelPageRequestResultDto; +import com.xspaceagi.agent.web.ui.controller.dto.PermissionRequestResponseDto; import com.xspaceagi.agent.web.ui.dto.ConversationCreateDto; import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigRpcDto; import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum; @@ -34,7 +38,6 @@ import com.xspaceagi.system.spec.enums.ErrorCodeEnum; import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import com.xspaceagi.system.spec.utils.I18nUtil; -import com.xspaceagi.system.spec.utils.RedisUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -52,6 +55,9 @@ import reactor.core.publisher.Flux; import java.time.Duration; import java.time.Instant; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; @Tag(name = "智能体会话相关接口") @RestController @@ -85,9 +91,6 @@ public class ConversationController { @Resource private ModelPageRequest modelPageRequest; - @Resource - private RedisUtil redisUtil; - @Resource private SandboxServerConfigService sandboxServerConfigService; @@ -97,6 +100,9 @@ public class ConversationController { @Resource private SandboxAgentClient sandboxAgentClient; + @Resource + private ChatMessagePubSubService chatMessagePubSubService; + @Operation(summary = "创建会话") @RequestMapping(path = "/create", method = RequestMethod.POST) public ReqResult create(@RequestBody ConversationCreateDto conversationCreateDto) { @@ -160,7 +166,12 @@ public class ConversationController { spacePermissionService.checkSpaceUserPermission(conversationDto.getAgent().getSpaceId()); } else { if (!conversationDto.getUserId().equals(RequestContext.get().getUserId())) { - return ReqResult.error("No permission to chat with this agent"); + if (conversationDto.getDevSpaceId() != null) { + spacePermissionService.checkSpaceUserPermission(conversationDto.getDevSpaceId()); + conversationDto.getAgent().setHasPermission(false); + } else { + return ReqResult.error("No permission to chat with this agent"); + } } PublishedPermissionDto publishedPermissionDto = publishApplicationService.hasPermission(Published.TargetType.Agent, conversationDto.getAgentId()); if (!publishedPermissionDto.isView()) { @@ -253,7 +264,7 @@ public class ConversationController { cid = Long.parseLong(conversationId); } catch (NumberFormatException e) { // Compatible with previous API calls - redisUtil.set("chat.stop." + conversationId, String.valueOf(System.currentTimeMillis()), 60); + conversationApplicationService.setChatStopStatus(conversationId); return ReqResult.success(); } ConversationDto conversation = conversationApplicationService.getConversation(RequestContext.get().getUserId(), cid); @@ -262,16 +273,93 @@ public class ConversationController { } if ("TaskAgent".equals(conversation.getAgent().getType())) { - if (!sandboxAgentClient.chatCancel(conversationId)) { - redisUtil.set("chat.stop." + conversationId, String.valueOf(System.currentTimeMillis()), 60); - } - } else { - redisUtil.set("chat.stop." + conversationId, String.valueOf(System.currentTimeMillis()), 60); + sandboxAgentClient.chatCancel(conversationId); } + conversationApplicationService.setChatStopStatus(conversationId); conversationApplicationService.updateConversationStatus(conversation.getId(), Conversation.ConversationTaskStatus.CANCEL); return ReqResult.success(); } + @Operation(summary = "订阅会话消息") + @RequestMapping(path = "/chat/sub/{conversationId}", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux sub(@PathVariable Long conversationId, HttpServletResponse response) { + response.setCharacterEncoding("utf-8"); + ConversationDto conversation = conversationApplicationService.getConversation(RequestContext.get().getUserId(), conversationId); + if (conversation == null) { + AgentOutputDto agentOutputDto = new AgentOutputDto(); + agentOutputDto.setEventType(AgentOutputDto.EventTypeEnum.ERROR); + agentOutputDto.setError("Conversation not found"); + return Flux.just(agentOutputDto); + } + if (conversation.getTaskStatus() != Conversation.ConversationTaskStatus.EXECUTING) { + AgentOutputDto agentOutputDto = new AgentOutputDto(); + agentOutputDto.setEventType(AgentOutputDto.EventTypeEnum.ERROR); + agentOutputDto.setError("Conversation not executing"); + return Flux.just(agentOutputDto); + } + + AtomicReference> consumer = new AtomicReference<>(); + Flux flux = Flux.create(sink -> { + AtomicBoolean completed = new AtomicBoolean(false); + List messages = chatMessagePubSubService.getMessages(conversationId.toString()); + if (messages != null && !messages.isEmpty()) { + for (String message : messages) { + try { + AgentOutputDto agentOutputDto = JSON.parseObject(message, AgentOutputDto.class); + sink.next(agentOutputDto); + if (agentOutputDto.getEventType() == AgentOutputDto.EventTypeEnum.FINAL_RESULT) { + sink.complete(); + completed.set(true); + } + } catch (Exception e) { + log.error("parse message failed", e); + } + } + } + if (completed.get()) { + return; + } + Consumer listener = message -> { + if (JSON.isValidArray(message)) { + JSONArray jsonArray = JSON.parseArray(message); + for (int i = 0; i < jsonArray.size(); i++) { + try { + AgentOutputDto agentOutputDto = JSON.parseObject(jsonArray.getString(i), AgentOutputDto.class); + sink.next(agentOutputDto); + if (agentOutputDto.getEventType() == AgentOutputDto.EventTypeEnum.FINAL_RESULT || agentOutputDto.getEventType() == AgentOutputDto.EventTypeEnum.ERROR) { + sink.complete(); + chatMessagePubSubService.unsubscribe(conversationId.toString(), consumer.get()); + } + } catch (Exception e) { + log.error("parse message failed", e); + } + } + } + }; + consumer.set(listener); + chatMessagePubSubService.subscribe(conversationId.toString(), listener); + }); + return flux.doOnCancel(() -> chatMessagePubSubService.unsubscribe(conversationId.toString(), consumer.get())); + } + + @Operation(summary = "权限请求回复") + @RequestMapping(path = "/chat/permission-request/response", method = RequestMethod.POST) + public ReqResult notifyResolved(@RequestBody PermissionRequestResponseDto permissionRequestResponseDto) { + ConversationDto conversation = conversationApplicationService.getConversation(RequestContext.get().getUserId(), permissionRequestResponseDto.getConversationId()); + if (conversation == null) { + return ReqResult.error("Conversation not found"); + } + + if ("TaskAgent".equals(conversation.getAgent().getType())) { + Assert.notNull(permissionRequestResponseDto.getOption(), "Option is null"); + sandboxAgentClient.notifyResolved(permissionRequestResponseDto.getConversationId().toString(), permissionRequestResponseDto.getToolId(), Map.of( + "optionId", permissionRequestResponseDto.getOption().getOptionId(), + "outcome", "selected" + )); + } + return ReqResult.success(); + } + @Operation(summary = "获取用户智能体电脑存活的Agent会话") @RequestMapping(path = "/alive/{sandboxServerId}", method = RequestMethod.POST) public ReqResult> getSandboxAliveAgentConversation(@PathVariable Long sandboxServerId) { @@ -290,6 +378,7 @@ public class ConversationController { if (conversationByCid == null || conversationByCid.getUserId().equals(RequestContext.get().getUserId())) { return ReqResult.error("No permission for this conversation"); } + conversationApplicationService.setChatStopStatus(conversationId.toString()); sandboxAgentClient.agentStop(conversationId.toString()); return ReqResult.success(); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ExecuteForPageController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ExecuteForPageController.java index f5b0b222..7f2967d3 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ExecuteForPageController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ExecuteForPageController.java @@ -1,10 +1,7 @@ package com.xspaceagi.agent.web.ui.controller; import com.alibaba.fastjson2.JSON; -import com.xspaceagi.agent.core.adapter.application.AgentApplicationService; -import com.xspaceagi.agent.core.adapter.application.PluginApplicationService; -import com.xspaceagi.agent.core.adapter.application.PublishApplicationService; -import com.xspaceagi.agent.core.adapter.application.WorkflowApplicationService; +import com.xspaceagi.agent.core.adapter.application.*; import com.xspaceagi.agent.core.adapter.dto.*; import com.xspaceagi.agent.core.adapter.dto.config.AgentComponentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; @@ -20,8 +17,8 @@ import com.xspaceagi.agent.web.ui.controller.util.ReferUtil; import com.xspaceagi.custompage.sdk.dto.DataSourceDto; import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.service.UserApplicationService; -import com.xspaceagi.system.sdk.permission.SpacePermissionService; import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.ReqResult; @@ -36,7 +33,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.http.MediaType; +import org.springframework.util.Assert; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; @@ -77,6 +76,9 @@ public class ExecuteForPageController { @Resource private IUserDataPermissionRpcService userDataPermissionRpcService; + @Resource + private TableWorkflowForPageApplicationService tableWorkflowForPageApplicationService; + @Operation(summary = "运行已发布的工作流") @RequestMapping(path = "/api/page/workflow/{key}/streamExecute", method = RequestMethod.POST, produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux streamExecuteWorkflow(@PathVariable String key, @RequestBody @Valid Map params, HttpServletRequest request, HttpServletResponse response) { @@ -117,14 +119,15 @@ public class ExecuteForPageController { .map(workflowExecutingDto -> " " + JSON.toJSONString(workflowExecutingDto)); } - @Operation(summary = "运行已发布的工作流") - @RequestMapping(path = "/api/page/workflow/{key}/execute", method = RequestMethod.POST) - public ReqResult executeWorkflow(@PathVariable String key, @RequestBody @Valid Map params, HttpServletRequest request, HttpServletResponse response) { + + @Operation(summary = "运行已发布的工作流(new)") + @RequestMapping(path = "/api/page/w/{key}", method = RequestMethod.POST) + public ReqResult executeW(@PathVariable String key, @RequestBody @Valid Map params, HttpServletRequest request, HttpServletResponse response) { String requestId = UUID.randomUUID().toString().replace("-", ""); ReferUtil.RefererParseVo refererParseVo = ReferUtil.parseRefer(request); PageDto pageDto = parseFromReferer(refererParseVo); Optional first = pageDto.getDataSources().stream().filter(dataSourceDto -> dataSourceDto.getKey() != null && dataSourceDto.getKey().equals(key)).findFirst(); - if (!first.isPresent()) { + if (first.isEmpty()) { return ReqResult.error("The requested data source does not exist"); } @@ -148,17 +151,23 @@ public class ExecuteForPageController { workflowExecuteRequestDto.setAgentId(refererParseVo.getAgentId()); response.setCharacterEncoding("utf-8"); WorkflowExecutingDto workflowExecutingDto = workflowApplicationService.executeWorkflow(workflowExecuteRequestDto, workflowConfigDto).blockLast(); - return ReqResult.success(workflowExecutingDto.getData()); + return workflowExecutingDto != null && workflowExecutingDto.isSuccess() ? ReqResult.success(workflowExecutingDto.getData()) : ReqResult.error(workflowExecutingDto == null ? "" : workflowExecutingDto.getMessage()); + } + + @Operation(summary = "运行已发布的工作流") + @RequestMapping(path = "/api/page/workflow/{key}/execute", method = RequestMethod.POST) + public ReqResult executeWorkflow(@PathVariable String key, @RequestBody @Valid Map params, HttpServletRequest request, HttpServletResponse response) { + return executeW(key, params, request, response); } private PageDto parseFromReferer(ReferUtil.RefererParseVo refererParseVo) { if (refererParseVo == null || refererParseVo.getPageId() == null) { throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentPageIdError); } - if (refererParseVo == null || refererParseVo.getAgentId() == null) { + if (refererParseVo.getAgentId() == null) { throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentPageAgentIdError); } - if (refererParseVo == null || refererParseVo.getEnv() == null) { + if (refererParseVo.getEnv() == null) { throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentPageEnvInvalid); } PageDto pageDto = customPageRpcService.queryPageDto(refererParseVo.getPageId(), false); @@ -224,16 +233,15 @@ public class ExecuteForPageController { emitter.complete(); } - - @Operation(summary = "插件运行接口") - @RequestMapping(path = "/api/page/plugin/{key}/execute", method = RequestMethod.POST) - public ReqResult execute(@PathVariable String key, @RequestBody Map params, HttpServletRequest request) { + @Operation(summary = "插件运行接口(new)") + @RequestMapping(path = "/api/page/p/{key}", method = RequestMethod.POST) + public ReqResult executeP(@PathVariable String key, @RequestBody Map params, HttpServletRequest request) { String requestId = UUID.randomUUID().toString().replace("-", ""); ReferUtil.RefererParseVo refererParseVo = ReferUtil.parseRefer(request); PageDto pageDto; pageDto = parseFromReferer(refererParseVo); Optional first = pageDto.getDataSources().stream().filter(dataSourceDto -> dataSourceDto.getKey() != null && dataSourceDto.getKey().equals(key)).findFirst(); - if (!first.isPresent()) { + if (first.isEmpty()) { throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentPageDatasourceNotFound); } PluginDto pluginDto = pluginApplicationService.queryPublishedPluginConfig(first.get().getId(), null); @@ -255,7 +263,13 @@ public class ExecuteForPageController { pluginExecuteRequestDto.setAgentId(refererParseVo.getAgentId()); pluginExecuteRequestDto.setTest(false); PluginExecuteResultDto pluginExecuteResultDto = pluginApplicationService.execute(pluginExecuteRequestDto, pluginDto); - return ReqResult.success(pluginExecuteResultDto.getResult()); + return pluginExecuteResultDto.isSuccess() ? ReqResult.success(pluginExecuteResultDto.getResult()) : ReqResult.error(pluginExecuteResultDto.getError()); + } + + @Operation(summary = "插件运行接口") + @RequestMapping(path = "/api/page/plugin/{key}/execute", method = RequestMethod.POST) + public ReqResult execute(@PathVariable String key, @RequestBody Map params, HttpServletRequest request) { + return executeP(key, params, request); } @Operation(summary = "获取插件、工作流接口文档schema") @@ -264,4 +278,38 @@ public class ExecuteForPageController { String data = iAgentRpcService.queryApiSchema(type, id, projectId).getData(); return ReqResult.success(data != null ? JSON.parse(data) : null); } + + @Operation(summary = "创建数据表操作API(SQL)") + @RequestMapping(path = "/api/table/sql/new", method = RequestMethod.POST) + public ReqResult tableNewSql(@RequestBody PageAppNewSqlDTO pageAppNewSqlDTO) { + PageDto pageDto = customPageRpcService.queryPageDto(Long.parseLong(pageAppNewSqlDTO.getProjectId()), false); + if (pageDto == null) { + throw new IllegalArgumentException("Invalid projectId"); + } + if (StringUtils.isBlank(pageAppNewSqlDTO.getGroupName())) { + pageAppNewSqlDTO.setGroupName(pageDto.getId().toString());//id不会变 + pageAppNewSqlDTO.setGroupDescription(pageDto.getName()); + } + pageAppNewSqlDTO.setSpaceId(pageDto.getSpaceId()); + spacePermissionService.checkSpaceUserPermission(pageAppNewSqlDTO.getSpaceId()); + PageSqlResultDTO resultDTO = tableWorkflowForPageApplicationService.tableNewSql(pageAppNewSqlDTO); + return ReqResult.success(resultDTO); + } + + @Operation(summary = "更新数据表操作API(SQL)") + @RequestMapping(path = "/api/table/sql/update", method = RequestMethod.POST) + public ReqResult tableUpdateSql(@RequestBody PageAppUpdateSqlDTO pageAppUpdateSqlDTO) { + WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryById(pageAppUpdateSqlDTO.getApiId()); + if (workflowConfigDto == null) { + throw new IllegalArgumentException("Invalid apiId"); + } + PageDto pageDto = customPageRpcService.queryPageDto(Long.parseLong(pageAppUpdateSqlDTO.getProjectId()), false); + if (pageDto == null) { + throw new IllegalArgumentException("Invalid projectId"); + } + Assert.isTrue(workflowConfigDto.getSpaceId().equals(pageDto.getSpaceId()), "Invalid spaceId"); + spacePermissionService.checkSpaceUserPermission(workflowConfigDto.getSpaceId()); + PageSqlResultDTO resultDTO = tableWorkflowForPageApplicationService.tableUpdateSql(pageAppUpdateSqlDTO); + return ReqResult.success(resultDTO); + } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/GitController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/GitController.java new file mode 100644 index 00000000..56d92be4 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/GitController.java @@ -0,0 +1,277 @@ +package com.xspaceagi.agent.web.ui.controller; + +import com.xspaceagi.agent.core.infra.rpc.GitRpcClient; +import com.xspaceagi.agent.web.ui.controller.base.BaseController; +import com.xspaceagi.agent.web.ui.controller.base.ConversationPermissionChecker; +import com.xspaceagi.agent.web.ui.controller.dto.git.*; +import com.xspaceagi.custompage.sdk.ICustomPageRpcService; +import com.xspaceagi.custompage.sdk.dto.CustomPageDto; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; +import com.xspaceagi.system.spec.dto.ReqResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +@Tag(name = "Git 版本管理接口", description = "通过 workspaceType 区分两种工作区:taskAgent(通用智能体会话,传 cId)和 pageApp(网页应用,传 projectId)") +@RestController +@RequestMapping("/api/git") +@Slf4j +public class GitController extends BaseController { + + @Resource + private GitRpcClient gitRpcClient; + @Resource + private ICustomPageRpcService iCustomPageRpcService; + @Resource + private SpacePermissionService spacePermissionService; + @Resource + private ConversationPermissionChecker conversationPermissionChecker; + + // ======================== 1. init ======================== + + @Operation(summary = "初始化 Git 仓库", description = "在项目目录中执行 git init,并生成 .gitignore。通常无需手动调用,首次 Git 操作时会自动初始化。") + @PostMapping(value = "/init", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> init(@RequestBody GitBaseReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + gitRpcClient::initTaskAgent, gitRpcClient::initPageApp); + } + + // ======================== 2. status ======================== + + @Operation(summary = "查看工作区状态", description = "返回当前 staged / modified / untracked 文件列表,对应 git status") + @PostMapping(value = "/status", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> status(@RequestBody GitBaseReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + gitRpcClient::statusTaskAgent, gitRpcClient::statusPageApp); + } + + // ======================== 3. commit ======================== + + @Operation(summary = "提交更改", description = "将暂存区的文件提交为新版本。files 为空时提交全部暂存文件;authorName / authorEmail 由后端自动从登录信息获取,无需前端传递。") + @PostMapping(value = "/commit", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> commit(@RequestBody GitCommitReq req) { + String authorName = getUser().getUserName(); + String authorEmail = getUser().getEmail(); + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.commitTaskAgent(cId, userId, req.getMessage(), + req.getFiles(), authorName, authorEmail), + projectId -> gitRpcClient.commitPageApp(projectId, req.getMessage(), + req.getFiles(), authorName, authorEmail)); + } + + // ======================== 4. add ======================== + + @Operation(summary = "暂存文件", description = "将文件加入暂存区(git add)。files 为空时暂存全部变更。文件写入/创建后可调用此接口让 Git 跟踪变更。") + @PostMapping(value = "/add", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> add(@RequestBody GitUnstageReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.addTaskAgent(cId, userId, req.getFiles()), + projectId -> gitRpcClient.addPageApp(projectId, req.getFiles())); + } + + // ======================== 5. unstage ======================== + + @Operation(summary = "取消暂存", description = "将文件从暂存区移回工作区(变为 unstaged),不丢弃文件内容。对应 git restore --staged ,files 为空时取消全部暂存。") + @PostMapping(value = "/unstage", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> unstage(@RequestBody GitUnstageReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.unstageTaskAgent(cId, userId, req.getFiles()), + projectId -> gitRpcClient.unstagePageApp(projectId, req.getFiles())); + } + + // ======================== 5. discard ======================== + + @Operation(summary = "丢弃文件更改", description = "丢弃工作区中未提交的修改,将文件恢复到最近一次 commit 的状态,不可恢复。对应 git restore ,files 为空时丢弃全部修改。") + @PostMapping(value = "/discard", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> discard(@RequestBody GitUnstageReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.discardTaskAgent(cId, userId, req.getFiles()), + projectId -> gitRpcClient.discardPageApp(projectId, req.getFiles())); + } + + // ======================== 6. log ======================== + + @Operation(summary = "查看提交历史", description = "分页返回 commit 列表,每条包含 hash、message、author、date。page 从 1 开始,默认 pageSize=50。") + @PostMapping(value = "/log", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> log(@RequestBody GitLogReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (c, u) -> gitRpcClient.logTaskAgent(c, u, req.getPage(), req.getPageSize(), req.getBranch(), req.getFilePath()), + p -> gitRpcClient.logPageApp(p, req.getPage(), req.getPageSize(), req.getBranch(), req.getFilePath())); + } + + // ======================== 7. diff ======================== + + @Operation(summary = "查看文件差异", description = "对比文件内容差异。source 取值:worktree(工作区 vs HEAD,默认)、staged(暂存区 vs HEAD)、commit(两个 commit 之间,需传 from/to;只传 from 时自动与前一个 commit 比较)。paths 指定文件范围,可空。") + @PostMapping(value = "/diff", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> diff(@RequestBody GitDiffReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.diffTaskAgent(cId, userId, req.getSource(), req.getFrom(), req.getTo(), req.getPaths()), + projectId -> gitRpcClient.diffPageApp(projectId, req.getSource(), req.getFrom(), req.getTo(), req.getPaths())); + } + + // ======================== 7.5 file-content ======================== + + @Operation(summary = "获取指定版本的文件内容", description = "获取指定 git 版本的文件内容,用于 Monaco Diff Editor 等场景的左右对比。ref 取值:worktree(工作区文件)、staged(暂存区文件)、HEAD(最新提交)、commit hash、HEAD~1 等。") + @PostMapping(value = "/file-content", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> fileContent(@RequestBody GitFileContentReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.fileContentTaskAgent(cId, userId, req.getRef(), req.getFilePath()), + projectId -> gitRpcClient.fileContentPageApp(projectId, req.getRef(), req.getFilePath())); + } + + // ======================== 8. reset ======================== + + @Operation(summary = "重置到指定版本", description = "移动 HEAD 到 target 版本。soft: 后续 commit 的改动保留在暂存区;mixed(默认): 改动变为 unstaged;hard: 暂存区和工作区全部恢复到 target,后续改动丢失。返回 previousHead 可用于再次 reset 回原版本。") + @PostMapping(value = "/reset", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> reset(@RequestBody GitResetReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.resetTaskAgent(cId, userId, req.getTarget(), req.getMode()), + projectId -> gitRpcClient.resetPageApp(projectId, req.getTarget(), req.getMode())); + } + + // ======================== 8.5 revert ======================== + + @Operation(summary = "回退到指定版本(保留历史)", description = "新建一个 commit 使文件树等于 target 版本,HEAD 向前推进,历史完整保留。与 reset 的区别:reset 会丢掉 target 之后的 commit,revert 不会。要求工作区 clean(未跟踪文件除外)。返回 previousHead 可用于审计。") + @PostMapping(value = "/revert", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> revert(@RequestBody GitRevertReq req) { + String authorName = getUser().getUserName(); + String authorEmail = getUser().getEmail(); + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.revertTaskAgent(cId, userId, req.getTarget(), req.getMessage(), authorName, authorEmail), + projectId -> gitRpcClient.revertPageApp(projectId, req.getTarget(), req.getMessage(), authorName, authorEmail)); + } + + // ======================== 9. checkout ======================== + + @Operation(summary = "检出目标版本文件", description = "将 target 版本的文件内容恢复到工作区和暂存区,HEAD 不移动(commit 历史不变)。暂存区中的变更是后续 commit 改动的反向,可直接 commit 生成一个回滚提交。") + @PostMapping(value = "/checkout", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> checkout(@RequestBody GitCheckoutReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.checkoutTaskAgent(cId, userId, req.getTarget()), + projectId -> gitRpcClient.checkoutPageApp(projectId, req.getTarget())); + } + + // ======================== 11. tags ======================== + + @Operation(summary = "查看标签列表", description = "返回所有 tag 名称及其对应的 commit hash") + @PostMapping(value = "/tags", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> tags(@RequestBody GitBaseReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + gitRpcClient::tagsTaskAgent, gitRpcClient::tagsPageApp); + } + + // ======================== 12. tag-create ======================== + + @Operation(summary = "创建标签", description = "在当前 HEAD 上创建轻量标签或附注标签") + @PostMapping(value = "/tag-create", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> tagCreate(@RequestBody GitTagReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.tagCreateTaskAgent(cId, userId, req.getTagName(), req.getMessage()), + projectId -> gitRpcClient.tagCreatePageApp(projectId, req.getTagName(), req.getMessage())); + } + + // ======================== 13. tag-delete ======================== + + @Operation(summary = "删除标签", description = "删除指定名称的标签") + @PostMapping(value = "/tag-delete", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> tagDelete(@RequestBody GitTagReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.tagDeleteTaskAgent(cId, userId, req.getTagName()), + projectId -> gitRpcClient.tagDeletePageApp(projectId, req.getTagName())); + } + + // ======================== 14. branches ======================== + + @Operation(summary = "查看分支列表", description = "返回所有本地分支名称,标注当前所在分支") + @PostMapping(value = "/branches", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> branches(@RequestBody GitBaseReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + gitRpcClient::branchesTaskAgent, gitRpcClient::branchesPageApp); + } + + // ======================== 15. branch-create ======================== + + @Operation(summary = "创建分支", description = "基于 startPoint(commit hash / tag / 分支名)创建新分支,startPoint 为空时基于当前 HEAD") + @PostMapping(value = "/branch-create", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> branchCreate(@RequestBody GitBranchReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.branchCreateTaskAgent(cId, userId, req.getBranchName(), req.getStartPoint()), + projectId -> gitRpcClient.branchCreatePageApp(projectId, req.getBranchName(), req.getStartPoint())); + } + + // ======================== 16. branch-switch ======================== + + @Operation(summary = "切换分支", description = "切换到指定分支,工作区必须干净(无未提交修改)才能切换") + @PostMapping(value = "/branch-switch", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> branchSwitch(@RequestBody GitBranchReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.branchSwitchTaskAgent(cId, userId, req.getBranchName()), + projectId -> gitRpcClient.branchSwitchPageApp(projectId, req.getBranchName())); + } + + // ======================== 17. branch-delete ======================== + + @Operation(summary = "删除分支", description = "删除指定分支,force=true 强制删除未合并的分支") + @PostMapping(value = "/branch-delete", produces = MediaType.APPLICATION_JSON_VALUE) + public ReqResult> branchDelete(@RequestBody GitBranchReq req) { + return dispatch(req.getWorkspaceType(), req.getCId(), req.getProjectId(), + (cId, userId) -> gitRpcClient.branchDeleteTaskAgent(cId, userId, req.getBranchName(), req.getForce()), + projectId -> gitRpcClient.branchDeletePageApp(projectId, req.getBranchName(), req.getForce())); + } + + // ======================== 权限校验 ======================== + + private void checkPageAppAuth(Long projectId) { + CustomPageDto pageDto = iCustomPageRpcService.queryDetail(projectId); + if (pageDto == null) { + throw new IllegalArgumentException("Project not found"); + } + spacePermissionService.checkSpaceUserPermission(pageDto.getSpaceId()); + } + + // ======================== dispatch ======================== + + private ReqResult> dispatch( + String workspaceType, Long cId, Long projectId, + BiFunction> taskAgentFn, + Function> pageAppFn) { + + Map result; + if ("pageApp".equals(workspaceType)) { + checkPageAppAuth(projectId); + result = pageAppFn.apply(projectId); + } else { + Long conversationUserId = conversationPermissionChecker.check(cId); + result = taskAgentFn.apply(cId, conversationUserId); + } + return wrapResult(result); + } + + private ReqResult> wrapResult(Map result) { + if (result == null) { + return ReqResult.error("Git operation failed"); + } + Object successObj = result.get("success"); + Object codeObj = result.get("code"); + boolean success; + if (successObj instanceof Boolean) { + success = (Boolean) successObj; + } else { + String code = codeObj != null ? codeObj.toString() : ReqResult.SUCCESS; + success = ReqResult.SUCCESS.equals(code); + } + if (!success) { + String message = result.getOrDefault("message", "Git operation failed").toString(); + String code = codeObj != null ? codeObj.toString() : "9999"; + return ReqResult.create(code, message, null); + } + return ReqResult.success(result); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/HomeController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/HomeController.java index a146aafa..dcf6b882 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/HomeController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/HomeController.java @@ -3,11 +3,14 @@ package com.xspaceagi.agent.web.ui.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.xspaceagi.agent.core.adapter.application.AgentApplicationService; import com.xspaceagi.agent.core.adapter.application.PublishApplicationService; +import com.xspaceagi.agent.core.adapter.application.RecommendApplicationService; import com.xspaceagi.agent.core.adapter.dto.PublishedDto; import com.xspaceagi.agent.core.adapter.dto.PublishedQueryDto; import com.xspaceagi.agent.core.adapter.dto.UserAgentDto; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; import com.xspaceagi.agent.core.adapter.repository.UserAgentSortRepository; import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; import com.xspaceagi.agent.core.adapter.repository.entity.UserAgentSort; import com.xspaceagi.agent.web.ui.controller.base.BaseController; import com.xspaceagi.agent.web.ui.controller.dto.HomeSortConfigDto; @@ -57,6 +60,9 @@ public class HomeController extends BaseController { @Resource private UserAgentSortRepository userAgentSortRepository; + @Resource + private RecommendApplicationService recommendApplicationService; + @Operation(summary = "分类以及智能体排序更新") @RequestMapping(path = "/sort/update", method = RequestMethod.POST) public ReqResult updateHomeSortConfig(@RequestBody HomeSortConfigDto homeSortConfigDto) { @@ -88,7 +94,7 @@ public class HomeController extends BaseController { List usedAgentDtos = agentApplicationService.queryRecentUseList(RequestContext.get().getUserId(), 100); Map usedAgentMap = usedAgentDtos.stream().collect(Collectors.toMap(UserAgentDto::getAgentId, config -> config, (c1, c2) -> c1)); HomeItemListDto homeItemListDto = new HomeItemListDto(); - List userAgentDtos = agentApplicationService.queryCollectionList(RequestContext.get().getUserId(), 1, 20); + List userAgentDtos = agentApplicationService.queryCollectionList(RequestContext.get().getUserId(), 1, 30); if (!userAgentDtos.isEmpty()) { List homeItemDtos = userAgentDtos.stream().map(userAgentDto -> { HomeItemDto homeItemDto = new HomeItemDto(); @@ -118,8 +124,13 @@ public class HomeController extends BaseController { } TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig(); - if (CollectionUtils.isNotEmpty(tenantConfigDto.getRecommendAgentIds())) { - List publishedDtos = publishApplicationService.queryPublishedList(Published.TargetType.Agent, tenantConfigDto.getRecommendAgentIds()); + List recommendAgentIds = recommendApplicationService.list(TargetRecommend.RecType.Official.name(), TargetRecommend.TargetType.Agent.name()).stream().map(TargetRecommendResponse::getTargetId).collect(Collectors.toList()); + recommendAgentIds = recommendAgentIds.isEmpty() ? tenantConfigDto.getRecommendAgentIds() : recommendAgentIds; + if (CollectionUtils.isNotEmpty(recommendAgentIds)) { + List publishedDtos = publishApplicationService.queryPublishedList(Published.TargetType.Agent, recommendAgentIds); + //根据recommendAgentIds的顺序对publishedDtos进行排序 + final List recommendAgentIds0 = recommendAgentIds; + publishedDtos = publishedDtos.stream().sorted(Comparator.comparing(publishedDto -> recommendAgentIds0.indexOf(publishedDto.getTargetId()))).toList(); List homeItemDtos = publishedDtos.stream().map(publishedDto -> convertToHomeItemDto(publishedDto, userAgentSortMap.get(HomeCategoryDto.HomeCategoryTypeEnum.AGENT_RECOMMEND.name()), usedAgentMap)).collect(Collectors.toList()); homeItemDtos.sort(Comparator.comparing(HomeItemDto::getSort)); Integer sort = getCategorySort(userAgentCategorySortMap, HomeCategoryDto.HomeCategoryTypeEnum.AGENT_RECOMMEND.name()); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ModelManageController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ModelManageController.java index 15bffb2a..008d670b 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ModelManageController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ModelManageController.java @@ -2,6 +2,7 @@ package com.xspaceagi.agent.web.ui.controller; import com.xspaceagi.agent.core.adapter.application.ModelApplicationService; import com.xspaceagi.agent.core.adapter.dto.ModelConfigAddDto; +import com.xspaceagi.agent.core.adapter.dto.SortUpdateDTO; import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; import com.xspaceagi.agent.core.adapter.repository.entity.ModelConfig; import com.xspaceagi.agent.core.spec.enums.ModelTypeEnum; @@ -12,8 +13,8 @@ import com.xspaceagi.system.application.service.SysSubjectPermissionApplicationS import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.ReqResult; -import com.xspaceagi.system.spec.enums.PermissionSubjectTypeEnum; import com.xspaceagi.system.spec.enums.ErrorCodeEnum; +import com.xspaceagi.system.spec.enums.PermissionSubjectTypeEnum; import com.xspaceagi.system.spec.exception.BizException; import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; import io.swagger.v3.oas.annotations.Operation; @@ -52,6 +53,13 @@ public class ModelManageController extends BaseController { return ReqResult.success(); } + @PostMapping("/sort/update") + @Operation(summary = "更新模型排序") + public ReqResult updatePackageSort(@RequestBody List updateDTOS) { + modelApplicationService.updateModelSort(updateDTOS); + return ReqResult.success(); + } + @RequireResource(MODEL_MANAGE_MODIFY) @Operation(summary = "更新模型管控状态") @RequestMapping(path = "/{modelId}/accessControl/{status}", method = RequestMethod.POST) diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ProjectController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ProjectController.java new file mode 100644 index 00000000..e93b3498 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ProjectController.java @@ -0,0 +1,68 @@ +package com.xspaceagi.agent.web.ui.controller; + +import com.xspaceagi.agent.core.adapter.application.ProjectApplicationService; +import com.xspaceagi.agent.core.adapter.dto.ProjectCreateDTO; +import com.xspaceagi.agent.core.adapter.dto.ProjectCreateResultDTO; +import com.xspaceagi.system.application.dto.SpaceDto; +import com.xspaceagi.system.application.service.SpaceApplicationService; +import com.xspaceagi.system.infra.dao.entity.Space; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.dto.ReqResult; +import com.xspaceagi.system.spec.exception.BizException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Tag(name = "项目相关接口") +@RestController +@RequestMapping("/api/project") +@Slf4j +public class ProjectController { + + public static final String DEFAULT_PROJECT_EN_NAME = "Unnamed project"; + + public static final String DEFAULT_PROJECT_CN_NAME = "未命名项目"; + + @Resource + private SpacePermissionService spacePermissionService; + + @Resource + private SpaceApplicationService spaceApplicationService; + + @Resource + private ProjectApplicationService projectApplicationService; + + @Operation(summary = "创建项目") + @RequestMapping(path = "/create", method = RequestMethod.POST) + public ReqResult create(@RequestBody ProjectCreateDTO projectCreateDTO) { + if (projectCreateDTO.getSpaceId() != null) { + spacePermissionService.checkSpaceUserPermission(projectCreateDTO.getSpaceId()); + } else { + List spaces = spaceApplicationService.queryListByUserId(RequestContext.get().getUserId()); + if (spaces.isEmpty()) { + throw new BizException("Invalid user data"); + } + SpaceDto spaceDto1 = spaces.stream().filter(spaceDto -> spaceDto.getType() == Space.Type.Personal).findFirst().orElse(spaces.get(0)); + projectCreateDTO.setSpaceId(spaceDto1.getId()); + } + projectCreateDTO.setCreatorId(RequestContext.get().getUserId()); + if (StringUtils.isBlank(projectCreateDTO.getName())) { + if ("en-US".equalsIgnoreCase(RequestContext.get().getLang())) { + projectCreateDTO.setName(DEFAULT_PROJECT_EN_NAME); + } else { + projectCreateDTO.setName(DEFAULT_PROJECT_CN_NAME); + } + } + return ReqResult.success(projectApplicationService.createProject(projectCreateDTO)); + } + +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishController.java index c115aa61..79a1d948 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishController.java @@ -6,23 +6,20 @@ import com.xspaceagi.agent.core.adapter.dto.*; import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto; import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; import com.xspaceagi.agent.core.adapter.repository.entity.PublishApply; import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; import com.xspaceagi.agent.core.domain.service.PublishDomainService; -import com.xspaceagi.agent.core.spec.utils.MarkdownExtractUtil; import com.xspaceagi.agent.web.ui.controller.base.BaseController; import com.xspaceagi.agent.web.ui.controller.dto.*; -import com.xspaceagi.sandbox.SandboxRequestAttributes; +import com.xspaceagi.sandbox.SandboxUtils; import com.xspaceagi.system.application.dto.SpaceDto; -import com.xspaceagi.system.application.dto.SpaceUserDto; -import com.xspaceagi.system.application.dto.TenantConfigDto; import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.service.SpaceApplicationService; -import com.xspaceagi.system.application.service.SysUserPermissionCacheService; import com.xspaceagi.system.application.service.UserApplicationService; -import com.xspaceagi.system.infra.dao.entity.Space; -import com.xspaceagi.system.infra.dao.entity.SpaceUser; import com.xspaceagi.system.sdk.permission.SpacePermissionService; +import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.ReqResult; import com.xspaceagi.system.spec.enums.ErrorCodeEnum; @@ -37,20 +34,15 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.util.Assert; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import static com.xspaceagi.system.spec.enums.ResourceEnum.*; +import static com.xspaceagi.system.spec.enums.ResourceEnum.PUBLISH_AUDIT_QUERY_DETAIL; @Tag(name = "发布相关接口") @RestController @@ -86,191 +78,33 @@ public class PublishController extends BaseController { private UserApplicationService userApplicationService; @Resource - private ModelApplicationService modelApplicationService; + private ConversationApplicationService conversationApplicationService; @Resource - private SysUserPermissionCacheService sysUserPermissionCacheService; + private AgentWorkspaceApplicationService agentWorkspaceApplicationService; + + @Resource + private RecommendApplicationService recommendApplicationService; // 因为此接口是聚合接口,不通过@RequireResource 校验权限,在接口实现中区分类型后校验权限 @Operation(summary = "提交发布申请") @RequestMapping(path = "/apply", method = RequestMethod.POST) public ReqResult publishApply(HttpServletRequest request, @RequestBody PublishApplySubmitDto publishApplySubmitDto) { - if (isSandboxSource(request)) { + if (SandboxUtils.isSandboxRequest(request)) { if (StringUtils.isBlank(publishApplySubmitDto.getCategory())) { publishApplySubmitDto.setCategory("Other"); } if (CollectionUtils.isNotEmpty(publishApplySubmitDto.getItems())) { for (PublishApplySubmitDto.PublishItem item : publishApplySubmitDto.getItems()) { if (item != null && item.getScope() == Published.PublishScope.Space && item.getSpaceId() == null) { - item.setSpaceId(getPersonalSpaceId()); + Long personalSpaceId = spaceApplicationService.getPersonalSpaceId(RequestContext.get().getUserId()); + item.setSpaceId(personalSpaceId); } } } } - //整体有两个地方做权限校验,一是校验有没有权限发布出去;而是校验有没有目标空间的发布权限 - Object targetConfig = checkPermissionAndReturnTargetConfig(publishApplySubmitDto.getTargetType(), publishApplySubmitDto.getTargetId()); - // 按目标类型校验资源权限 - checkPublishResourcePermission(publishApplySubmitDto.getTargetType(), targetConfig); - Assert.notNull(publishApplySubmitDto.getCategory(), "Please select a category"); - if (CollectionUtils.isEmpty(publishApplySubmitDto.getItems())) { - throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentPublishScopeNotSelected); - } - if (targetConfig instanceof PluginDto || targetConfig instanceof WorkflowConfigDto) { - String name = targetConfig instanceof PluginDto - ? ((PluginDto) targetConfig).getName() - : ((WorkflowConfigDto) targetConfig).getName(); - name = name == null ? "" : name; - try { - //判断是否为英文 - name = name.trim().toLowerCase().replace(" ", "_").replace("__", "_"); - if (StringUtils.isNotBlank(name) && !name.matches("[a-zA-Z0-9_]+")) { - EnNameDto enNameDto = modelApplicationService.call(name, new ParameterizedTypeReference() { - }); - if (enNameDto != null && StringUtils.isNotBlank(enNameDto.getEnName())) { - if (targetConfig instanceof PluginDto) { - ((PluginDto) targetConfig).setFunctionName(enNameDto.getEnName()); - } else { - ((WorkflowConfigDto) targetConfig).setFunctionName(enNameDto.getEnName()); - } - } - } - } catch (Exception e) { - //忽略 - log.error("Exception when calling model conversion API", e); - } - } - if (targetConfig instanceof SkillConfigDto skillConfig) { - List files = skillConfig.getFiles(); - if (CollectionUtils.isNotEmpty(files)) { - List keyFiles = files.stream().filter(file -> "SKILL.MD".equalsIgnoreCase(file.getName()) && !Boolean.TRUE.equals(file.getIsDir())).toList(); - if (CollectionUtils.isNotEmpty(keyFiles)) { - for (SkillFileDto file : keyFiles) { - String name = MarkdownExtractUtil.extractFieldValue(file.getContents(), "name"); - if (StringUtils.isNotBlank(name)) { - skillConfig.setEnName(name); - break; - } - } - } - } - } - - TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig(); - String name = null; - String description = null; - String icon = null; - Integer publishAudit = YesOrNoEnum.Y.getKey(); - Long spaceId = null; - String agentType = null; - Object ext = null; - if (publishApplySubmitDto.getTargetType() == Published.TargetType.Agent) { - assert targetConfig instanceof AgentConfigDto; - AgentConfigDto agentConfigDto = (AgentConfigDto) targetConfig; - //私有电脑的agent不允许发布到广场 - if (agentConfigDto.getExtra() != null && agentConfigDto.getExtra().get("private") != null - && publishApplySubmitDto.getItems().stream().anyMatch(item -> item.getScope() == Published.PublishScope.Tenant)) { - throw new BizException(I18nUtil.systemMessage("Backend.Publish.AgentPrivateCannotPublishToSquare")); - } - - // 构建代理MCP,id存储在agentConfigDto的extra字段中 - agentApplicationService.buildProxyMcp(agentConfigDto, false); - name = agentConfigDto.getName(); - description = agentConfigDto.getDescription(); - icon = agentConfigDto.getIcon(); - spaceId = agentConfigDto.getSpaceId(); - publishAudit = tenantConfigDto.getAgentPublishAudit(); - agentType = agentConfigDto.getType(); - } - if (publishApplySubmitDto.getTargetType() == Published.TargetType.Workflow) { - assert targetConfig instanceof WorkflowConfigDto; - WorkflowConfigDto workflowConfigDto = (WorkflowConfigDto) targetConfig; - name = workflowConfigDto.getName(); - description = workflowConfigDto.getDescription(); - icon = workflowConfigDto.getIcon(); - spaceId = workflowConfigDto.getSpaceId(); - publishAudit = tenantConfigDto.getWorkflowPublishAudit(); - } - if (publishApplySubmitDto.getTargetType() == Published.TargetType.Plugin) { - assert targetConfig instanceof PluginDto; - PluginDto pluginDto = (PluginDto) targetConfig; - name = pluginDto.getName(); - description = pluginDto.getDescription(); - icon = pluginDto.getIcon(); - spaceId = pluginDto.getSpaceId(); - publishAudit = tenantConfigDto.getPluginPublishAudit(); - } - if (publishApplySubmitDto.getTargetType() == Published.TargetType.Skill) { - assert targetConfig instanceof SkillConfigDto; - SkillConfigDto skillConfigDto = (SkillConfigDto) targetConfig; - name = skillConfigDto.getName(); - description = skillConfigDto.getDescription(); - icon = skillConfigDto.getIcon(); - ext = skillConfigDto.getExt(); - spaceId = skillConfigDto.getSpaceId(); - publishAudit = tenantConfigDto.getSkillPublishAudit(); - } - - List userSpaceIds = obtainAuthSpaceIds(); - //参数检查 - for (PublishApplySubmitDto.PublishItem publishItem : publishApplySubmitDto.getItems()) { - Assert.notNull(publishItem.getScope(), "Publish scope is required"); - if (publishItem.getScope() == Published.PublishScope.Space) { - Assert.notNull(publishItem.getSpaceId(), "Space ID is required"); - SpaceDto spaceDto = spaceApplicationService.queryById(publishItem.getSpaceId()); - if (spaceDto == null) { - throw new BizException(I18nUtil.systemMessage("Backend.Publish.SpaceNotFound", publishItem.getSpaceId().toString())); - } - if (!userSpaceIds.contains(publishItem.getSpaceId())) { - throw new BizException(I18nUtil.systemMessage("Backend.Publish.SpaceNoPermission", publishItem.getSpaceId().toString())); - } - if (!spaceDto.getId().equals(spaceId) && spaceDto.getReceivePublish() != YesOrNoEnum.Y.getKey()) { - throw new BizException(I18nUtil.systemMessage("Backend.Publish.SpaceReceivePublishDisabled", publishItem.getSpaceId().toString())); - } - } - } - - List tenantPublishApplyDtos = new ArrayList<>(); - List spacePublishApplyDtos = new ArrayList<>(); - String message = I18nUtil.systemMessage("Backend.Publish.Success"); - for (PublishApplySubmitDto.PublishItem publishItem : publishApplySubmitDto.getItems()) { - PublishApplyDto publishApplyDto = new PublishApplyDto(); - publishApplyDto.setApplyUser((UserDto) RequestContext.get().getUser()); - publishApplyDto.setTargetType(publishApplySubmitDto.getTargetType()); - if (agentType != null) { - publishApplyDto.setTargetSubType(Published.TargetSubType.valueOf(agentType)); - } - publishApplyDto.setTargetId(publishApplySubmitDto.getTargetId()); - publishApplyDto.setChannels(List.of(Published.PublishChannel.System)); - publishApplyDto.setRemark(publishApplySubmitDto.getRemark()); - publishApplyDto.setName(name); - publishApplyDto.setDescription(description); - publishApplyDto.setIcon(icon); - publishApplyDto.setExt(ext); - publishApplyDto.setTargetConfig(targetConfig); - publishApplyDto.setSpaceId(spaceId); - publishApplyDto.setScope(publishItem.getScope()); - publishApplyDto.setCategory(publishApplySubmitDto.getCategory()); - publishApplyDto.setAllowCopy(publishItem.getAllowCopy()); - publishApplyDto.setOnlyTemplate(publishItem.getOnlyTemplate()); - if (publishItem.getScope() == Published.PublishScope.Space) { - publishApplyDto.setSpaceId(publishItem.getSpaceId()); - } - Long applyId = publishApplicationService.publishApply(publishApplyDto); - publishApplyDto.setId(applyId); - if (publishItem.getScope() == Published.PublishScope.Space) { - spacePublishApplyDtos.add(publishApplyDto); - } else { - tenantPublishApplyDtos.add(publishApplyDto); - } - } - - if (publishAudit == null || publishAudit.equals(YesOrNoEnum.N.getKey()) || CollectionUtils.isEmpty(tenantPublishApplyDtos)) { - publishApplicationService.publish(publishApplySubmitDto.getTargetType(), publishApplySubmitDto.getTargetId(), Published.PublishScope.Tenant, tenantPublishApplyDtos); - } else { - message = I18nUtil.systemMessage("Backend.Publish.AuditPending"); - } - publishApplicationService.publish(publishApplySubmitDto.getTargetType(), publishApplySubmitDto.getTargetId(), Published.PublishScope.Space, spacePublishApplyDtos); + String message = publishApplicationService.publishOrApply(publishApplySubmitDto); return ReqResult.create(ReqResult.SUCCESS, message, message); } @@ -281,14 +115,14 @@ public class PublishController extends BaseController { public ReqResult> queryPublishItems(@RequestBody PublishQueryDto publishQueryDto) { Assert.notNull(publishQueryDto.getTargetType(), "targetType is required"); Assert.notNull(publishQueryDto.getTargetId(), "targetId is required"); - checkPermissionAndReturnTargetConfig(publishQueryDto.getTargetType(), publishQueryDto.getTargetId()); + publishApplicationService.checkPermissionAndReturnTargetConfig(publishQueryDto.getTargetType(), publishQueryDto.getTargetId()); List publishApplyList = publishDomainService.queryPublishApplyingList(publishQueryDto.getTargetType(), publishQueryDto.getTargetId()); List publishedList = publishDomainService.queryPublishedList(publishQueryDto.getTargetType(), List.of(publishQueryDto.getTargetId())); //publishApplyList提取userIds List userIds = publishApplyList.stream().map(PublishApply::getApplyUserId).collect(Collectors.toList()); - userIds.addAll(publishedList.stream().map(Published::getUserId).collect(Collectors.toList())); - List userDtos = userApplicationService.queryUserListByIds(userIds); - Map userMap = userDtos.stream().collect(Collectors.toMap(UserDto::getId, userDto -> userDto)); + userIds.addAll(publishedList.stream().map(Published::getUserId).toList()); + List users = userApplicationService.queryUserListByIds(userIds); + Map userMap = users.stream().collect(Collectors.toMap(UserDto::getId, userDto -> userDto)); List publishItemDtoList = publishedList.stream().map(published -> { PublishItemDto publishItemDto = new PublishItemDto(); publishItemDto.setPublishId(published.getId()); @@ -298,6 +132,7 @@ public class PublishController extends BaseController { publishItemDto.setAllowCopy(published.getAllowCopy()); publishItemDto.setOnlyTemplate(published.getOnlyTemplate()); publishItemDto.setSpaceId(published.getSpaceId()); + publishItemDto.setCategory(published.getCategory()); UserDto userDto = userMap.get(published.getUserId()); if (userDto != null) { PublishUserDto publishUserDto = PublishUserDto.builder() @@ -318,6 +153,7 @@ public class PublishController extends BaseController { publishItemDto.setPublishDate(publishApply.getModified()); publishItemDto.setAllowCopy(publishApply.getAllowCopy()); publishItemDto.setOnlyTemplate(publishApply.getOnlyTemplate()); + publishItemDto.setCategory(publishApply.getCategory()); UserDto userDto = userMap.get(publishApply.getApplyUserId()); if (userDto != null) { PublishUserDto publishUserDto = PublishUserDto.builder() @@ -329,7 +165,7 @@ public class PublishController extends BaseController { publishItemDto.setPublishUser(publishUserDto); } return publishItemDto; - }).collect(Collectors.toList()); + }).toList(); publishItemDtoList.addAll(0, publishItemDtoList0); List spaceDtos = spaceApplicationService.queryByIds(publishItemDtoList.stream().map(PublishItemDto::getSpaceId).collect(Collectors.toList())); //以spaceId为key转map @@ -384,7 +220,51 @@ public class PublishController extends BaseController { if (skillConfigDto.getSpaceId() == null) { throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentSkillConfigParseFailed); } + + boolean isSandboxSkill = skillConfigDto.getDevAgentConversationId() != null; + if (isSandboxSkill) { + skillConfigDto.setFiles(null); + } id = skillApplicationService.copySkill(skillConfigDto, templateCopyDto.getTargetSpaceId()); + + // 沙盒技能复制:需要为新技能创建开发会话,并复制沙箱工作空间 + if (isSandboxSkill) { + try { + TargetRecommendResponse recommendResponse = recommendApplicationService.list(TargetRecommend.RecType.ChatBoxNav.name(), TargetRecommend.TargetType.Agent.name()).stream().filter(targetRecommendResponse -> targetRecommendResponse.getFunctionType().equals(TargetRecommend.FunctionType.SkillDev.name())).findFirst().orElse(null); + if (recommendResponse == null) { + throw new IllegalArgumentException("Develop agent not config"); + } + + Long currentUserId = RequestContext.get().getUserId(); + + // 创建开发会话(新技能的 creatorId 是当前用户) + Long newCId = conversationApplicationService.createConversationForProjectDevelopment( + RequestContext.get().getTenantId(), + currentUserId, + templateCopyDto.getTargetSpaceId(), + recommendResponse.getTargetId(), + Published.TargetType.Skill.name(), + id + ).getId(); + + // 更新新技能的 devAgentConversationId + SkillConfigDto updateDto = new SkillConfigDto(); + updateDto.setId(id); + updateDto.setDevAgentConversationId(newCId); + skillApplicationService.update(updateDto, false); + + // 上传技能文件到新工作空间,并初始化 git + agentWorkspaceApplicationService.uploadZipToWorkspace( + currentUserId, + newCId, + skillConfigDto.getZipFileUrl()); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + log.error("Failed to setup sandbox workspace for copied skill, skillId={}", id, e); + throw new RuntimeException("Failed to setup sandbox workspace for copied skill", e); + } + } } return ReqResult.success(id); } @@ -446,109 +326,31 @@ public class PublishController extends BaseController { return ReqResult.success(); } - /** - * 根据发布目标类型校验当前用户是否具备相应的发布资源权限 - */ - private void checkPublishResourcePermission(Published.TargetType targetType, Object targetConfig) { - Long userId = RequestContext.get().getUserId(); - if (userId == null || targetType == null || targetConfig == null) { - return; + @RequireResource(PUBLISH_AUDIT_QUERY_DETAIL) + @Operation(summary = "发布申请中的技能详情接口") + @RequestMapping(path = "/skill/{skillId}", method = RequestMethod.GET) + public ReqResult skillDetail(@PathVariable Long skillId) { + List publishApplyList = publishDomainService.queryPublishApplyingList(Published.TargetType.Skill, skillId); + if (CollectionUtils.isEmpty(publishApplyList)) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentSkillOffline); } - - String resourceCode = null; - if (targetType == Published.TargetType.Agent) { - AgentConfigDto agentConfigDto = (AgentConfigDto) targetConfig; - String type = agentConfigDto.getType(); - if (Published.TargetSubType.PageApp.name().equals(type)) { - resourceCode = PAGE_APP_PUBLISH.getCode(); - } else { - resourceCode = AGENT_PUBLISH.getCode(); - } - } else if (targetType == Published.TargetType.Plugin || targetType == Published.TargetType.Workflow) { - resourceCode = COMPONENT_LIB_PUBLISH.getCode(); - } else if (targetType == Published.TargetType.Skill) { - resourceCode = SKILL_PUBLISH.getCode(); - } - - if (resourceCode != null) { - sysUserPermissionCacheService.checkResourcePermissionAny(userId, List.of(resourceCode)); + PublishApply publishApply = publishApplyList.get(0); + SkillConfigDto skillConfigDto = skillApplicationService.parsePublishedSkillConfig(publishApply.getConfig(), publishApply.getExt()); + if (skillConfigDto == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentSkillConfigParseFailed); } + SkillDetailDto skillDetailDto = new SkillDetailDto(); + skillDetailDto.setId(skillId); + skillDetailDto.setName(skillConfigDto.getName()); + skillDetailDto.setDescription(skillConfigDto.getDescription()); + skillDetailDto.setIcon(skillConfigDto.getIcon()); + skillDetailDto.setFiles(skillConfigDto.getFiles()); + skillDetailDto.setExt(publishApply.getExt()); + skillDetailDto.setRemark(publishApply.getRemark()); + skillDetailDto.setCreated(publishApply.getCreated()); + skillDetailDto.setAllowCopy(publishApply.getAllowCopy()); + skillDetailDto.setCategory(publishApply.getCategory()); + return ReqResult.success(skillDetailDto); } - private Object checkPermissionAndReturnTargetConfig(Published.TargetType targetType, Long targetId) { - Assert.notNull(targetType, "targetType is required"); - Assert.notNull(targetId, "targetId is required"); - Long spaceId = null; - Long creatorId = null; - Object targetConfig = null; - if (targetType == Published.TargetType.Agent) { - AgentConfigDto agentDto = agentApplicationService.queryById(targetId); - if (agentDto == null) { - throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentNotFoundAlt); - } - spaceId = agentDto.getSpaceId(); - creatorId = agentDto.getCreatorId(); - targetConfig = agentDto; - } - if (targetType == Published.TargetType.Plugin) { - PluginDto pluginDto = pluginApplicationService.queryById(targetId); - if (pluginDto == null) { - throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentOpenapiPluginNotFound); - } - spaceId = pluginDto.getSpaceId(); - creatorId = pluginDto.getCreatorId(); - targetConfig = pluginDto; - } - if (targetType == Published.TargetType.Workflow) { - WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryById(targetId); - if (workflowConfigDto == null) { - throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentWorkflowNotFound); - } - spaceId = workflowConfigDto.getSpaceId(); - creatorId = workflowConfigDto.getCreatorId(); - targetConfig = workflowConfigDto; - } - if (targetType == Published.TargetType.Skill) { - SkillConfigDto skillConfigDto = skillApplicationService.queryById(targetId); - if (skillConfigDto == null) { - throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentOpenapiSkillNotFound); - } - spaceId = skillConfigDto.getSpaceId(); - creatorId = skillConfigDto.getCreatorId(); - targetConfig = skillConfigDto; - } - Assert.notNull(spaceId, "spaceId is required"); - SpaceUserDto spaceUserDto = spaceApplicationService.querySpaceUser(spaceId, RequestContext.get().getUserId()); - if (spaceUserDto == null) { - throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.permissionDenied); - } - if (creatorId.equals(RequestContext.get().getUserId())) { - return targetConfig; - } - if (spaceUserDto.getRole() == SpaceUser.Role.User) { - throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.permissionDenied); - } - return targetConfig; - } - - private boolean isSandboxSource(HttpServletRequest request) { - if (request == null) { - return false; - } - Object src = request.getAttribute(SandboxRequestAttributes.REQUEST_SOURCE); - return SandboxRequestAttributes.SOURCE_SANDBOX.equals(src); - } - - private Long getPersonalSpaceId() { - Long userId = RequestContext.get().getUserId(); - List spaceDtos = spaceApplicationService.queryListByUserId(userId); - SpaceDto personalSpace = spaceDtos.stream() - .filter(spaceDto -> spaceDto.getType() == Space.Type.Personal) - .findFirst() - .orElse(null); - if (personalSpace == null) { - throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentUserNoPersonalSpace); - } - return personalSpace.getId(); - } } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishManageController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishManageController.java index 59e0089f..206c5ddd 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishManageController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishManageController.java @@ -121,9 +121,6 @@ public class PublishManageController { if (CollectionUtils.isNotEmpty(tenantConfigDto.getDefaultAgentIds())) { agentIds.addAll(tenantConfigDto.getDefaultAgentIds()); } - if (CollectionUtils.isNotEmpty(tenantConfigDto.getRecommendAgentIds())) { - agentIds.addAll(tenantConfigDto.getRecommendAgentIds()); - } if (CollectionUtils.isNotEmpty(agentIds) && StringUtils.isBlank(configSelectAgentQueryDto.getKw())) { List agentList = publishApplicationService.queryPublishedList(Published.TargetType.Agent, agentIds); if (CollectionUtils.isNotEmpty(agentList)) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishedController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishedController.java index cd5bc3b0..e9397dff 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishedController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/PublishedController.java @@ -7,8 +7,10 @@ import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto; import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; import com.xspaceagi.agent.core.adapter.repository.entity.Published; import com.xspaceagi.agent.core.adapter.repository.entity.PublishedStatistics; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; import com.xspaceagi.agent.core.domain.service.ConversationDomainService; import com.xspaceagi.agent.core.infra.rpc.CategoryRpcService; import com.xspaceagi.agent.core.infra.rpc.ResourcePricingRpcService; @@ -57,6 +59,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.util.Assert; import org.springframework.web.bind.annotation.*; import java.net.URLEncoder; @@ -114,6 +117,9 @@ public class PublishedController extends BaseController { @Resource private ResourcePricingRpcService resourcePricingRpcService; + @Resource + private RecommendApplicationService recommendApplicationService; + @Operation(summary = "广场-智能体与插件分类") @RequestMapping(path = "/category/list", method = RequestMethod.GET) public ReqResult> categoryList() { @@ -183,7 +189,15 @@ public class PublishedController extends BaseController { } if (publishedQueryDto.getShowRecommend() == null) { publishedQueryDto.setShowRecommend(true); + List recommendIds = recommendApplicationService.list(TargetRecommend.RecType.Official.name(), TargetRecommend.TargetType.Agent.name()).stream().map(TargetRecommendResponse::getTargetId).collect(Collectors.toList()); + publishedQueryDto.setRecommendIds(recommendIds); + if (recommendIds.isEmpty()) { + publishedQueryDto.setShowRecommend(false); + } } + + List excludeIds = recommendApplicationService.list(TargetRecommend.RecType.ChatBoxNav.name(), TargetRecommend.TargetType.Agent.name()).stream().map(TargetRecommendResponse::getTargetId).collect(Collectors.toList()); + publishedQueryDto.setExcludeIds(excludeIds); completeOfficialTargetIds(publishedQueryDto); SuperPage page = publishApplicationService.queryPublishedList(publishedQueryDto); page.getRecords().forEach(publishedDto -> { @@ -191,8 +205,11 @@ public class PublishedController extends BaseController { }); if (RequestContext.get().getUserId() != null) { - UserDataPermissionDto userDataPermission = userDataPermissionRpcService.getUserDataPermission(RequestContext.get().getUserId()); - page.getRecords().removeIf(publishedDto -> publishedDto.getAccessControl() != null && publishedDto.getAccessControl() == 1 && !userDataPermission.getAgentIds().contains(publishedDto.getTargetId())); + UserDto user = (UserDto) RequestContext.get().getUser(); + if (user.getRole() != User.Role.Admin) {//管理员展示列表 + UserDataPermissionDto userDataPermission = userDataPermissionRpcService.getUserDataPermission(RequestContext.get().getUserId()); + page.getRecords().removeIf(publishedDto -> publishedDto.getAccessControl() != null && publishedDto.getAccessControl() == 1 && !userDataPermission.getAgentIds().contains(publishedDto.getTargetId())); + } } else { page.getRecords().removeIf(publishedDto -> publishedDto.getAccessControl() != null && publishedDto.getAccessControl() == 1); } @@ -217,19 +234,28 @@ public class PublishedController extends BaseController { publishedQueryDto.setShowRecommend(false); TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig(); if (publishedQueryDto.getTargetType() == Published.TargetType.Agent) { - publishedQueryDto.setTargetIds(tenantConfigDto.getOfficialAgentIds()); + List recommendIds; + if (publishedQueryDto.getTargetSubType() != null && publishedQueryDto.getTargetSubType() == Published.TargetSubType.PageApp) { + recommendIds = recommendApplicationService.list(TargetRecommend.RecType.Official.name(), TargetRecommend.TargetType.PageApp.name()).stream().map(TargetRecommendResponse::getTargetId).collect(Collectors.toList()); + } else { + recommendIds = recommendApplicationService.list(TargetRecommend.RecType.Official.name(), TargetRecommend.TargetType.Agent.name()).stream().map(TargetRecommendResponse::getTargetId).collect(Collectors.toList()); + } + publishedQueryDto.setTargetIds(recommendIds.isEmpty() ? tenantConfigDto.getOfficialAgentIds() : recommendIds); } if (publishedQueryDto.getTargetType() == Published.TargetType.Plugin && tenantConfigDto.getOfficialPluginIds() != null) { List ids = parseToLongIds(tenantConfigDto.getOfficialPluginIds()); - publishedQueryDto.setTargetIds(ids); + List recommendIds = recommendApplicationService.list(TargetRecommend.RecType.Official.name(), TargetRecommend.TargetType.Plugin.name()).stream().map(TargetRecommendResponse::getTargetId).collect(Collectors.toList()); + publishedQueryDto.setTargetIds(recommendIds.isEmpty() ? ids : recommendIds); } if (publishedQueryDto.getTargetType() == Published.TargetType.Workflow && tenantConfigDto.getOfficialWorkflowIds() != null) { + List recommendIds = recommendApplicationService.list(TargetRecommend.RecType.Official.name(), TargetRecommend.TargetType.Workflow.name()).stream().map(TargetRecommendResponse::getTargetId).collect(Collectors.toList()); List ids = parseToLongIds(tenantConfigDto.getOfficialWorkflowIds()); - publishedQueryDto.setTargetIds(ids); + publishedQueryDto.setTargetIds(recommendIds.isEmpty() ? ids : recommendIds); } if (publishedQueryDto.getTargetType() == Published.TargetType.Skill && tenantConfigDto.getOfficialSkillIds() != null) { + List recommendIds = recommendApplicationService.list(TargetRecommend.RecType.Official.name(), TargetRecommend.TargetType.Skill.name()).stream().map(TargetRecommendResponse::getTargetId).collect(Collectors.toList()); List ids = parseToLongIds(tenantConfigDto.getOfficialSkillIds()); - publishedQueryDto.setTargetIds(ids); + publishedQueryDto.setTargetIds(recommendIds.isEmpty() ? ids : recommendIds); } if (CollectionUtils.isEmpty(publishedQueryDto.getTargetIds())) { publishedQueryDto.setTargetIds(List.of(-1L)); @@ -275,6 +301,33 @@ public class PublishedController extends BaseController { return ReqResult.success(page); } + @Operation(summary = "【新】已发布分组列表接口(广场以及弹框选择中全部插件)") + @RequestMapping(path = "/grouped/list", method = RequestMethod.POST) + public ReqResult> groupedPluginList(@RequestBody PublishedQueryDto publishedQueryDto) { + Assert.isTrue(publishedQueryDto.getTargetType() == Published.TargetType.Plugin || publishedQueryDto.getTargetType() == Published.TargetType.Workflow, "Invalid TargetType"); + if (publishedQueryDto.getAllowCopy() == null || publishedQueryDto.getAllowCopy().equals(YesOrNoEnum.N.getKey())) { + publishedQueryDto.setOnlyTemplate(YesOrNoEnum.N.getKey()); + } + if (publishedQueryDto.getLastTimestamp() == null) { + publishedQueryDto.setLastTimestamp(System.currentTimeMillis()); + } + if ("Plugin".equals(publishedQueryDto.getCategory()) || "Workflow".equals(publishedQueryDto.getCategory())) { + publishedQueryDto.setCategory(null); + } + //查询用户有权限的空间,限制访问空间,比如工作流查询全部知识库,要限制用户有权限的空间下的知识库 + if (publishedQueryDto.getSpaceId() != null) { + //查询用户有权限的空间,限制访问空间,比如工作流查询全部知识库,要限制用户有权限的空间下的知识库 + var spaceIds = this.obtainAuthSpaceIds(); + if (spaceIds == null || !spaceIds.contains(publishedQueryDto.getSpaceId())) { + publishedQueryDto.setSpaceId(null); + } + } + completeOfficialTargetIds(publishedQueryDto); + List page = publishApplicationService.queryGroupedPublishedList(publishedQueryDto); + page.forEach(publishedDto -> publishedDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(publishedDto.getIcon(), publishedDto.getName(), publishedDto.getType(), true))); + return ReqResult.success(page); + } + @Operation(summary = "已发布模板列表接口") @RequestMapping(path = "/template/list", method = RequestMethod.POST) public ReqResult> templateList(@RequestBody PublishedQueryDto publishedQueryDto) { @@ -655,6 +708,10 @@ public class PublishedController extends BaseController { }); } + /** + * 分页参数可能不准的原因: + * 同一个技能(同 targetId)如果既在用户有权限的空间发布、又在租户级别发布,就会查出多条 Published 记录。SQL LIMIT 10 拿到 10 行,去重后可能只剩 7 条,但返回的 size 仍然是 10。 + */ @Operation(summary = "查询技能列表-用于@技能") @RequestMapping(path = "/skill/list-for-at", method = RequestMethod.POST) public ReqResult> skillListForAt(@RequestBody PublishedQueryDto publishedQueryDto) { @@ -662,6 +719,7 @@ public class PublishedController extends BaseController { queryDto.setTargetType(Published.TargetType.Skill); queryDto.setPage(publishedQueryDto.getPage()); queryDto.setPageSize(publishedQueryDto.getPageSize()); + queryDto.setLastTimestamp(publishedQueryDto.getLastTimestamp()); queryDto.setKw(publishedQueryDto.getKw()); queryDto.setUsageScenarios(publishedQueryDto.getUsageScenarios()); queryDto.setJustReturnSpaceData(false); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/RecommendController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/RecommendController.java new file mode 100644 index 00000000..252594d3 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/RecommendController.java @@ -0,0 +1,90 @@ +package com.xspaceagi.agent.web.ui.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xspaceagi.agent.core.adapter.dto.recommend.RecommendHomeResponse; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; +import com.xspaceagi.agent.core.infra.dao.mapper.TargetRecommendMapper; +import com.xspaceagi.agent.web.ui.controller.base.BaseController; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.dto.ReqResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Tag(name = "推荐接口") +@RestController +@RequestMapping("/api/display/recommend") +@Slf4j +public class RecommendController extends BaseController { + + @Resource + private TargetRecommendMapper targetRecommendMapper; + + @Operation(summary = "获取首页和对话框推荐列表") + @RequestMapping(path = "/list", method = RequestMethod.GET) + public ReqResult list() { + Long tenantId = RequestContext.get().getTenantId(); + + List homeList = targetRecommendMapper.selectList( + new LambdaQueryWrapper() + .eq(TargetRecommend::getTenantId, tenantId) + .eq(TargetRecommend::getRecType, TargetRecommend.RecType.Home) + .orderByAsc(TargetRecommend::getSort) + ); + + List chatBoxNavList = targetRecommendMapper.selectList( + new LambdaQueryWrapper() + .eq(TargetRecommend::getTenantId, tenantId) + .eq(TargetRecommend::getRecType, TargetRecommend.RecType.ChatBoxNav) + .orderByAsc(TargetRecommend::getSort) + ); + + Map> recHome = homeList.stream() + .map(this::toResponse) + .collect(Collectors.groupingBy( + r -> r.getTargetType(), + LinkedHashMap::new, + Collectors.toList() + )); + + Map> recChatBoxNav = chatBoxNavList.stream() + .map(this::toResponse) + .collect(Collectors.groupingBy( + r -> r.getTargetType(), + LinkedHashMap::new, + Collectors.toList() + )); + + return ReqResult.success(RecommendHomeResponse.builder() + .recHome(recHome) + .recChatBoxNav(recChatBoxNav) + .build()); + } + + private TargetRecommendResponse toResponse(TargetRecommend entity) { + return TargetRecommendResponse.builder() + .id(entity.getId()) + .tenantId(entity.getTenantId()) + .targetType(entity.getTargetType() != null ? entity.getTargetType().name() : null) + .targetId(entity.getTargetId()) + .recType(entity.getRecType() != null ? entity.getRecType().name() : null) + .functionType(entity.getFunctionType() != null ? entity.getFunctionType().name() : null) + .label(entity.getLabel()) + .icon(entity.getIcon()) + .placeholder(entity.getPlaceholder()) + .sort(entity.getSort()) + .modified(entity.getModified()) + .created(entity.getCreated()) + .build(); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ResourceGroupController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ResourceGroupController.java new file mode 100644 index 00000000..acaf42cd --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/ResourceGroupController.java @@ -0,0 +1,136 @@ +package com.xspaceagi.agent.web.ui.controller; + +import com.xspaceagi.agent.core.adapter.application.ResourceGroupApplicationService; +import com.xspaceagi.agent.core.adapter.dto.ResourceGroupDto; +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation; +import com.xspaceagi.agent.web.ui.controller.dto.ResourceGroupQueryDto; +import com.xspaceagi.agent.web.ui.controller.dto.ResourceTargetDto; +import com.xspaceagi.system.application.util.DefaultIconUrlUtil; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; +import com.xspaceagi.system.spec.dto.ReqResult; +import com.xspaceagi.system.spec.enums.ErrorCodeEnum; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "资源分组相关接口") +@RestController +@RequestMapping("/api/resource/group") +@Slf4j +public class ResourceGroupController { + + @Resource + private ResourceGroupApplicationService resourceGroupApplicationService; + + @Resource + private SpacePermissionService spacePermissionService; + + @Operation(summary = "查询资源分组列表") + @RequestMapping(path = "/list", method = RequestMethod.POST) + public ReqResult> list(@RequestBody ResourceGroupQueryDto resourceGroupQueryDto) { + Assert.notNull(resourceGroupQueryDto, "resourceGroupQueryDto is null"); + Assert.notNull(resourceGroupQueryDto.getSpaceId(), "spaceId is null"); + spacePermissionService.checkSpaceUserPermission(resourceGroupQueryDto.getSpaceId()); + List resourceGroups = resourceGroupApplicationService.queryList(resourceGroupQueryDto.getName(), resourceGroupQueryDto.getTypes().stream().map(Published.TargetType::name).toList(), resourceGroupQueryDto.getSpaceId()); + resourceGroups.forEach(resourceGroup -> DefaultIconUrlUtil.setDefaultIconUrl(resourceGroup.getIcon(), resourceGroup.getName())); + return ReqResult.success(resourceGroups); + } + + @Operation(summary = "添加资源分组") + @RequestMapping(path = "/add", method = RequestMethod.POST) + public ReqResult add(@RequestBody ResourceGroupDto dto) { + if (dto.getSpaceId() == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "spaceId"); + } + spacePermissionService.checkSpaceUserPermission(dto.getSpaceId()); + Long id = resourceGroupApplicationService.add(dto); + return ReqResult.success(id); + } + + @Operation(summary = "更新资源分组") + @RequestMapping(path = "/update/{id}", method = RequestMethod.POST) + public ReqResult update(@PathVariable Long id, @RequestBody ResourceGroupDto dto) { + ResourceGroupDto exist = resourceGroupApplicationService.queryById(id); + if (exist == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.resourceDataNotFound); + } + spacePermissionService.checkSpaceUserPermission(exist.getSpaceId()); + dto.setId(id); + resourceGroupApplicationService.update(dto); + return ReqResult.success(); + } + + @Operation(summary = "删除资源分组") + @RequestMapping(path = "/delete/{id}", method = RequestMethod.POST) + public ReqResult delete(@PathVariable Long id) { + ResourceGroupDto exist = resourceGroupApplicationService.queryById(id); + if (exist == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.resourceDataNotFound); + } + spacePermissionService.checkSpaceUserPermission(exist.getSpaceId()); + resourceGroupApplicationService.delete(id); + return ReqResult.success(); + } + + @Operation(summary = "查询资源分组详情") + @RequestMapping(path = "/{id}", method = RequestMethod.GET) + public ReqResult detail(@PathVariable Long id) { + ResourceGroupDto dto = resourceGroupApplicationService.queryById(id); + if (dto == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.resourceDataNotFound); + } + spacePermissionService.checkSpaceUserPermission(dto.getSpaceId()); + return ReqResult.success(dto); + } + + @Operation(summary = "添加资源到分组") + @RequestMapping(path = "/{id}/resource/add", method = RequestMethod.POST) + public ReqResult addResource(@PathVariable Long id, @RequestBody ResourceTargetDto resourceTargetDto) { + ResourceGroupDto exist = resourceGroupApplicationService.queryById(id); + if (exist == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.resourceDataNotFound); + } + checkPermissionAndRequired(exist.getSpaceId(), resourceTargetDto.getTargetType(), resourceTargetDto.getTargetId()); + resourceGroupApplicationService.addResourceToGroup(id, resourceTargetDto.getTargetType().name(), resourceTargetDto.getTargetId()); + return ReqResult.success(); + } + + private void checkPermissionAndRequired(Long spaceId, Published.TargetType targetType, Long targetId) { + spacePermissionService.checkSpaceUserPermission(spaceId); + if (targetType == null || targetId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "targetType/targetId"); + } + } + + @Operation(summary = "从分组移除资源") + @RequestMapping(path = "/{id}/resource/remove", method = RequestMethod.POST) + public ReqResult removeResource(@PathVariable Long id, @RequestBody ResourceTargetDto resourceTargetDto) { + ResourceGroupDto exist = resourceGroupApplicationService.queryById(id); + if (exist == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.resourceDataNotFound); + } + checkPermissionAndRequired(exist.getSpaceId(), resourceTargetDto.getTargetType(), resourceTargetDto.getTargetId()); + resourceGroupApplicationService.removeResourceFromGroup(id, resourceTargetDto.getTargetType().name(), resourceTargetDto.getTargetId()); + return ReqResult.success(); + } + + @Operation(summary = "查询分组下的资源列表") + @RequestMapping(path = "/{id}/resources", method = RequestMethod.GET) + public ReqResult> listResources(@PathVariable Long id) { + ResourceGroupDto exist = resourceGroupApplicationService.queryById(id); + if (exist == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.resourceDataNotFound); + } + spacePermissionService.checkSpaceUserPermission(exist.getSpaceId()); + List relations = resourceGroupApplicationService.queryGroupRelations(id); + return ReqResult.success(relations); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/SkillController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/SkillController.java index 4163009c..e52544a4 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/SkillController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/SkillController.java @@ -1,10 +1,10 @@ package com.xspaceagi.agent.web.ui.controller; -import com.xspaceagi.agent.core.adapter.application.ConfigHistoryApplicationService; -import com.xspaceagi.agent.core.adapter.application.PublishApplicationService; -import com.xspaceagi.agent.core.adapter.application.SkillApplicationService; +import com.xspaceagi.agent.core.adapter.application.*; import com.xspaceagi.agent.core.adapter.dto.*; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; import com.xspaceagi.agent.core.spec.enums.UsageScenarioEnum; import com.xspaceagi.agent.core.spec.utils.FileTypeUtils; import com.xspaceagi.agent.web.ui.controller.util.SpaceObjectPermissionUtil; @@ -13,12 +13,10 @@ import com.xspaceagi.agent.web.ui.dto.SkillCopyDto; import com.xspaceagi.agent.web.ui.dto.SkillDto; import com.xspaceagi.agent.web.ui.dto.SkillUpdateDto; import com.xspaceagi.custompage.sdk.dto.CopyTypeEnum; -import com.xspaceagi.sandbox.SandboxRequestAttributes; -import com.xspaceagi.system.application.dto.SpaceDto; +import com.xspaceagi.sandbox.SandboxUtils; import com.xspaceagi.system.application.dto.SpaceUserDto; import com.xspaceagi.system.application.service.SpaceApplicationService; import com.xspaceagi.system.application.util.DefaultIconUrlUtil; -import com.xspaceagi.system.infra.dao.entity.Space; import com.xspaceagi.system.sdk.permission.SpacePermissionService; import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.common.RequestContext; @@ -39,7 +37,6 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.io.InputStream; import java.net.URLEncoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -49,6 +46,8 @@ import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import static com.xspaceagi.agent.core.application.service.SkillApplicationServiceImpl.MAX_SINGLE_FILE_SIZE; +import static com.xspaceagi.agent.core.application.service.SkillApplicationServiceImpl.MAX_SKILL_FILE_SIZE; import static com.xspaceagi.system.spec.enums.ResourceEnum.*; @Tag(name = "技能相关接口") @@ -67,14 +66,22 @@ public class SkillController { private PublishApplicationService publishApplicationService; @Resource private ConfigHistoryApplicationService configHistoryApplicationService; + @Resource + private AgentWorkspaceApplicationService agentWorkspaceApplicationService; + @Resource + private ConversationApplicationService conversationApplicationService; + @Resource + private RecommendApplicationService recommendApplicationService; @RequireResource(SKILL_CREATE) @Operation(summary = "新增技能") @PostMapping(value = "/add", produces = MediaType.APPLICATION_JSON_VALUE) public ReqResult add(@RequestBody @Valid SkillAddDto skillAddDto, HttpServletRequest request) { - if (isSandboxSource(request)) { - Long personalSpaceId = getPersonalSpaceId(); - skillAddDto.setSpaceId(personalSpaceId); + if (SandboxUtils.isSandboxRequest(request)) { + if (skillAddDto.getSpaceId() == null) { + Long personalSpaceId = spaceApplicationService.getPersonalSpaceId(RequestContext.get().getUserId()); + skillAddDto.setSpaceId(personalSpaceId); + } } if (skillAddDto.getSpaceId() == null) { throw new IllegalArgumentException("Invalid spaceId"); @@ -108,7 +115,12 @@ public class SkillController { @Operation(summary = "修改技能") @PostMapping(value = "/update", produces = MediaType.APPLICATION_JSON_VALUE) public ReqResult update(@RequestBody @Valid SkillUpdateDto skillUpdateDto) { - checkSkillPermission(skillUpdateDto.getId()); + SkillConfigDto existSkill = checkSkillPermission(skillUpdateDto.getId()); + + // 沙盒技能不允许修改文件 + if (isSandboxSkill(existSkill) && !CollectionUtils.isEmpty(skillUpdateDto.getFiles())) { + throw new IllegalArgumentException("Cannot modify files of sandbox-developed skill"); + } if (skillUpdateDto.getName() != null) { if (!skillUpdateDto.getName().matches("^[\\u4e00-\\u9fa5A-Za-z0-9_-]+$")) { @@ -140,22 +152,25 @@ public class SkillController { throw new IllegalArgumentException("Invalid skillId"); } - // 检查单个文件大小,限制为 80M - long maxSingleFileSize = 80L * 1024 * 1024; - if (file.getSize() >= maxSingleFileSize) { + // 检查单个文件大小 + if (file.getSize() >= MAX_SINGLE_FILE_SIZE) { throw new IllegalArgumentException("Skill package size must not exceed 80 MB"); } // 检查技能权限 SkillConfigDto exist = checkSkillPermission(skillId); + // 沙盒技能不支持上传文件 + if (isSandboxSkill(exist)) { + throw new IllegalArgumentException("Cannot upload files to sandbox-developed skill"); + } + // 计算现有文件的总大小 long existingTotalSize = calculateTotalFileSize(exist.getFiles()); - // 检查新文件 + 原文件总大小,限制为 80M - long maxTotalSize = 80L * 1024 * 1024; + // 检查新文件 + 原文件总大小 long newFileSize = file.getSize(); - if (existingTotalSize + newFileSize > maxTotalSize) { + if (existingTotalSize + newFileSize > MAX_SKILL_FILE_SIZE) { throw new IllegalArgumentException("Skill package size must not exceed 80 MB"); } @@ -201,14 +216,13 @@ public class SkillController { throw new IllegalArgumentException("Invalid skillId"); } - // 检查单个文件大小,限制为 80M - long maxSingleFileSize = 80L * 1024 * 1024; + // 检查单个文件大小 long newFilesTotalSize = 0L; for (MultipartFile file : files) { if (file == null || file.isEmpty()) { continue; } - if (file.getSize() >= maxSingleFileSize) { + if (file.getSize() >= MAX_SINGLE_FILE_SIZE) { throw new IllegalArgumentException("Skill package size must not exceed 80 MB"); } newFilesTotalSize += file.getSize(); @@ -217,12 +231,16 @@ public class SkillController { // 检查技能权限 SkillConfigDto exist = checkSkillPermission(skillId); + // 沙盒技能不支持上传文件 + if (isSandboxSkill(exist)) { + throw new IllegalArgumentException("Cannot upload files to sandbox-developed skill"); + } + // 计算现有文件的总大小 long existingTotalSize = calculateTotalFileSize(exist.getFiles()); - // 检查新文件 + 原文件总大小,限制为 80M - long maxTotalSize = 80L * 1024 * 1024; - if (existingTotalSize + newFilesTotalSize > maxTotalSize) { + // 检查新文件 + 原文件总大小 + if (existingTotalSize + newFilesTotalSize > MAX_SKILL_FILE_SIZE) { throw new IllegalArgumentException("Skill package size must not exceed 80 MB"); } @@ -279,7 +297,17 @@ public class SkillController { @Operation(summary = "删除技能") @PostMapping(path = "/delete/{skillId}", produces = MediaType.APPLICATION_JSON_VALUE) public ReqResult delete(@PathVariable Long skillId) { - checkSkillPermission(skillId); + SkillConfigDto exist = checkSkillPermission(skillId); + + // 沙盒技能需要额外清理沙箱工作空间 + if (isSandboxSkill(exist)) { + try { + agentWorkspaceApplicationService.deleteWorkspace(exist.getCreatorId(), exist.getDevAgentConversationId()); + } catch (Exception e) { + log.warn("Failed to delete sandbox workspace for skill, skillId={}, cId={}", skillId, exist.getDevAgentConversationId(), e); + } + } + skillApplicationService.delete(skillId); return ReqResult.success(); } @@ -302,9 +330,11 @@ public class SkillController { @Operation(summary = "查询技能列表") @GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE) public ReqResult> list(SkillQueryDto queryDto, HttpServletRequest request) { - if (isSandboxSource(request)) { - Long personalSpaceId = getPersonalSpaceId(); - queryDto.setSpaceId(personalSpaceId); + if (SandboxUtils.isSandboxRequest(request)) { + if (queryDto.getSpaceId() == null) { + Long personalSpaceId = spaceApplicationService.getPersonalSpaceId(RequestContext.get().getUserId()); + queryDto.setSpaceId(personalSpaceId); + } } if (queryDto.getSpaceId() == null) { throw new IllegalArgumentException("Invalid spaceId"); @@ -325,9 +355,15 @@ public class SkillController { @Operation(summary = "导出技能") @GetMapping(path = "/export/{skillId}", produces = "application/octet-stream") public byte[] export(@PathVariable Long skillId, HttpServletResponse response) { - checkSkillPermission(skillId); + SkillConfigDto exist = checkSkillPermission(skillId); - SkillExportResultDto exportResult = skillApplicationService.exportSkill(skillId); + SkillExportResultDto exportResult; + if (isSandboxSkill(exist)) { + // 沙盒技能从沙箱导出 + exportResult = agentWorkspaceApplicationService.exportSkillFromSandbox(exist.getCreatorId(), exist.getDevAgentConversationId(), exist.getName()); + } else { + exportResult = skillApplicationService.exportSkill(skillId); + } // 设置响应头 response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(exportResult.getFileName(), Charset.forName("UTF-8"))); @@ -339,29 +375,24 @@ public class SkillController { @RequireResource(SKILL_IMPORT) @Operation(summary = "导入技能") @PostMapping(value = "/import", produces = MediaType.APPLICATION_JSON_VALUE) - public ReqResult importSkill(@RequestParam("file") MultipartFile file, - @RequestParam(value = "targetSkillId", required = false) Long targetSkillId, - @RequestParam(value = "targetSpaceId", required = false) Long targetSpaceId, - @RequestParam(value = "usageScenarios", required = false) List usageScenarios, - HttpServletRequest request) throws IOException { - if (file.isEmpty()) { - throw new IllegalArgumentException("Please select a file to upload"); - } - // 检查文件大小,限制为 20M - long maxSize = 20L * 1024 * 1024; - if (file.getSize() > maxSize) { - throw new IllegalArgumentException("Import file must be smaller than 20 MB"); - } - if (isSandboxSource(request)) { - Long personalSpaceId = getPersonalSpaceId(); - targetSpaceId = personalSpaceId; - // sandbox 场景下,targetSpaceId 由个人空间决定,需要校验权限 - spacePermissionService.checkSpaceUserPermission(targetSpaceId); + public ReqResult importSkill(@RequestParam(value = "url", required = false) String url, + @RequestParam(value = "file", required = false) MultipartFile file, + @RequestParam(value = "targetSkillId", required = false) Long targetSkillId, + @RequestParam(value = "targetSpaceId", required = false) Long targetSpaceId, + @RequestParam(value = "usageScenarios", required = false) List usageScenarios, + HttpServletRequest request) throws IOException { + if (SandboxUtils.isSandboxRequest(request)) { + if (targetSpaceId == null) { + targetSpaceId = spaceApplicationService.getPersonalSpaceId(RequestContext.get().getUserId()); + } } - SkillConfigDto existSkill = null; if (targetSkillId != null) { - existSkill = checkSkillPermission(targetSkillId); + SkillConfigDto existSkill = checkSkillPermission(targetSkillId); + // 沙盒技能不支持导入 + if (isSandboxSkill(existSkill)) { + throw new IllegalArgumentException("Cannot import to sandbox-developed skill"); + } } else { if (targetSpaceId == null) { throw new IllegalArgumentException("Please select a target space"); @@ -369,8 +400,7 @@ public class SkillController { spacePermissionService.checkSpaceUserPermission(targetSpaceId); } - SkillExtDto importExt = parseImportExt(usageScenarios); - Long resultId = skillApplicationService.importSkill(file, existSkill, targetSpaceId, importExt); + Long resultId = skillApplicationService.importSkill(url, file, targetSkillId, targetSpaceId, usageScenarios); return ReqResult.success(resultId); } @@ -413,6 +443,48 @@ public class SkillController { } Long id = skillApplicationService.copySkill(skillConfigDto, targetSpaceId); + + // 沙盒技能复制:需要为新技能创建开发会话,并复制沙箱工作空间 + if (isSandboxSkill(skillConfigDto)) { + try { + TargetRecommendResponse recommendResponse = recommendApplicationService.list(TargetRecommend.RecType.ChatBoxNav.name(), TargetRecommend.TargetType.Agent.name()).stream().filter(targetRecommendResponse -> targetRecommendResponse.getFunctionType().equals(TargetRecommend.FunctionType.SkillDev.name())).findFirst().orElse(null); + if (recommendResponse == null) { + throw new IllegalArgumentException("Develop agent not config"); + } + + Long currentUserId = RequestContext.get().getUserId(); + + // 创建开发会话(新技能的 creatorId 是当前用户) + Long newCId = conversationApplicationService.createConversationForProjectDevelopment( + RequestContext.get().getTenantId(), + currentUserId, + targetSpaceId, + recommendResponse.getTargetId(), + Published.TargetType.Skill.name(), + id + ).getId(); + + // 更新新技能的 devAgentConversationId + SkillConfigDto updateDto = new SkillConfigDto(); + updateDto.setId(id); + updateDto.setDevAgentConversationId(newCId); + skillApplicationService.update(updateDto, false); + + // 复制源沙箱工作空间到新工作空间,并初始化 git + agentWorkspaceApplicationService.copySandboxWorkspace( + skillConfigDto.getCreatorId(), + skillConfigDto.getDevAgentConversationId(), + currentUserId, + newCId + ); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + log.error("Failed to setup sandbox workspace for copied skill, skillId={}", id, e); + throw new RuntimeException("Failed to setup sandbox workspace for copied skill", e); + } + } + return ReqResult.success(id); } @@ -429,22 +501,11 @@ public class SkillController { @Operation(summary = "查询技能模板") @GetMapping(value = "/template", produces = MediaType.APPLICATION_JSON_VALUE) public ReqResult getTemplate() { - try { - InputStream inputStream = SkillController.class.getClassLoader().getResourceAsStream("skill-template.json"); - if (inputStream == null) { - log.error("Cannot find skill template file: skill-template.json"); - throw new IllegalArgumentException("Skill template file does not exist"); - } - - SkillConfigDto skillConfigDto = skillApplicationService.getSkillTemplate(inputStream); - SkillDto skillDto = new SkillDto(); - BeanUtils.copyProperties(skillConfigDto, skillDto); - skillDto.setUsageScenarios(parseUsageScenarios(skillConfigDto.getExt())); - return ReqResult.success(skillDto); - } catch (Exception e) { - log.error("Failed to read skill template", e); - throw e; - } + SkillConfigDto skillConfigDto = skillApplicationService.getSkillTemplate(); + SkillDto skillDto = new SkillDto(); + BeanUtils.copyProperties(skillConfigDto, skillDto); + skillDto.setUsageScenarios(parseUsageScenarios(skillConfigDto.getExt())); + return ReqResult.success(skillDto); } //检查技能权限 @@ -460,44 +521,15 @@ public class SkillController { return skillDto; } - private boolean isSandboxSource(HttpServletRequest request) { - if (request == null) { - return false; - } - Object src = request.getAttribute(SandboxRequestAttributes.REQUEST_SOURCE); - return SandboxRequestAttributes.SOURCE_SANDBOX.equals(src); - } - - private Long getPersonalSpaceId() { - Long userId = RequestContext.get().getUserId(); - List spaceDtos = spaceApplicationService.queryListByUserId(userId); - SpaceDto personalSpace = spaceDtos.stream() - .filter(spaceDto -> spaceDto.getType() == Space.Type.Personal) - .findFirst() - .orElse(null); - if (personalSpace == null) { - throw new IllegalArgumentException("User has no personal space"); - } - return personalSpace.getId(); - } - - private SkillExtDto parseImportExt(List usageScenarios) { - if (usageScenarios == null || usageScenarios.isEmpty()) { - return buildDefaultSkillExt(); - } - return convertExtArray(usageScenarios, true); - } - - private SkillExtDto buildDefaultSkillExt() { - SkillExtDto skillExtDto = new SkillExtDto(); - skillExtDto.setSupportTaskAgent(1); - skillExtDto.setSupportPageApp(0); - return skillExtDto; - } - private SkillExtDto convertExtArray(List extArray, boolean useDefaultWhenEmpty) { if (extArray == null || extArray.isEmpty()) { - return useDefaultWhenEmpty ? buildDefaultSkillExt() : null; + if (!useDefaultWhenEmpty) { + return null; + } + SkillExtDto defaultExt = new SkillExtDto(); + defaultExt.setSupportTaskAgent(1); + defaultExt.setSupportPageApp(0); + return defaultExt; } SkillExtDto ext = new SkillExtDto(); ext.setSupportTaskAgent(extArray.contains(UsageScenarioEnum.TaskAgent) ? 1 : 0); @@ -546,6 +578,10 @@ public class SkillController { } } + private boolean isSandboxSkill(SkillConfigDto skill) { + return skill != null && skill.getDevAgentConversationId() != null; + } + /** * 计算文件列表的总大小 * 对于文本文件,直接计算字符串的字节大小 diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/UserAgentController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/UserAgentController.java index ab5c397b..55db2114 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/UserAgentController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/UserAgentController.java @@ -1,9 +1,12 @@ package com.xspaceagi.agent.web.ui.controller; import com.xspaceagi.agent.core.adapter.application.AgentApplicationService; +import com.xspaceagi.agent.core.adapter.application.RecommendApplicationService; import com.xspaceagi.agent.core.adapter.dto.UserAgentDto; import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; import com.xspaceagi.agent.core.adapter.repository.entity.UserTargetRelation; import com.xspaceagi.agent.core.domain.service.UserTargetRelationDomainService; import com.xspaceagi.system.application.util.DefaultIconUrlUtil; @@ -21,6 +24,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static com.xspaceagi.system.spec.enums.ResourceEnum.AGENT_COLLECT; @@ -39,6 +44,9 @@ public class UserAgentController { @Resource private UserTargetRelationDomainService userTargetRelationDomainService; + @Resource + private RecommendApplicationService recommendApplicationService; + @Operation(summary = "查询用户最近编辑的智能体列表") @RequestMapping(path = "/edit/list/{size}", method = RequestMethod.GET) public ReqResult> editList(@PathVariable Integer size) { @@ -51,7 +59,10 @@ public class UserAgentController { @RequestMapping(path = "/used/list/{size}", method = RequestMethod.GET) public ReqResult> usedList(@PathVariable Integer size, @RequestParam(required = false) Integer pageIndex, @RequestParam(required = false) String kw) { List userAgentDtos = agentApplicationService.queryRecentUseList(RequestContext.get().getUserId(), kw, size, pageIndex); + List list = recommendApplicationService.list(TargetRecommend.RecType.ChatBoxNav.name(), TargetRecommend.TargetType.Agent.name()); + Set navIds = list.stream().map(TargetRecommendResponse::getTargetId).collect(Collectors.toSet()); userAgentDtos.forEach(userAgentDto -> userAgentDto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(userAgentDto.getIcon(), userAgentDto.getName()))); + userAgentDtos.removeIf(userAgentDto -> navIds.contains(userAgentDto.getAgentId())); return ReqResult.success(userAgentDtos); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/WorkflowController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/WorkflowController.java index 7d54f3e7..9a8f3714 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/WorkflowController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/WorkflowController.java @@ -32,7 +32,9 @@ import com.xspaceagi.system.application.dto.UserDto; import com.xspaceagi.system.application.service.SpaceApplicationService; import com.xspaceagi.system.application.util.DefaultIconUrlUtil; import com.xspaceagi.system.sdk.common.TraceContext; +import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService; import com.xspaceagi.system.sdk.permission.SpacePermissionService; +import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto; import com.xspaceagi.system.spec.annotation.RequireResource; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.ReqResult; @@ -103,6 +105,10 @@ public class WorkflowController { @Resource private ResourcePricingRpcService resourcePricingRpcService; + @Resource + private IUserDataPermissionRpcService userDataPermissionRpcService; + + @RequireResource(COMPONENT_LIB_CREATE) @Operation(summary = "新增工作流接口") @RequestMapping(path = "/add", method = RequestMethod.POST) @@ -111,6 +117,7 @@ public class WorkflowController { WorkflowConfigDto workflowConfigDto = new WorkflowConfigDto(); BeanUtils.copyProperties(workflowAddDto, workflowConfigDto); workflowConfigDto.setCreatorId(RequestContext.get().getUserId()); + workflowConfigDto.setType(WorkflowConfigDto.Type.Workflow.name()); Long workflowId = workflowApplicationService.add(workflowConfigDto); return ReqResult.success(workflowId); } @@ -140,7 +147,6 @@ public class WorkflowController { return ReqResult.error("1011", I18nUtil.systemMessage("Backend.Workflow.ConcurrentModification")); } } - workflowApplicationService.save(jsonObject, workflowConfigDto); return ReqResult.success(workflowApplicationService.workflowEditVersion(workflowId, true)); } @@ -344,8 +350,8 @@ public class WorkflowController { if (workflowContext1.getTraceContext().isEnableSubscription()) { List estimateTargets = new ArrayList<>(); resourcePricingRpcService.completeWorkflowNodeEstimateTargets(estimateTargets, nodeDto, new HashSet<>()); - PriceEstimate priceEstimate = resourcePricingRpcService.estimatePrice(tenantConfigDto.getTenantId(), workflowContext1.getTraceContext().getBillUserId(), - List.of(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.WORKFLOW).targetId(workflowConfigDto.getId().toString()).build())); + estimateTargets.add(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.WORKFLOW).targetId(workflowConfigDto.getId().toString()).build()); + PriceEstimate priceEstimate = resourcePricingRpcService.estimatePrice(tenantConfigDto.getTenantId(), workflowContext1.getTraceContext().getBillUserId(), estimateTargets); if (!priceEstimate.isPass()) { WorkflowExecutingDto workflowExecutingDto = new WorkflowExecutingDto(); workflowExecutingDto.setSuccess(false); @@ -479,12 +485,27 @@ public class WorkflowController { mcpRpcService.checkMcpPermission(deployedMcp, workflowNodeAddDto.getNodeConfigDto().getToolName()); workflowMcpLoopCheck(workflowNodeAddDto.getWorkflowId(), deployedMcp, workflowNodeAddDto.getNodeConfigDto().getToolName()); } - if (workflowNodeAddDto.getType() == WorkflowNodeConfig.NodeType.Knowledge && workflowNodeAddDto.getTypeId() != null) { + if ((workflowNodeAddDto.getType() == WorkflowNodeConfig.NodeType.Knowledge || workflowNodeAddDto.getType() == WorkflowNodeConfig.NodeType.KnowledgeInsert) + && workflowNodeAddDto.getTypeId() != null) { KnowledgeConfigModel knowledgeConfigModel = knowledgeConfigApplicationService.queryOneInfoById(workflowNodeAddDto.getTypeId()); if (knowledgeConfigModel == null) { throw new IllegalArgumentException("Invalid knowledge base ID"); } - spacePermissionService.checkSpaceUserPermission(knowledgeConfigModel.getSpaceId()); + // 检查是否在用户组授权了 + try { + spacePermissionService.checkSpaceUserPermission(knowledgeConfigModel.getSpaceId()); + } catch (Exception e) { + UserDataPermissionDto userDataPermission = userDataPermissionRpcService.getUserDataPermission(RequestContext.get().getUserId()); + if (!userDataPermission.getKnowledgeIds().contains(workflowNodeAddDto.getTypeId())) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } + } + } + if (workflowNodeAddDto.getType() == WorkflowNodeConfig.NodeType.Agent) { + PublishedPermissionDto publishedPermissionDto = publishApplicationService.hasPermission(Published.TargetType.Agent, workflowNodeAddDto.getTypeId()); + if (!publishedPermissionDto.isExecute()) { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } } if (workflowNodeAddDto.getType() != null && workflowNodeAddDto.getType().name().startsWith("Table")) { checkTablePermission(workflowNodeAddDto.getTypeId()); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/AgentDevApiForSandboxController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/AgentDevApiForSandboxController.java new file mode 100644 index 00000000..03c51f31 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/AgentDevApiForSandboxController.java @@ -0,0 +1,639 @@ +package com.xspaceagi.agent.web.ui.controller.api; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.xspaceagi.agent.core.adapter.application.AgentApplicationService; +import com.xspaceagi.agent.core.adapter.application.PluginApplicationService; +import com.xspaceagi.agent.core.adapter.application.PublishApplicationService; +import com.xspaceagi.agent.core.adapter.application.WorkflowApplicationService; +import com.xspaceagi.agent.core.adapter.dto.PublishedDto; +import com.xspaceagi.agent.core.adapter.dto.PublishedPermissionDto; +import com.xspaceagi.agent.core.adapter.dto.PublishedQueryDto; +import com.xspaceagi.agent.core.adapter.dto.SkillConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.AgentComponentConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.AgentConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.Arg; +import com.xspaceagi.agent.core.adapter.dto.config.bind.KnowledgeBaseBindConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.bind.McpBindConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.NodeConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; +import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowNodeDto; +import com.xspaceagi.agent.core.adapter.repository.entity.AgentComponentConfig; +import com.xspaceagi.agent.core.adapter.repository.entity.PluginConfig; +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import com.xspaceagi.agent.core.adapter.repository.entity.WorkflowNodeConfig; +import com.xspaceagi.agent.core.infra.component.agent.AgentContext; +import com.xspaceagi.agent.core.infra.component.knowledge.KnowledgeBaseSearcher; +import com.xspaceagi.agent.core.infra.component.knowledge.SearchContext; +import com.xspaceagi.agent.core.infra.converter.ArgConverter; +import com.xspaceagi.agent.core.sdk.IAgentRpcService; +import com.xspaceagi.agent.core.sdk.dto.PluginExecuteRequestDto; +import com.xspaceagi.agent.core.sdk.dto.WorkflowExecuteRequestDto; +import com.xspaceagi.agent.core.sdk.dto.WorkflowExecuteResultDto; +import com.xspaceagi.agent.core.sdk.enums.WfExecuteResultTypeEnum; +import com.xspaceagi.agent.web.ui.controller.api.dto.*; +import com.xspaceagi.file.sdk.IFileAccessService; +import com.xspaceagi.knowledge.sdk.request.KnowledgeConfigRequestVo; +import com.xspaceagi.knowledge.sdk.response.KnowledgeQaVo; +import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeConfigRpcService; +import com.xspaceagi.mcp.sdk.dto.McpDto; +import com.xspaceagi.pricing.sdk.dto.PriceEstimate; +import com.xspaceagi.pricing.sdk.rpc.IPricingRpcService; +import com.xspaceagi.pricing.spec.enums.TargetTypeEnum; +import com.xspaceagi.system.application.dto.TenantConfigDto; +import com.xspaceagi.system.application.dto.UserDto; +import com.xspaceagi.system.sdk.common.TraceContext; +import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; +import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto; +import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.dto.ReqResult; +import com.xspaceagi.system.spec.enums.YesOrNoEnum; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.page.SuperPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; + +import java.time.Duration; +import java.util.*; + +@Slf4j +@Tag(name = "自定义智能体开发相关接口") +@RestController +@RequestMapping("/api/v1/4sandbox/agent") +public class AgentDevApiForSandboxController { + + @Resource + private PublishApplicationService publishApplicationService; + + @Resource + private SpacePermissionService spacePermissionService; + + @Resource + private IUserDataPermissionRpcService userDataPermissionRpcService; + + @Resource + private IKnowledgeConfigRpcService knowledgeConfigRpcService; + + @Resource + private AgentApplicationService agentApplicationService; + + @Resource + private IAgentRpcService iAgentRpcService; + + @Resource + private IPricingRpcService iPricingRpcService; + + @Resource + private KnowledgeBaseSearcher knowledgeBaseSearcher; + + @Resource + private IFileAccessService iFileAccessService; + + @Resource + private PluginApplicationService pluginApplicationService; + + @Resource + private WorkflowApplicationService workflowApplicationService; + + @Operation(summary = "获取当前开发的智能体基本配置信息") + @RequestMapping(path = "/dev/config/{devAgentId}", method = RequestMethod.GET) + public ReqResult agentConfig(@PathVariable Long devAgentId) { + AgentConfigDto agentConfigDto = agentApplicationService.queryConfigForTestExecute(devAgentId); + spacePermissionService.checkSpaceUserPermission(agentConfigDto.getSpaceId()); + DevAgentConfigDTO devAgentConfigDTO = new DevAgentConfigDTO(); + devAgentConfigDTO.setSystemPrompt(agentConfigDto.getSystemPrompt()); + devAgentConfigDTO.setOpeningChatMsg(agentConfigDto.getOpeningChatMsg()); + List toolSearchResultItemDTOS = new ArrayList<>(); + List skillResultItemDTOS = new ArrayList<>(); + List mcpConfigs = new ArrayList<>(); + agentConfigDto.getAgentComponentConfigList().forEach(agentComponentConfig -> { + if (agentComponentConfig.getType() == AgentComponentConfig.Type.Plugin) { + PluginDto pluginDto = (PluginDto) agentComponentConfig.getTargetConfig(); + String schema = buildPluginSchema((PluginConfigDto) pluginDto.getConfig()); + toolSearchResultItemDTOS.add(ToolSearchResultItemDTO.builder() + .name(pluginDto.getName()) + .description(pluginDto.getDescription()) + .targetId(pluginDto.getId()) + .targetType(Published.TargetType.Plugin) + .schema(schema) + .build()); + } + if (agentComponentConfig.getType() == AgentComponentConfig.Type.Workflow) { + WorkflowConfigDto workflowDto = (WorkflowConfigDto) agentComponentConfig.getTargetConfig(); + String schema = buildWorkflowSchema(workflowDto.getInputArgs(), workflowDto.getOutputArgs()); + toolSearchResultItemDTOS.add(ToolSearchResultItemDTO.builder() + .name(workflowDto.getName()) + .description(workflowDto.getDescription()) + .targetId(workflowDto.getId()) + .targetType(Published.TargetType.Workflow) + .schema(schema) + .build()); + } + if (agentComponentConfig.getType() == AgentComponentConfig.Type.Skill) { + SkillConfigDto skillDto = (SkillConfigDto) agentComponentConfig.getTargetConfig(); + skillResultItemDTOS.add(SkillResultItemDTO.builder() + .name(skillDto.getName()) + .description(skillDto.getDescription()) + .id(skillDto.getId()) + .downloadUrl(iFileAccessService.getFileUrlWithAk(skillDto.getZipFileUrl(), true)).build()); + + } + if (agentComponentConfig.getType() == AgentComponentConfig.Type.Mcp) { + McpDto mcpDto = (McpDto) agentComponentConfig.getTargetConfig(); + McpBindConfigDto mcpBindConfigDto = (McpBindConfigDto) agentComponentConfig.getBindConfig(); + mcpConfigs.add(McpResultDTO.builder() + .name(mcpDto.getName()) + .description(mcpDto.getDescription()) + .serverConfig(mcpDto.getMcpConfig().getServerConfig()) + .usedTool(mcpBindConfigDto.getToolName()) + .build()); + } + + if (agentComponentConfig.getType() == AgentComponentConfig.Type.Knowledge) { + toolSearchResultItemDTOS.add(ToolSearchResultItemDTO.builder() + .name(agentComponentConfig.getName()) + .description(agentComponentConfig.getDescription()) + .targetId(agentComponentConfig.getTargetId()) + .targetType(Published.TargetType.Knowledge) + .schema(buildKnowledgeSchema()) + .build()); + } + + }); + devAgentConfigDTO.setTools(toolSearchResultItemDTOS); + devAgentConfigDTO.setSkills(skillResultItemDTOS); + devAgentConfigDTO.setMcpConfigs(mcpConfigs); + return ReqResult.success(devAgentConfigDTO); + } + + @Operation(summary = "更新前智能体的基本配置信息") + @RequestMapping(path = "/dev/config/update", method = RequestMethod.POST) + public ReqResult updateAgentConfig(@RequestBody DevAgentConfigUpdateDTO devAgentConfigUpdateDTO) { + Assert.notNull(devAgentConfigUpdateDTO, "devAgentConfigUpdateDTO cannot be empty"); + Assert.notNull(devAgentConfigUpdateDTO.getDevAgentId(), "devAgentId cannot be empty"); + AgentConfigDto agentConfigDto = agentApplicationService.queryConfigForTestExecute(devAgentConfigUpdateDTO.getDevAgentId()); + spacePermissionService.checkSpaceUserPermission(agentConfigDto.getSpaceId()); + if (agentConfigDto.getDevAgentConversationId() == null) { + return ReqResult.error("Non-custom agents cannot be modified"); + } + if (StringUtils.isNotBlank(devAgentConfigUpdateDTO.getSystemPrompt()) || StringUtils.isNotBlank(devAgentConfigUpdateDTO.getOpeningChatMsg())) { + AgentConfigDto agentConfigUpdate = new AgentConfigDto(); + agentConfigUpdate.setSystemPrompt(StringUtils.isNotBlank(devAgentConfigUpdateDTO.getSystemPrompt()) ? devAgentConfigUpdateDTO.getSystemPrompt() : null); + agentConfigUpdate.setOpeningChatMsg(StringUtils.isNotBlank(devAgentConfigUpdateDTO.getOpeningChatMsg()) ? devAgentConfigUpdateDTO.getOpeningChatMsg() : null); + agentConfigUpdate.setId(devAgentConfigUpdateDTO.getDevAgentId()); + agentApplicationService.update(agentConfigUpdate); + } + return ReqResult.success(); + } + + @Operation(summary = "向智能体的添加工具") + @RequestMapping(path = "/dev/config/tool/add", method = RequestMethod.POST) + public ReqResult addAgentComponent(@RequestBody ToolAddDTO toolAddDTO) { + AgentConfigDto agentConfigDto = agentApplicationService.queryConfigForTestExecute(toolAddDTO.getDevAgentId()); + spacePermissionService.checkSpaceUserPermission(agentConfigDto.getSpaceId()); + if (agentConfigDto.getDevAgentConversationId() == null) { + return ReqResult.error("Non-custom agents cannot be modified"); + } + if (toolAddDTO.getTargetType() == Published.TargetType.Plugin) { + // Check plugin usage permission + pluginApplicationService.checkSpacePluginPermission(agentConfigDto.getSpaceId(), toolAddDTO.getTargetId()); + } + if (toolAddDTO.getTargetType() == Published.TargetType.Workflow) { + // Check workflow usage permission + workflowApplicationService.checkSpaceWorkflowPermission(agentConfigDto.getSpaceId(), toolAddDTO.getTargetId()); + } + AgentComponentConfig.Type type = null; + if (toolAddDTO.getTargetType() == Published.TargetType.Plugin) { + type = AgentComponentConfig.Type.Plugin; + } + if (toolAddDTO.getTargetType() == Published.TargetType.Workflow) { + type = AgentComponentConfig.Type.Workflow; + } + if (toolAddDTO.getTargetType() == Published.TargetType.Knowledge) { + type = AgentComponentConfig.Type.Knowledge; + } + if (toolAddDTO.getTargetType() == Published.TargetType.Skill) { + type = AgentComponentConfig.Type.Skill; + } + if (type == null) { + return ReqResult.error("Error targetType"); + } + AgentComponentConfigDto agentComponentConfigDto = AgentComponentConfigDto.builder() + .agentId(agentConfigDto.getId()) + .type(type) + .targetId(toolAddDTO.getTargetId()) + .agentId(agentConfigDto.getId()) + .build(); + agentApplicationService.addComponentConfig(agentComponentConfigDto); + return ReqResult.success(); + } + + @Operation(summary = "删除智能体已添加的工具") + @RequestMapping(path = "/dev/config/tool/delete", method = RequestMethod.POST) + public ReqResult deleteAgentComponent(@RequestBody ToolAddDTO toolAddDTO) { + AgentConfigDto agentConfigDto = agentApplicationService.queryConfigForTestExecute(toolAddDTO.getDevAgentId()); + spacePermissionService.checkSpaceUserPermission(agentConfigDto.getSpaceId()); + if (agentConfigDto.getDevAgentConversationId() == null) { + return ReqResult.error("Non-custom agents cannot be modified"); + } + if (toolAddDTO.getTargetType() == Published.TargetType.Plugin) { + agentConfigDto.getAgentComponentConfigList().stream().filter( + agentComponentConfig -> agentComponentConfig.getTargetId().equals(toolAddDTO.getTargetId()) + && agentComponentConfig.getType() == AgentComponentConfig.Type.Plugin).findFirst() + .ifPresent(agentComponentConfigDto -> agentApplicationService.deleteComponentConfig(agentComponentConfigDto.getId())); + } + if (toolAddDTO.getTargetType() == Published.TargetType.Workflow) { + agentConfigDto.getAgentComponentConfigList().stream().filter( + agentComponentConfig -> agentComponentConfig.getTargetId().equals(toolAddDTO.getTargetId()) + && agentComponentConfig.getType() == AgentComponentConfig.Type.Workflow).findFirst() + .ifPresent(agentComponentConfigDto -> agentApplicationService.deleteComponentConfig(agentComponentConfigDto.getId())); + } + if (toolAddDTO.getTargetType() == Published.TargetType.Knowledge) { + agentConfigDto.getAgentComponentConfigList().stream().filter( + agentComponentConfig -> agentComponentConfig.getTargetId().equals(toolAddDTO.getTargetId()) + && agentComponentConfig.getType() == AgentComponentConfig.Type.Knowledge).findFirst() + .ifPresent(agentComponentConfigDto -> agentApplicationService.deleteComponentConfig(agentComponentConfigDto.getId())); + } + if (toolAddDTO.getTargetType() == Published.TargetType.Skill) { + agentConfigDto.getAgentComponentConfigList().stream().filter( + agentComponentConfig -> agentComponentConfig.getTargetId().equals(toolAddDTO.getTargetId()) + && agentComponentConfig.getType() == AgentComponentConfig.Type.Skill).findFirst() + .ifPresent(agentComponentConfigDto -> agentApplicationService.deleteComponentConfig(agentComponentConfigDto.getId())); + } + return ReqResult.success(); + } + + @Operation(summary = "工具搜索相关接口") + @RequestMapping(path = "/dev/tool/search", method = RequestMethod.POST) + public ReqResult> toolSearch(@RequestBody ToolSearchDTO toolSearchDTO) { + Assert.notNull(toolSearchDTO, "toolSearchDTO must not be null"); + Assert.notNull(toolSearchDTO.getDevSpaceId(), "devSpaceId must not be null, read from env DEV_SPACE_ID"); + spacePermissionService.checkSpaceUserPermission(toolSearchDTO.getDevSpaceId()); + PublishedQueryDto publishedQueryDto = new PublishedQueryDto(); + publishedQueryDto.setOnlyTemplate(YesOrNoEnum.N.getKey()); + publishedQueryDto.setSpaceId(toolSearchDTO.getDevSpaceId()); + publishedQueryDto.setCategory(null); + publishedQueryDto.setPage(toolSearchDTO.getPage() == null || toolSearchDTO.getPage() <= 0 ? 1 : toolSearchDTO.getPage()); + publishedQueryDto.setPageSize(toolSearchDTO.getPageSize() == null || toolSearchDTO.getPageSize() <= 0 ? 10 : toolSearchDTO.getPageSize()); + if (toolSearchDTO.getType() != null && toolSearchDTO.getType().equals("skill")) { + publishedQueryDto.setTargetType(Published.TargetType.Skill); + publishedQueryDto.setReturnConfig(true); + publishedQueryDto.setKw(toolSearchDTO.getKw()); + List toolSearchResultItemDTOS = new ArrayList<>(); + SuperPage page = publishApplicationService.queryPublishedList(publishedQueryDto); + page.getRecords().forEach(publishedDto -> { + SkillConfigDto skillConfigDto = JSON.parseObject(publishedDto.getConfig(), SkillConfigDto.class); + toolSearchResultItemDTOS.add(ToolSearchResultItemDTO.builder() + .name(skillConfigDto.getName()) + .description(skillConfigDto.getDescription()) + .targetId(skillConfigDto.getId()) + .targetType(Published.TargetType.Skill) + .schema("The skill's download url is " + iFileAccessService.getFileUrlWithAk(skillConfigDto.getZipFileUrl(), true)) + .build()); + }); + return ReqResult.success(toolSearchResultItemDTOS); + } + + publishedQueryDto.setTargetType(Published.TargetType.Plugin); + publishedQueryDto.setReturnConfig(true); + publishedQueryDto.setKw(toolSearchDTO.getKw()); + List toolSearchResultItemDTOS = new ArrayList<>(); + SuperPage page = publishApplicationService.queryPublishedList(publishedQueryDto); + page.getRecords().forEach(publishedDto -> { + String schema = null; + try { + PluginConfig pluginConfig = JSON.parseObject(publishedDto.getConfig(), PluginConfig.class); + PluginDto pluginDto = new PluginDto(); + BeanUtils.copyProperties(pluginConfig, pluginDto); + pluginDto.setConfig(PluginDto.convertToPluginConfigDto(pluginDto, pluginConfig.getConfig())); + PluginConfigDto pluginConfigDto = (PluginConfigDto) pluginDto.getConfig(); + schema = buildPluginSchema(pluginConfigDto); + } catch (Exception e) { + // Ignore + log.warn("Failed to parse plugin config {}", publishedDto.getConfig()); + } + toolSearchResultItemDTOS.add(ToolSearchResultItemDTO.builder() + .name(publishedDto.getName()) + .description(publishedDto.getDescription()) + .targetId(publishedDto.getTargetId()) + .targetType(Published.TargetType.Plugin) + .schema(schema) + .build()); + }); + + publishedQueryDto.setTargetType(Published.TargetType.Workflow); + page = publishApplicationService.queryPublishedList(publishedQueryDto); + page.getRecords().forEach(publishedDto -> { + String schema = null; + try { + JSONObject jsonObject = JSON.parseObject(publishedDto.getConfig()); + List nodes = jsonObject.getJSONArray("nodes").stream().map(node -> { + JSONObject nodeJson = (JSONObject) node; + WorkflowNodeDto workflowNodeDto = nodeJson.toJavaObject(WorkflowNodeDto.class); + NodeConfigDto nodeConfigDto = WorkflowConfigDto.convertToNodeConfigDto(WorkflowNodeConfig.NodeType.valueOf(nodeJson.getString("type")), nodeJson.getString("nodeConfig")); + workflowNodeDto.setNodeConfig(nodeConfigDto); + return workflowNodeDto; + }).toList(); + WorkflowNodeDto startNode = nodes.stream().filter(node -> node.getType() == WorkflowNodeConfig.NodeType.Start).findFirst().orElse(null); + WorkflowNodeDto endNode = nodes.stream().filter(node -> node.getType() == WorkflowNodeConfig.NodeType.End).findFirst().orElse(null); + if (startNode != null && endNode != null) { + schema = buildWorkflowSchema(startNode.getNodeConfig().getInputArgs(), endNode.getNodeConfig().getOutputArgs()); + } + } catch (Exception e) { + log.warn("Failed to parse workflow config {}", publishedDto.getConfig()); + } + + toolSearchResultItemDTOS.add(ToolSearchResultItemDTO.builder() + .name(publishedDto.getName()) + .description(publishedDto.getDescription()) + .targetId(publishedDto.getTargetId()) + .targetType(Published.TargetType.Workflow) + .schema(schema) + .build()); + }); + + UserDataPermissionDto userDataPermissionDto = userDataPermissionRpcService.getUserDataPermission(RequestContext.get().getUserId()); + List knowledgeIds = userDataPermissionDto.getKnowledgeIds(); + KnowledgeConfigRequestVo knowledgeConfigRequestVo = KnowledgeConfigRequestVo.builder() + .kw(publishedQueryDto.getKw()) + .spaceId(publishedQueryDto.getSpaceId()) + .authSpaceIds(List.of(toolSearchDTO.getDevSpaceId())) + .page(publishedQueryDto.getPage()) + .pageSize(publishedQueryDto.getPageSize()) + .knowledgeIds(knowledgeIds) + .build(); + + var voResponse = this.knowledgeConfigRpcService.queryListKnowledgeConfig(knowledgeConfigRequestVo); + voResponse.getConfigPage().getRecords().forEach(knowledgeConfigVo -> { + String schema = buildKnowledgeSchema(); + toolSearchResultItemDTOS.add(ToolSearchResultItemDTO.builder() + .name(knowledgeConfigVo.getName()) + .description(knowledgeConfigVo.getDescription()) + .targetId(knowledgeConfigVo.getId()) + .targetType(Published.TargetType.Knowledge) + .schema(schema) + .build()); + }); + + return ReqResult.success(toolSearchResultItemDTOS); + } + + private String buildKnowledgeSchema() { + Map schema = new LinkedHashMap<>(); + schema.put("method", "POST"); + schema.put("url", "${PLATFORM_BASE_URL}/api/v1/4sandbox/agent/kb/query"); + schema.put("authorization", "Bearer ${SANDBOX_ACCESS_KEY}"); + schema.put("requestBody", Map.of( + "devAgentId", "该参数仅用于开发者搜索到API后调试使用,通过环境 ${DEV_AGENT_ID} 获取值,正式使用时禁止传递该参数", + "targetType", "Knowledge", + "targetId", "number", + "params", Map.of( + "type", "object", + "properties", Map.of( + "query", Map.of("type", "string", "description", "Search query for knowledge base retrieval") + ), + "required", List.of("query") + ) + )); + schema.put("response", Map.of( + "type", "object", + "properties", Map.of( + "items", Map.of("type", "array", "description", "Retrieved knowledge base items"), + "error", buildErrorSchema() + ) + )); + return JSON.toJSONString(schema); + } + + private String buildWorkflowSchema(List inputArgs, List outputArgs) { + Map schema = new LinkedHashMap<>(); + schema.put("method", "POST"); + schema.put("url", "${PLATFORM_BASE_URL}/api/v1/4sandbox/agent/workflow/execute"); + schema.put("authorization", "Bearer ${SANDBOX_ACCESS_KEY}"); + schema.put("streaming", true); + schema.put("contentType", "text/event-stream"); + schema.put("requestBody", Map.of( + "devAgentId", "该参数仅用于开发者搜索到API后调试使用,通过环境 ${DEV_AGENT_ID} 获取值,正式使用时禁止传递该参数", + "targetType", "Workflow", + "targetId", "number", + "params", inputArgs != null && !inputArgs.isEmpty() + ? ArgConverter.convertArgsToJsonSchema(inputArgs) + : Map.of("type", "object", "properties", Map.of()) + )); + Map finalResultDataProperties = new LinkedHashMap<>(); + if (outputArgs != null && !outputArgs.isEmpty()) { + Map outputSchema = ArgConverter.convertArgsToJsonSchema(outputArgs); + @SuppressWarnings("unchecked") + Map outputProps = (Map) outputSchema.get("properties"); + if (outputProps != null) { + finalResultDataProperties.putAll(outputProps); + } + } + finalResultDataProperties.put("error", buildErrorSchema()); + schema.put("sseEvents", List.of( + Map.of("event", "Heartbeat", "description", "Keep-alive event, ignore"), + Map.of("event", "FinalResult", "description", "Final execution result", + "data", Map.of("type", "object", "properties", finalResultDataProperties)) + )); + return JSON.toJSONString(schema); + } + + private String buildPluginSchema(PluginConfigDto pluginConfigDto) { + if (pluginConfigDto == null) { + return ""; + } + Map schema = new LinkedHashMap<>(); + schema.put("method", "POST"); + schema.put("url", "${PLATFORM_BASE_URL}/api/v1/4sandbox/agent/plugin/execute"); + schema.put("authorization", "Bearer ${SANDBOX_ACCESS_KEY}"); + schema.put("requestBody", Map.of( + "devAgentId", "该参数仅用于开发者搜索到API后调试使用,通过环境 ${DEV_AGENT_ID} 获取值,该参数仅用于调试,绝对禁止用到项目中", + "targetType", "Plugin", + "targetId", "number", + "params", pluginConfigDto.getInputArgs() != null && !pluginConfigDto.getInputArgs().isEmpty() + ? ArgConverter.convertArgsToJsonSchema(pluginConfigDto.getInputArgs()) + : Map.of("type", "object", "properties", Map.of()) + )); + Map responseProperties = new LinkedHashMap<>(); + if (pluginConfigDto.getOutputArgs() != null && !pluginConfigDto.getOutputArgs().isEmpty()) { + Map outputSchema = ArgConverter.convertArgsToJsonSchema(pluginConfigDto.getOutputArgs()); + @SuppressWarnings("unchecked") + Map outputProps = (Map) outputSchema.get("properties"); + if (outputProps != null) { + responseProperties.putAll(outputProps); + } + } + responseProperties.put("error", buildErrorSchema()); + schema.put("response", Map.of("type", "object", "properties", responseProperties)); + return JSON.toJSONString(schema); + } + + + private Map buildErrorSchema() { + return Map.of( + "type", "object", + "properties", Map.of( + "message", Map.of("type", "string", "description", "Error message"), + "code", Map.of("type", "string", "description", "Error code") + ) + ); + } + + @Operation(summary = "插件执行接口") + @RequestMapping(path = "/plugin/execute", method = RequestMethod.POST) + public Object executeTool(@RequestBody ToolExecuteDTO toolExecuteDTO) { + try { + List agentComponentConfigList = checkConfigAndPermission(toolExecuteDTO); + Assert.isTrue(toolExecuteDTO.getTargetType() == Published.TargetType.Plugin, "Invalid TargetType"); + AgentComponentConfigDto componentConfigDto = agentComponentConfigList.stream().filter(agentComponentConfig -> + agentComponentConfig.getTargetId().equals(toolExecuteDTO.getTargetId()) && agentComponentConfig.getType() == AgentComponentConfig.Type.Plugin) + .findFirst().orElse(null); + if (componentConfigDto == null) { + throw new BizException("Error plugin id"); + } + UserAccessKeyDto userAccessKeyDto = (UserAccessKeyDto) RequestContext.get().getUserAccessKey(); + return executePlugin(userAccessKeyDto, toolExecuteDTO, componentConfigDto); + } catch (Exception e) { + return Map.of("error", Map.of("message", e.getMessage(), "code", "0001")); + } + } + + @Operation(summary = "知识库检索接口") + @RequestMapping(path = "/kb/query", method = RequestMethod.POST) + public KbQueryResultDTO executeKb(@RequestBody ToolExecuteDTO toolExecuteDTO) { + try { + List agentComponentConfigList = checkConfigAndPermission(toolExecuteDTO); + Assert.isTrue(toolExecuteDTO.getTargetType() == Published.TargetType.Knowledge, "Invalid TargetType"); + AgentComponentConfigDto componentConfigDto = agentComponentConfigList.stream().filter(agentComponentConfig -> + agentComponentConfig.getTargetId().equals(toolExecuteDTO.getTargetId()) && agentComponentConfig.getType() == AgentComponentConfig.Type.Knowledge) + .findFirst().orElse(null); + if (componentConfigDto == null) { + throw new BizException("Error kb id"); + } + Assert.isTrue(toolExecuteDTO.getParams().containsKey("query"), "query can not be null"); + KnowledgeBaseBindConfigDto knowledgeBaseBindConfigDto = (KnowledgeBaseBindConfigDto) componentConfigDto.getBindConfig(); + SearchContext searchContext = new SearchContext(); + searchContext.setQuery(toolExecuteDTO.getParams().get("query").toString()); + searchContext.setSearchStrategy(knowledgeBaseBindConfigDto.getSearchStrategy()); + searchContext.setMaxRecallCount(knowledgeBaseBindConfigDto.getMaxRecallCount()); + searchContext.setMatchingDegree(knowledgeBaseBindConfigDto.getMatchingDegree()); + searchContext.setKnowledgeBaseIds(List.of(componentConfigDto.getTargetId())); + AgentContext agentContext = new AgentContext(); + agentContext.setTenantConfig((TenantConfigDto) RequestContext.get().getTenantConfig()); + agentContext.setUser((UserDto) RequestContext.get().getUser()); + searchContext.setAgentContext(agentContext); + List qaVoList = knowledgeBaseSearcher.search(searchContext).timeout(Duration.ofSeconds(60)).block(); + return new KbQueryResultDTO(qaVoList, null); + } catch (Exception e) { + return new KbQueryResultDTO(null, Map.of("error", Map.of("message", e.getMessage(), "code", "0001"))); + } + } + + @Operation(summary = "工作流执行接口") + @RequestMapping(path = "/workflow/execute", method = RequestMethod.POST, produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux executeWorkflow(@RequestBody ToolExecuteDTO toolExecuteDTO) { + try { + List agentComponentConfigList = checkConfigAndPermission(toolExecuteDTO); + Assert.isTrue(toolExecuteDTO.getTargetType() == Published.TargetType.Workflow, "Invalid TargetType"); + AgentComponentConfigDto componentConfigDto = agentComponentConfigList.stream().filter(agentComponentConfig -> + agentComponentConfig.getTargetId().equals(toolExecuteDTO.getTargetId()) && agentComponentConfig.getType() == AgentComponentConfig.Type.Workflow) + .findFirst().orElse(null); + if (componentConfigDto == null) { + throw new BizException("Error workflow id"); + } + UserAccessKeyDto userAccessKeyDto = (UserAccessKeyDto) RequestContext.get().getUserAccessKey(); + return executeWorkflow(userAccessKeyDto, toolExecuteDTO, componentConfigDto).mapNotNull(workflowExecutingDTO -> { + if (workflowExecutingDTO.getType() == WfExecuteResultTypeEnum.EXECUTE_RESULT) { + return WorkflowExecutingDTO.builder().type(WorkflowExecutingDTO.EventType.FinalResult).data(workflowExecutingDTO.getData()).build(); + } + return null; + }).filter(Objects::nonNull) + .mergeWith(Flux.interval(Duration.ofSeconds(20)) + .map(i -> WorkflowExecutingDTO.builder().type(WorkflowExecutingDTO.EventType.Heartbeat).build())) + .takeUntil(dto -> dto != null && dto.getType() == WorkflowExecutingDTO.EventType.FinalResult); + } catch (Exception e) { + return Flux.just(WorkflowExecutingDTO.builder().type(WorkflowExecutingDTO.EventType.FinalResult).data(Map.of("error", Map.of("message", e.getMessage(), "code", "0001"))).build()); + } + } + + private List checkConfigAndPermission(ToolExecuteDTO toolExecuteDTO) { + Assert.notNull(toolExecuteDTO, "toolExecuteDTO must not be null"); + Assert.notNull(toolExecuteDTO.getTargetId(), "targetId must not be null"); + Assert.notNull(toolExecuteDTO.getTargetType(), "targetType must not be null"); + UserAccessKeyDto userAccessKeyDto = (UserAccessKeyDto) RequestContext.get().getUserAccessKey(); + String targetId = userAccessKeyDto.getTargetId(); + Long agentId = toolExecuteDTO.getDevAgentId() != null ? toolExecuteDTO.getDevAgentId() : Long.parseLong(targetId); + AgentConfigDto agentConfig; + //验证用户有没有智能体的权限 + if (YesOrNoEnum.Y.getKey().equals(userAccessKeyDto.getConfig().getIsDevMode()) || toolExecuteDTO.getDevAgentId() != null) { + agentConfig = agentApplicationService.queryConfigForTestExecute(agentId); + if (agentConfig != null) { + spacePermissionService.checkSpaceUserPermission(agentConfig.getSpaceId(), userAccessKeyDto.getUserId()); + } + } else { + agentConfig = agentApplicationService.queryPublishedConfig(agentId, true); + PublishedPermissionDto publishedPermissionDto = publishApplicationService.hasPermission(Published.TargetType.Agent, agentId); + if (!publishedPermissionDto.isExecute()) { + throw new BizException("No permission"); + } + } + if (agentConfig == null) { + throw new BizException("Error agent id"); + } + return agentConfig.getAgentComponentConfigList(); + } + + private Flux executeWorkflow(UserAccessKeyDto userAccessKeyDto, ToolExecuteDTO toolExecuteDTO, AgentComponentConfigDto component) { + TraceContext traceContext = userAccessKeyDto.getConfig().getTraceContext(); + WorkflowExecuteRequestDto workflowExecuteRequest = new WorkflowExecuteRequestDto(); + workflowExecuteRequest.setWorkflowId(component.getTargetId()); + workflowExecuteRequest.setSpaceId(component.getSpaceId()); + workflowExecuteRequest.setParams(toolExecuteDTO.getParams()); + workflowExecuteRequest.setConversationId(traceContext.getConversationId()); + workflowExecuteRequest.setRequestId(traceContext.getTraceId()); + workflowExecuteRequest.setUser(RequestContext.get().getUser()); + workflowExecuteRequest.setConfig(component.getTargetConfig()); + workflowExecuteRequest.setBindConfig(component.getBindConfig()); + workflowExecuteRequest.setTraceContext(traceContext); + TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig(); + if (tenantConfig != null && tenantConfig.getEnableSubscription() != null && tenantConfig.getEnableSubscription() == 1) { + List estimateTargets = List.of(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.WORKFLOW).targetId(component.getTargetId().toString()).build()); + PriceEstimate priceEstimate = iPricingRpcService.estimatePrice(traceContext.getTenantId(), traceContext.getBillUserId(), estimateTargets); + if (priceEstimate != null && !priceEstimate.isPass()) { + throw new BizException(priceEstimate.getMessage()); + } + } + return iAgentRpcService.executeWorkflow(workflowExecuteRequest); + } + + private Object executePlugin(UserAccessKeyDto userAccessKeyDto, ToolExecuteDTO toolExecuteDTO, AgentComponentConfigDto agentComponentConfig) { + PluginExecuteRequestDto pluginExecuteRequest = new PluginExecuteRequestDto(); + pluginExecuteRequest.setSpaceId(agentComponentConfig.getSpaceId()); + pluginExecuteRequest.setParams(toolExecuteDTO.getParams()); + pluginExecuteRequest.setConfig(agentComponentConfig.getTargetConfig()); + pluginExecuteRequest.setBindConfig(agentComponentConfig.getBindConfig()); + pluginExecuteRequest.setUser(RequestContext.get().getUser()); + pluginExecuteRequest.setTraceContext(userAccessKeyDto.getConfig().getTraceContext()); + TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig(); + if (tenantConfig != null && tenantConfig.getEnableSubscription() != null && tenantConfig.getEnableSubscription() == 1) { + List estimateTargets = List.of(PriceEstimate.EstimateTarget.builder().targetType(TargetTypeEnum.PLUGIN).targetId(agentComponentConfig.getTargetId().toString()).build()); + PriceEstimate priceEstimate = iPricingRpcService.estimatePrice(userAccessKeyDto.getTenantId(), userAccessKeyDto.getConfig().getTraceContext().getBillUserId(), estimateTargets); + if (priceEstimate != null && !priceEstimate.isPass()) { + throw new BizException(priceEstimate.getMessage()); + } + } + return iAgentRpcService.executePlugin(pluginExecuteRequest).timeout(Duration.ofSeconds(180)).block(); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/ChatApiController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/ChatApiController.java index f50d8fb6..0ba0e864 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/ChatApiController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/ChatApiController.java @@ -146,7 +146,7 @@ public class ChatApiController { @GetMapping("/{conversationId}/files") public ReqResult getFileList(@PathVariable Long conversationId) { String proxyPath = String.format("/api/v1/chat/%s/file", conversationId); - Map result = computerFileApplicationService.getFileList(RequestContext.get().getUserId(), conversationId, proxyPath, null); + Map result = computerFileApplicationService.getFileList(RequestContext.get().getUserId(), conversationId, proxyPath, null, null); if (result == null) { return ReqResult.error("Failed to query file list"); } @@ -181,7 +181,8 @@ public class ChatApiController { if (cId == null) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } - return computerFileApplicationService.getStaticFile(cId, request); + String customTargetDir = request.getParameter("customTargetDir"); + return computerFileApplicationService.getStaticFile(cId, request, customTargetDir); } private Map getHeadersFromRequest(HttpServletRequest request) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/PluginAndWorkflowApiController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/PluginAndWorkflowApiController.java index 5e81a2dc..5edbf618 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/PluginAndWorkflowApiController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/PluginAndWorkflowApiController.java @@ -12,6 +12,7 @@ import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; import com.xspaceagi.agent.core.adapter.repository.entity.Published; import com.xspaceagi.system.application.dto.SpaceDto; import com.xspaceagi.system.application.service.SpaceApplicationService; +import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto; import com.xspaceagi.system.spec.common.RequestContext; import com.xspaceagi.system.spec.dto.ReqResult; import com.xspaceagi.system.spec.enums.ErrorCodeEnum; @@ -48,8 +49,12 @@ public class PluginAndWorkflowApiController { @Operation(summary = "工作流流式执行接口") @RequestMapping(path = "/api/v1/workflow/{id}/sse/execute", method = RequestMethod.POST, produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux streamExecuteWorkflow(@PathVariable Long id, @RequestBody @Valid Map params, HttpServletResponse response) { - String requestId = UUID.randomUUID().toString().replace("-", ""); + public Flux streamExecuteWorkflow(@PathVariable Long id, @RequestBody @Valid Map params, HttpServletRequest request, HttpServletResponse response) { + String requestId0 = request.getHeader("Trace-Id"); + if (requestId0 == null) { + requestId0 = UUID.randomUUID().toString().replace("-", ""); + } + final String requestId = requestId0; WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryPublishedWorkflowConfig(id, null, true); if (workflowConfigDto == null) { return Flux.create(emitter -> sendError(emitter, requestId, "Error workflow id or not published")); @@ -73,8 +78,12 @@ public class PluginAndWorkflowApiController { @Operation(summary = "工作流执行接口") @RequestMapping(path = "/api/v1/workflow/{id}/execute", method = RequestMethod.POST) - public ReqResult executeWorkflow(@PathVariable Long id, @RequestBody @Valid Map params, HttpServletRequest request, HttpServletResponse response) { - String requestId = UUID.randomUUID().toString().replace("-", ""); + public ReqResult executeWorkflow(@PathVariable Long id, @RequestBody @Valid Map params, HttpServletRequest request) { + UserAccessKeyDto userAccessKey = (UserAccessKeyDto) RequestContext.get().getUserAccessKey(); + String requestId = request.getHeader("Trace-Id"); + if (requestId == null) { + requestId = UUID.randomUUID().toString().replace("-", ""); + } WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryPublishedWorkflowConfig(id, null, true); if (workflowConfigDto == null) { return ReqResult.error("Error workflow id or not published"); @@ -87,8 +96,9 @@ public class PluginAndWorkflowApiController { workflowExecuteRequestDto.setParams(params); workflowExecuteRequestDto.setRequestId(requestId); workflowExecuteRequestDto.setAgentId(-1L); - response.setCharacterEncoding("utf-8"); + workflowExecuteRequestDto.setApiKey(userAccessKey.getAccessKey()); WorkflowExecutingDto workflowExecutingDto = workflowApplicationService.executeWorkflow(workflowExecuteRequestDto, workflowConfigDto).blockLast(); + assert workflowExecutingDto != null; return ReqResult.success(workflowExecutingDto.getData()); } @@ -106,7 +116,11 @@ public class PluginAndWorkflowApiController { @Operation(summary = "插件运行接口") @RequestMapping(path = "/api/v1/plugin/{id}/execute", method = RequestMethod.POST) public ReqResult execute(@PathVariable Long id, @RequestBody Map params, HttpServletRequest request) { - String requestId = UUID.randomUUID().toString().replace("-", ""); + UserAccessKeyDto userAccessKey = (UserAccessKeyDto) RequestContext.get().getUserAccessKey(); + String requestId = request.getHeader("Trace-Id"); + if (requestId == null) { + requestId = UUID.randomUUID().toString().replace("-", ""); + } PluginDto pluginDto = pluginApplicationService.queryPublishedPluginConfig(id, null); if (pluginDto == null) { throw new BizException("Error plugin id or not published"); @@ -116,13 +130,27 @@ public class PluginAndWorkflowApiController { pluginExecuteRequestDto.setParams(params); pluginExecuteRequestDto.setPluginId(id); pluginExecuteRequestDto.setRequestId(requestId); - pluginExecuteRequestDto.setRequestId(UUID.randomUUID().toString()); pluginExecuteRequestDto.setAgentId(-1L); pluginExecuteRequestDto.setTest(false); + pluginExecuteRequestDto.setApiKey(userAccessKey.getAccessKey()); PluginExecuteResultDto pluginExecuteResultDto = pluginApplicationService.execute(pluginExecuteRequestDto, pluginDto); return ReqResult.success(pluginExecuteResultDto.getResult()); } + + @Operation(summary = "工具执行接口") + @RequestMapping(path = "/api/v1/tool/{id}", method = RequestMethod.POST) + public Object executeTool(@PathVariable String id, @RequestBody Map params, HttpServletRequest request) { + try { + if (id.startsWith("p")) { + return execute(Long.parseLong(id.substring(1)), params, request).getData(); + } + return executeWorkflow(Long.parseLong(id.substring(1)), params, request).getData(); + } catch (Exception e) { + return Map.of("error", Map.of("message", e.getMessage(), "code", "0001")); + } + } + private void checkPermission(List publishedSpaceIds, Published.PublishScope scope) { if (scope == Published.PublishScope.Tenant) { return; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/DevAgentConfigDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/DevAgentConfigDTO.java new file mode 100644 index 00000000..131c31c1 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/DevAgentConfigDTO.java @@ -0,0 +1,15 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class DevAgentConfigDTO { + + private String systemPrompt; + private String openingChatMsg; + private List tools; + private List skills; + private List mcpConfigs; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/DevAgentConfigUpdateDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/DevAgentConfigUpdateDTO.java new file mode 100644 index 00000000..4ca959cd --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/DevAgentConfigUpdateDTO.java @@ -0,0 +1,17 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class DevAgentConfigUpdateDTO { + + @Schema(description = "开发的AgentID,DEV_AGENT_ID 环境变量", requiredMode = Schema.RequiredMode.REQUIRED) + private Long devAgentId; + + @Schema(description = "系统提示词,留空不修改") + private String systemPrompt; + + @Schema(description = "开始聊天提示词,留空不修改") + private String openingChatMsg; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/KbQueryResultDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/KbQueryResultDTO.java new file mode 100644 index 00000000..80feaa74 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/KbQueryResultDTO.java @@ -0,0 +1,22 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import com.xspaceagi.knowledge.sdk.response.KnowledgeQaVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class KbQueryResultDTO { + @Schema(description = "检索结果") + private List items; + @Schema(description = "错误信息") + private Map error; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/McpResultDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/McpResultDTO.java new file mode 100644 index 00000000..a98f8a7b --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/McpResultDTO.java @@ -0,0 +1,17 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class McpResultDTO { + private String name; + private String description; + private String serverConfig; + private String usedTool; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/SkillResultItemDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/SkillResultItemDTO.java new file mode 100644 index 00000000..7a409169 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/SkillResultItemDTO.java @@ -0,0 +1,17 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SkillResultItemDTO { + private Long id; + private String name; + private String description; + private String downloadUrl; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolAddDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolAddDTO.java new file mode 100644 index 00000000..271f6a88 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolAddDTO.java @@ -0,0 +1,13 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class ToolAddDTO { + @Schema(description = "开发的AgentID,DEV_AGENT_ID 环境变量", requiredMode = Schema.RequiredMode.REQUIRED) + private Long devAgentId; + private Published.TargetType targetType; + private Long targetId; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolExecuteDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolExecuteDTO.java new file mode 100644 index 00000000..bf1b843b --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolExecuteDTO.java @@ -0,0 +1,23 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Map; + +@Data +public class ToolExecuteDTO { + + @Schema(description = "开发项目在平台注册的ID,从环境变量中获取 DEV_AGENT_ID") + private Long devAgentId; + + @Schema(description = "目标类型,Plugin 插件API,Workflow 工作流API,Knowledge 知识库") + private Published.TargetType targetType; + + @Schema(description = "目标对象ID") + private Long targetId; + + @Schema(description = "目标对象参数") + private Map params; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolSearchDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolSearchDTO.java new file mode 100644 index 00000000..0fc2a120 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolSearchDTO.java @@ -0,0 +1,23 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class ToolSearchDTO { + + @Schema(description = "开发空间ID,使用 DEV_SPACE_ID 环境变量", requiredMode = Schema.RequiredMode.REQUIRED) + private Long devSpaceId; + + @Schema(description = "搜索数据类型,包括 tool、skill", requiredMode = Schema.RequiredMode.REQUIRED) + private String type; + + @Schema(description = "页码") + private Integer page; + + @Schema(description = "每页数量") + private Integer pageSize; + + @Schema(description = "关键字搜索") + private String kw; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolSearchResultItemDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolSearchResultItemDTO.java new file mode 100644 index 00000000..e8a71c60 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/ToolSearchResultItemDTO.java @@ -0,0 +1,30 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ToolSearchResultItemDTO { + + @Schema(description = "目标类型,Plugin 插件API,Workflow 工作流API,Knowledge 知识库") + private Published.TargetType targetType; + + @Schema(description = "目标对象ID") + private Long targetId; + + @Schema(description = "目标对象名称") + private String name; + + @Schema(description = "目标对象描述") + private String description; + + @Schema(description = "目标对象接口定义") + private String schema; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/WorkflowExecutingDTO.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/WorkflowExecutingDTO.java new file mode 100644 index 00000000..be224ee9 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/api/dto/WorkflowExecutingDTO.java @@ -0,0 +1,28 @@ +package com.xspaceagi.agent.web.ui.controller.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class WorkflowExecutingDTO { + + @Schema(description = "事件类型") + private EventType type; + @Schema(description = "事件数据") + private Object data; + @Schema(description = "错误信息") + private Map error; + + public enum EventType { + Heartbeat, + FinalResult, + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/base/ConversationPermissionChecker.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/base/ConversationPermissionChecker.java new file mode 100644 index 00000000..abaf5fec --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/base/ConversationPermissionChecker.java @@ -0,0 +1,103 @@ +package com.xspaceagi.agent.web.ui.controller.base; + +import com.xspaceagi.agent.core.adapter.application.ConversationApplicationService; +import com.xspaceagi.agent.core.adapter.dto.ConversationDto; +import com.xspaceagi.sandbox.sdk.server.ISandboxConfigRpcService; +import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigRpcDto; +import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum; +import com.xspaceagi.system.sdk.permission.SpacePermissionService; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.enums.ErrorCodeEnum; +import com.xspaceagi.system.spec.exception.BizException; +import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +@Component +public class ConversationPermissionChecker { + + @Resource + private ConversationApplicationService conversationApplicationService; + @Resource + private SpacePermissionService spacePermissionService; + @Resource + private ISandboxConfigRpcService sandboxConfigRpcService; + + /** + * 校验当前用户对会话的访问权限: + * 1. cId 非空校验 + * 2. 会话存在性校验 + * 3. 如果是会话创建者,直接通过 + * 4. 如果不是创建者,校验开发空间权限 + * + * @param cId 会话ID + * @return 会话创建者用户ID + */ + public Long check(Long cId) { + return check(cId, null); + } + + /** + * 校验当前用户对会话的访问权限,并在传入 customTargetDir 时校验会话电脑类型: + * 云端电脑(sandboxServerId 为空或 -1)不允许指定 customTargetDir,个人电脑允许。 + */ + public Long check(Long cId, String customTargetDir) { + if (cId == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "cId"); + } + ConversationDto conversation = conversationApplicationService.getConversationByCid(cId); + if (conversation == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentConversationNotFound); + } + validateCustomTargetDir(conversation, customTargetDir); + Long conversationUserId = conversation.getUserId(); + if (RequestContext.get().getUserId().equals(conversationUserId)) { + return conversationUserId; + } + Long devSpaceId = conversation.getDevSpaceId(); + if (devSpaceId != null) { + spacePermissionService.checkSpaceUserPermission(devSpaceId, RequestContext.get().getUserId()); + } else { + throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied); + } + return conversationUserId; + } + + /** + * 校验 customTargetDir 是否允许用于指定会话(不校验用户权限,适用于已有独立鉴权逻辑的接口)。 + */ + public void checkCustomTargetDir(Long cId, String customTargetDir) { + if (cId == null || StringUtils.isBlank(customTargetDir)) { + return; + } + ConversationDto conversation = conversationApplicationService.getConversationByCid(cId); + if (conversation == null) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentConversationNotFound); + } + validateCustomTargetDir(conversation, customTargetDir); + } + + private void validateCustomTargetDir(ConversationDto conversation, String customTargetDir) { + if (StringUtils.isBlank(customTargetDir)) { + return; + } + String sandboxServerId = conversation.getSandboxServerId(); + if (sandboxServerId == null || "-1".equals(sandboxServerId)) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.validationFailedWithDetail, + "customTargetDir is not supported for cloud computer"); + } + long sandboxConfigId; + try { + sandboxConfigId = Long.parseLong(sandboxServerId); + } catch (NumberFormatException e) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.validationFailedWithDetail, + "customTargetDir is not supported for cloud computer"); + } + SandboxConfigRpcDto sandboxConfigDto = sandboxConfigRpcService.queryById(sandboxConfigId); + if (sandboxConfigDto == null || sandboxConfigDto.getScope() != SandboxScopeEnum.USER) { + throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.validationFailedWithDetail, + "customTargetDir is not supported for cloud computer"); + } + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/PermissionRequestResponseDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/PermissionRequestResponseDto.java new file mode 100644 index 00000000..2b8feced --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/PermissionRequestResponseDto.java @@ -0,0 +1,22 @@ +package com.xspaceagi.agent.web.ui.controller.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class PermissionRequestResponseDto { + + @Schema(description = "Tool Id") + private String toolId; + @Schema(description = "Conversation Id") + private Long conversationId; + @Schema(description = "Option") + private Option option; + + @Data + public static class Option { + @Schema(description = "Option Id") + private String optionId; + private String outcome; + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/PublishItemDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/PublishItemDto.java index a9d3536d..dba3cff1 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/PublishItemDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/PublishItemDto.java @@ -36,4 +36,7 @@ public class PublishItemDto { @Schema(description = "发布者信息") private PublishUserDto publishUser; + + @Schema(description = "分类") + private String category; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/ResourceGroupQueryDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/ResourceGroupQueryDto.java new file mode 100644 index 00000000..4702f5ce --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/ResourceGroupQueryDto.java @@ -0,0 +1,14 @@ +package com.xspaceagi.agent.web.ui.controller.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import lombok.Data; + +import java.util.List; + +@Data +public class ResourceGroupQueryDto { + + private Long spaceId; + private List types; + private String name; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/ResourceTargetDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/ResourceTargetDto.java new file mode 100644 index 00000000..1f615ccd --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/ResourceTargetDto.java @@ -0,0 +1,11 @@ +package com.xspaceagi.agent.web.ui.controller.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import lombok.Data; + +@Data +public class ResourceTargetDto { + + private Published.TargetType targetType; + private Long targetId; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitBaseReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitBaseReq.java new file mode 100644 index 00000000..c6fb465f --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitBaseReq.java @@ -0,0 +1,18 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "Git 基础请求") +public class GitBaseReq { + + @Schema(description = "工作空间类型: taskAgent / pageApp") + private String workspaceType; + + @Schema(description = "会话ID(taskAgent 模式)") + private Long cId; + + @Schema(description = "项目ID(pageApp 模式)") + private Long projectId; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitBranchReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitBranchReq.java new file mode 100644 index 00000000..e9b2373f --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitBranchReq.java @@ -0,0 +1,20 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git branch 请求(创建/切换/删除共用)") +public class GitBranchReq extends GitBaseReq { + + @Schema(description = "分支名称") + private String branchName; + + @Schema(description = "起始点(创建分支时使用)") + private String startPoint; + + @Schema(description = "是否强制删除(删除分支时使用)") + private Boolean force; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitCheckoutReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitCheckoutReq.java new file mode 100644 index 00000000..f67e6661 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitCheckoutReq.java @@ -0,0 +1,14 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git checkout 请求(将目标版本文件检出到工作区和暂存区,HEAD 不动)") +public class GitCheckoutReq extends GitBaseReq { + + @Schema(description = "检出目标(commit hash / tag / branch)") + private String target; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitCommitReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitCommitReq.java new file mode 100644 index 00000000..01861d02 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitCommitReq.java @@ -0,0 +1,19 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git commit 请求") +public class GitCommitReq extends GitBaseReq { + + @Schema(description = "提交信息") + private String message; + + @Schema(description = "提交文件列表") + private List files; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitDiffReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitDiffReq.java new file mode 100644 index 00000000..4b1e58fb --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitDiffReq.java @@ -0,0 +1,25 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git diff 请求") +public class GitDiffReq extends GitBaseReq { + + @Schema(description = "起始提交/引用") + private String from; + + @Schema(description = "目标提交/引用") + private String to; + + @Schema(description = "对比来源: worktree(工作区vs HEAD), staged(暂存区vs HEAD), commit(两个commit之间)") + private String source; + + @Schema(description = "文件路径列表") + private List paths; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitFileContentReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitFileContentReq.java new file mode 100644 index 00000000..6c970f25 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitFileContentReq.java @@ -0,0 +1,17 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git 文件内容请求") +public class GitFileContentReq extends GitBaseReq { + + @Schema(description = "git 引用(commit hash / 分支名 / HEAD / HEAD~1 / worktree / staged)") + private String ref; + + @Schema(description = "文件相对路径") + private String filePath; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitLogReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitLogReq.java new file mode 100644 index 00000000..2546acb8 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitLogReq.java @@ -0,0 +1,23 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git log 请求") +public class GitLogReq extends GitBaseReq { + + @Schema(description = "页码,从1开始") + private Integer page; + + @Schema(description = "每页条数") + private Integer pageSize; + + @Schema(description = "分支名称") + private String branch; + + @Schema(description = "文件相对路径,指定时只返回该文件的提交历史") + private String filePath; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitResetReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitResetReq.java new file mode 100644 index 00000000..bdc9befb --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitResetReq.java @@ -0,0 +1,17 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git reset 请求") +public class GitResetReq extends GitBaseReq { + + @Schema(description = "重置目标(commit hash / tag / branch)") + private String target; + + @Schema(description = "重置模式: soft / mixed / hard(默认 mixed)") + private String mode; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitRevertReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitRevertReq.java new file mode 100644 index 00000000..6fb6e781 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitRevertReq.java @@ -0,0 +1,17 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git revert 请求(新建 commit 使文件树回退到 target 版本,保留完整历史)") +public class GitRevertReq extends GitBaseReq { + + @Schema(description = "回退目标(commit hash / tag / branch)", required = true) + private String target; + + @Schema(description = "自定义 commit message,为空时默认 'Revert to '") + private String message; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitTagReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitTagReq.java new file mode 100644 index 00000000..b11c1cd3 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitTagReq.java @@ -0,0 +1,17 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git tag 请求(创建/删除共用)") +public class GitTagReq extends GitBaseReq { + + @Schema(description = "标签名称") + private String tagName; + + @Schema(description = "标签信息(创建时使用)") + private String message; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitUnstageReq.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitUnstageReq.java new file mode 100644 index 00000000..aea158b3 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/dto/git/GitUnstageReq.java @@ -0,0 +1,16 @@ +package com.xspaceagi.agent.web.ui.controller.dto.git; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "Git unstage 请求") +public class GitUnstageReq extends GitBaseReq { + + @Schema(description = "取消暂存的文件列表") + private List files; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/manage/RecommendManageController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/manage/RecommendManageController.java new file mode 100644 index 00000000..3e4a775e --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/manage/RecommendManageController.java @@ -0,0 +1,257 @@ +package com.xspaceagi.agent.web.ui.controller.manage; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendResponse; +import com.xspaceagi.agent.core.adapter.dto.recommend.TargetRecommendSaveRequest; +import com.xspaceagi.agent.core.adapter.repository.TargetRecommendRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.TargetRecommend; +import com.xspaceagi.agent.core.infra.dao.mapper.TargetRecommendMapper; +import com.xspaceagi.agent.web.ui.controller.manage.dto.ManagePageResponse; +import com.xspaceagi.agent.web.ui.controller.manage.dto.ManageQueryRequest; +import com.xspaceagi.system.application.dto.permission.SortIndexUpdateDto; +import com.xspaceagi.system.spec.annotation.RequireResource; +import com.xspaceagi.system.spec.common.RequestContext; +import com.xspaceagi.system.spec.dto.ReqResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.xspaceagi.system.spec.enums.ResourceEnum.*; + +@Tag(name = "展示推荐管理") +@RestController +@RequestMapping("/api/system/display/recommend") +public class RecommendManageController extends BaseManageController { + + @Resource + private TargetRecommendMapper targetRecommendMapper; + + @Resource + private TargetRecommendRepository targetRecommendRepository; + + @RequireResource(DISPLAY_RECOMMEND_QUERY) + @Operation(summary = "查询推荐列表") + @PostMapping("/list") + public ReqResult> list(@RequestBody ManageQueryRequest request) { + Page page = new Page<>(request.getPageNo(), request.getPageSize()); + Long tenantId = RequestContext.get().getTenantId(); + TargetRecommend.RecType recType = null; + if (StringUtils.isNotBlank(request.getRecType())) { + try { + recType = TargetRecommend.RecType.valueOf(request.getRecType()); + } catch (IllegalArgumentException e) { + return ReqResult.error("recType参数无效"); + } + } + TargetRecommend.TargetType targetType = null; + if (StringUtils.isNotBlank(request.getTargetType())) { + try { + targetType = TargetRecommend.TargetType.valueOf(request.getTargetType()); + } catch (IllegalArgumentException e) { + return ReqResult.error("targetType参数无效"); + } + } + TargetRecommend.RecType filterRecType = recType; + TargetRecommend.TargetType filterTargetType = targetType; + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(TargetRecommend::getTenantId, tenantId) + .like(request.getName() != null, TargetRecommend::getLabel, request.getName()) + .eq(filterRecType != null, TargetRecommend::getRecType, filterRecType) + .eq(filterTargetType != null, TargetRecommend::getTargetType, filterTargetType) + .orderByAsc(TargetRecommend::getSort) + .orderByDesc(TargetRecommend::getCreated); + + IPage result = targetRecommendMapper.selectPage(page, wrapper); + + List items = result.getRecords().stream() + .map(this::toResponse) + .collect(Collectors.toList()); + + ManagePageResponse response = ManagePageResponse.builder() + .total(result.getTotal()) + .pageNo(request.getPageNo()) + .pageSize(request.getPageSize()) + .records(items) + .build(); + + return ReqResult.success(response); + } + + @RequireResource(DISPLAY_RECOMMEND_SAVE) + @Operation(summary = "新增推荐") + @PostMapping("/save") + public ReqResult save(@RequestBody TargetRecommendSaveRequest request) { + String validateError = validateSaveRequest(request); + if (validateError != null) { + return ReqResult.error(validateError); + } + Long tenantId = RequestContext.get().getTenantId(); + String duplicateError = validateDuplicate(request, tenantId); + if (duplicateError != null) { + return ReqResult.error(duplicateError); + } + TargetRecommend entity = toEntity(request); + entity.setTenantId(tenantId); + if (entity.getSort() == null) { + entity.setSort(0); + } + targetRecommendMapper.insert(entity); + return ReqResult.success(null); + } + + @RequireResource(DISPLAY_RECOMMEND_SAVE) + @Operation(summary = "编辑推荐") + @PostMapping("/update") + public ReqResult update(@RequestBody TargetRecommendSaveRequest request) { + String validateError = validateSaveRequest(request); + if (validateError != null) { + return ReqResult.error(validateError); + } + Long tenantId = RequestContext.get().getTenantId(); + String duplicateError = validateDuplicate(request, tenantId); + if (duplicateError != null) { + return ReqResult.error(duplicateError); + } + TargetRecommend entity = toEntity(request); + entity.setId(request.getId()); + entity.setTenantId(tenantId); + targetRecommendMapper.updateById(entity); + return ReqResult.success(null); + } + + @RequireResource(DISPLAY_RECOMMEND_DELETE) + @Operation(summary = "删除推荐") + @PostMapping("/delete/{id}") + public ReqResult delete(@PathVariable Long id) { + targetRecommendMapper.deleteById(id); + return ReqResult.success(null); + } + + @RequireResource(DISPLAY_RECOMMEND_SAVE) + @Operation(summary = "批量调整排序") + @PostMapping("/updateSort") + public ReqResult updateSort(@RequestBody SortIndexUpdateDto dto) { + if (dto == null || CollectionUtils.isEmpty(dto.getItems())) { + return ReqResult.error("参数不能为空"); + } + List updateList = dto.getItems().stream() + .map(item -> { + TargetRecommend entity = new TargetRecommend(); + entity.setId(item.getId()); + entity.setSort(item.getSortIndex()); + return entity; + }) + .collect(Collectors.toList()); + targetRecommendRepository.updateBatchById(updateList); + return ReqResult.success(null); + } + + private String validateSaveRequest(TargetRecommendSaveRequest request) { + if (request == null) { + return "参数不能为空"; + } + if (StringUtils.isBlank(request.getRecType())) { + return "recType不能为空"; + } + if (StringUtils.isBlank(request.getTargetType())) { + return "targetType不能为空"; + } + if (request.getTargetId() == null) { + return "targetId不能为空"; + } + try { + TargetRecommend.RecType.valueOf(request.getRecType()); + TargetRecommend.TargetType.valueOf(request.getTargetType()); + } catch (IllegalArgumentException e) { + return "recType或targetType参数无效"; + } + return null; + } + + private String validateDuplicate(TargetRecommendSaveRequest request, Long tenantId) { + if (StringUtils.isBlank(request.getRecType())) { + return null; + } + TargetRecommend.RecType recType = TargetRecommend.RecType.valueOf(request.getRecType()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(TargetRecommend::getTenantId, tenantId) + .eq(TargetRecommend::getRecType, recType); + if (request.getId() != null) { + wrapper.ne(TargetRecommend::getId, request.getId()); + } + + if (recType == TargetRecommend.RecType.Home || recType == TargetRecommend.RecType.Official) { + wrapper.eq(TargetRecommend::getTargetType, TargetRecommend.TargetType.valueOf(request.getTargetType())) + .eq(TargetRecommend::getTargetId, request.getTargetId()); + } else if (recType == TargetRecommend.RecType.ChatBoxNav) { + if (StringUtils.isBlank(request.getFunctionType())) { + return null; + } + TargetRecommend.FunctionType functionType = TargetRecommend.FunctionType.valueOf(request.getFunctionType()); + wrapper.eq(TargetRecommend::getFunctionType, functionType) + .eq(TargetRecommend::getTargetType, TargetRecommend.TargetType.valueOf(request.getTargetType())); + if (functionType == TargetRecommend.FunctionType.Chat) { + wrapper.eq(TargetRecommend::getTargetId, request.getTargetId()); + } + } else { + return null; + } + + Long count = targetRecommendMapper.selectCount(wrapper); + if (count != null && count > 0) { + if (recType == TargetRecommend.RecType.ChatBoxNav) { + TargetRecommend.FunctionType functionType = TargetRecommend.FunctionType.valueOf(request.getFunctionType()); + if (functionType == TargetRecommend.FunctionType.Chat) { + return "相同功能类型、推荐类型、目标类型和目标已存在"; + } + return "相同功能类型、推荐类型和目标类型的配置已存在"; + } + return "相同推荐类型、目标类型和目标已存在"; + } + return null; + } + + private TargetRecommend toEntity(TargetRecommendSaveRequest request) { + TargetRecommend entity = new TargetRecommend(); + if (StringUtils.isNotBlank(request.getTargetType())) { + entity.setTargetType(TargetRecommend.TargetType.valueOf(request.getTargetType())); + } + entity.setTargetId(request.getTargetId()); + if (StringUtils.isNotBlank(request.getRecType())) { + entity.setRecType(TargetRecommend.RecType.valueOf(request.getRecType())); + } + if (StringUtils.isNotBlank(request.getFunctionType())) { + entity.setFunctionType(TargetRecommend.FunctionType.valueOf(request.getFunctionType())); + } + entity.setLabel(request.getLabel()); + entity.setIcon(request.getIcon()); + entity.setPlaceholder(request.getPlaceholder()); + entity.setSort(request.getSort()); + return entity; + } + + private TargetRecommendResponse toResponse(TargetRecommend entity) { + return TargetRecommendResponse.builder() + .id(entity.getId()) + .tenantId(entity.getTenantId()) + .targetType(entity.getTargetType() != null ? entity.getTargetType().name() : null) + .targetId(entity.getTargetId()) + .recType(entity.getRecType() != null ? entity.getRecType().name() : null) + .functionType(entity.getFunctionType() != null ? entity.getFunctionType().name() : null) + .label(entity.getLabel()) + .icon(entity.getIcon()) + .placeholder(entity.getPlaceholder()) + .sort(entity.getSort()) + .modified(entity.getModified()) + .created(entity.getCreated()) + .build(); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/manage/dto/ManageQueryRequest.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/manage/dto/ManageQueryRequest.java index e7221df3..41f4500d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/manage/dto/ManageQueryRequest.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/manage/dto/ManageQueryRequest.java @@ -29,4 +29,10 @@ public class ManageQueryRequest { @Schema(description = "管控条件") private Integer accessControl; + + @Schema(description = "推荐类型:Home、Official、ChatBoxNav") + private String recType; + + @Schema(description = "目标类型:Agent、PageApp、Skill、Plugin、Workflow") + private String targetType; } \ No newline at end of file diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/dto/ComponentQueryDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/dto/ComponentQueryDto.java new file mode 100644 index 00000000..b7b2a1b4 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/dto/ComponentQueryDto.java @@ -0,0 +1,20 @@ +package com.xspaceagi.agent.web.ui.dto; + +import com.xspaceagi.agent.core.adapter.repository.entity.Published; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +public class ComponentQueryDto { + + @Schema(description = "空间ID") + private Long spaceId; + + @Schema(description = "分组ID") + private Long groupId; + + @Schema(description = "组件类型") + private List types; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/dto/SkillDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/dto/SkillDto.java index 7d8baa6f..c5d1755d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/dto/SkillDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/dto/SkillDto.java @@ -77,4 +77,7 @@ public class SkillDto implements Serializable { @Schema(description = "权限列表") private List permissions; + + @Schema(description = "开发时使用的会话ID") + private Long devAgentConversationId; } \ No newline at end of file