You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/09/15 02:58:17 UTC
(rocketmq-dashboard) branch rocketmq-studio updated: feat(server): add MCP tool framework and AI integration. (#4225)
This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 6d29cb737 feat(server): add MCP tool framework and AI integration. (#4225)
6d29cb737 is described below
commit 6d29cb737be542505a6b1a7c652d32a6e55e4124
Author: echooymxq <ec...@gmail.com>
AuthorDate: Tue Sep 15 10:58:08 2026 +0800
feat(server): add MCP tool framework and AI integration. (#4225)
---
server/pom.xml | 42 +-
.../rocketmq/studio/auth/AuthInterceptor.java | 53 +-
.../apache/rocketmq/studio/auth/AuthWebConfig.java | 24 +-
.../cluster/broker/BrokerConfigDiffService.java | 9 +
.../studio/cluster/broker/ClusterProvider.java | 12 +-
.../studio/cluster/broker/ClusterService.java | 69 +-
.../cluster/broker/RuntimeAdminClientResolver.java | 42 +-
.../studio/cluster/client/ClientConnectionVO.java | 1 +
.../studio/cluster/metrics/MetricsService.java | 18 +
.../nameserver/NameServerConfigDiffService.java | 8 +
.../studio/cluster/proxy/ProxyAddressService.java | 38 +
.../config/LegacyJackson2Config.java} | 37 +-
.../studio/common/exception/BusinessException.java | 5 +
.../rocketmq/studio/instance/InstanceResolver.java | 59 ++
.../studio/instance/acl/AclRepository.java | 2 +
.../rocketmq/studio/instance/acl/AclService.java | 34 +-
.../instance/acl/MybatisPlusAclRepository.java | 6 +
.../rocketmq/studio/instance/dlq/DLQProvider.java | 2 +-
.../studio/instance/topic/MetadataService.java | 214 ++++-
.../rocketmq/studio/ops/ai/AiController.java | 33 +-
.../apache/rocketmq/studio/ops/ai/AiService.java | 33 -
.../ops/ai/{AiToolVO.java => AiToolCallDTO.java} | 14 +-
.../apache/rocketmq/studio/ops/ai/AiToolVO.java | 4 +
.../rocketmq/studio/ops/ai/McpToolController.java | 64 ++
.../ai/{AiController.java => ToolController.java} | 50 +-
.../studio/ops/ai/ToolExceptionHandler.java | 75 ++
.../ai/auth/McpAuthentication.java} | 28 +-
.../ai/auth/McpAuthenticationException.java} | 15 +-
.../ops/ai/auth/McpAuthenticationFilter.java | 84 ++
.../studio/ops/ai/auth/McpAuthenticator.java | 222 ++++++
.../studio/ops/ai/mcp/McpConfiguration.java | 101 +++
.../studio/ops/ai/mcp/McpToolRegistrar.java | 141 ++++
.../ops/ai/tool/AlertRuleListToolHandler.java | 82 --
.../ops/ai/tool/CapabilitiesToolHandler.java | 64 --
.../studio/ops/ai/tool/CapabilityResolver.java | 64 --
.../studio/ops/ai/tool/ClusterListToolHandler.java | 73 --
.../ops/ai/tool/ConsumerGroupListToolHandler.java | 92 ---
.../ops/ai/tool/DashboardSummaryToolHandler.java | 111 ---
.../ops/ai/tool/MessageQueryToolHandler.java | 127 ---
.../ops/ai/tool/MessageTraceToolHandler.java | 89 ---
.../studio/ops/ai/tool/ToolAccessPolicy.java | 86 --
.../studio/ops/ai/tool/ToolDefinition.java | 83 --
.../studio/ops/ai/tool/ToolGatewayService.java | 252 ------
.../studio/ops/ai/tool/ToolListPagination.java | 61 --
.../studio/ops/ai/tool/TopicListToolHandler.java | 85 --
.../ops/ai/tool/catalog/CapabilityResolver.java | 72 ++
.../ops/ai/tool/{ => catalog}/ToolCatalog.java | 130 +--
.../ai/tool/contract/acl/AclListInput.java} | 22 +-
.../ops/ai/tool/contract/acl/AclMutationInput.java | 62 ++
.../ops/ai/tool/contract/acl/AclRuleItem.java | 48 ++
.../ai/tool/contract/acl/AclUserItem.java} | 33 +-
.../ai/tool/contract/acl/UserCreateInput.java} | 17 +-
.../tool/contract/alert/AlertRuleListInput.java} | 13 +-
.../ai/tool/contract/alert/AlertRuleListItem.java | 54 ++
.../tool/contract/broker/BrokerConfigOutput.java | 98 +++
.../contract/broker/BrokerConfigUpdateInput.java} | 22 +-
.../contract/broker/BrokerConfigUpdateOutput.java | 49 ++
.../tool/contract/broker/BrokerDescribeInput.java} | 13 +-
.../tool/contract/client/ClientDescribeInput.java} | 16 +-
.../ai/tool/contract/client/ClientInstance.java | 44 ++
.../ai/tool/contract/client/ClientListInput.java} | 13 +-
.../tool/contract/cluster/ClusterListInput.java} | 13 +-
.../ai/tool/contract/cluster/ClusterListItem.java} | 43 +-
.../tool/contract/common/BrokerClusterInput.java} | 13 +-
.../tool/contract/common/CapabilitiesOutput.java} | 15 +-
.../ai/tool/contract/common/ClusterInput.java} | 13 +-
.../tool/contract/common/ClusterListOutput.java} | 13 +-
.../ai/tool/contract/common/ListOutput.java} | 13 +-
.../ai/tool/contract/common/MutationOutput.java} | 22 +-
.../ai/tool/contract/common/PageOutput.java} | 34 +-
.../ai/tool/contract/common/PageRequest.java} | 18 +-
.../tool/contract/common/ResourceDeleteInput.java} | 13 +-
.../ai/tool/contract/common/TimeRange.java} | 13 +-
.../ai/tool/contract/dlq/DLQClearInput.java} | 15 +-
.../ai/tool/contract/dlq/DLQListInput.java} | 19 +-
.../ops/ai/tool/contract/dlq/DLQListOutput.java | 95 +++
.../ai/tool/contract/dlq/DLQResendInput.java} | 17 +-
.../ai/tool/contract/dlq/DLQResendOutput.java} | 41 +-
.../ai/tool/contract/group/GroupClientsOutput.java | 63 ++
.../tool/contract/group/GroupDescribeInput.java} | 13 +-
.../tool/contract/group/GroupDescribeOutput.java | 117 +++
.../ops/ai/tool/contract/group/GroupInput.java | 78 ++
.../ai/tool/contract/group/GroupListInput.java} | 13 +-
.../ops/ai/tool/contract/group/GroupListItem.java | 50 ++
.../tool/contract/group/GroupProgressOutput.java | 73 ++
.../contract/group/GroupResetOffsetInput.java} | 13 +-
.../ai/tool/contract/group/GroupTopicInput.java} | 13 +-
.../ai/tool/contract/group/ResetOffsetOutput.java} | 13 +-
.../contract/group/SkipAccumulatedOutput.java} | 15 +-
.../ai/tool/contract/message/DlqMessageItem.java} | 45 +-
.../ops/ai/tool/contract/message/MessageItem.java | 50 ++
.../contract/message/MessageQueryByIdInput.java} | 13 +-
.../contract/message/MessageQueryByKeyInput.java} | 18 +-
.../message/MessageQueryByTopicInput.java} | 18 +-
.../tool/contract/message/MessageResendInput.java} | 19 +-
.../contract/message/MessageResendOutput.java} | 19 +-
.../tool/contract/message/MessageTraceOutput.java} | 34 +-
.../studio/ops/ai/tool/contract/ops/AuditItem.java | 46 ++
.../ai/tool/contract/ops/AuditListInput.java} | 23 +-
.../ai/tool/contract/ops/MetricsQueryInput.java} | 20 +-
.../ai/tool/contract/plan/PlanDescription.java} | 26 +-
.../studio/ops/ai/tool/contract/plan/ToolPlan.java | 136 ++++
.../contract/proxy/ProxyConfigUpdateInput.java} | 13 +-
.../contract/proxy/ProxyConfigUpdateOutput.java} | 16 +-
.../tool/contract/topic/LiteTopicListInput.java} | 13 +-
.../tool/contract/topic/TopicDescribeInput.java} | 13 +-
.../tool/contract/topic/TopicDescribeOutput.java | 126 +++
.../ops/ai/tool/contract/topic/TopicInput.java | 62 ++
.../ai/tool/contract/topic/TopicListInput.java} | 13 +-
.../ops/ai/tool/contract/topic/TopicListItem.java | 50 ++
.../ops/ai/tool/contract/topic/TopicOutput.java | 44 ++
.../ai/tool/contract/topic/TopicRouteInput.java} | 13 +-
.../ops/ai/tool/contract/topic/TopicRouteItem.java | 57 ++
.../ai/tool/contract/topic/TopicSendInput.java} | 19 +-
.../ai/tool/contract/topic/TopicSendOutput.java} | 21 +-
.../ai/tool/contract/topic/TopicUpdateInput.java | 59 ++
.../ai/tool/core/ToolDefinition.java} | 38 +-
.../studio/ops/ai/tool/core/ToolError.java | 198 +++++
.../ops/ai/tool/core/ToolExecutionContext.java | 129 +++
.../ops/ai/tool/core/ToolExecutionException.java | 62 ++
.../ai/tool/core/ToolExecutionResult.java} | 32 +-
.../ai/tool/core/ToolHandler.java} | 15 +-
.../studio/ops/ai/tool/core/ToolInvocation.java | 49 ++
.../ai/tool/core/ToolRiskLevel.java} | 22 +-
.../studio/ops/ai/tool/filter/ToolAuditFilter.java | 60 ++
.../ops/ai/tool/filter/ToolCapabilityFilter.java | 54 ++
.../ai/tool/filter/ToolExecutionFilter.java} | 24 +-
.../ToolFilterChain.java} | 38 +-
.../ops/ai/tool/filter/ToolMutationFilter.java | 89 +++
.../ToolValidationFilter.java} | 27 +-
.../CapabilitiesToolHandler.java} | 30 +-
.../ai/tool/handler/MutationToolHandler.java} | 27 +-
.../ai/tool/handler/acl/AclCreateToolHandler.java | 62 ++
.../ai/tool/handler/acl/AclDeleteToolHandler.java | 64 ++
.../ai/tool/handler/acl/AclListToolHandler.java | 54 ++
.../ai/tool/handler/acl/AclUpdateToolHandler.java | 79 ++
.../ai/tool/handler/acl/UserCreateToolHandler.java | 74 ++
.../ai/tool/handler/acl/UserDeleteToolHandler.java | 64 ++
.../acl/UserListToolHandler.java} | 32 +-
.../handler/alert/AlertRuleListToolHandler.java | 76 ++
.../handler/broker/BrokerConfigToolHandler.java | 50 ++
.../broker/BrokerConfigUpdateToolHandler.java | 84 ++
.../handler/broker/BrokerDescribeToolHandler.java | 53 ++
.../tool/handler/broker/BrokerListToolHandler.java | 53 ++
.../broker/BrokerRuntimeInfoToolHandler.java} | 44 +-
.../handler/client/ClientDescribeToolHandler.java | 64 ++
.../tool/handler/client/ClientListToolHandler.java | 61 ++
.../handler/cluster/ClusterListToolHandler.java | 61 ++
.../dashboard/DashboardSummaryToolHandler.java | 125 +++
.../ai/tool/handler/dlq/DLQClearToolHandler.java | 84 ++
.../ai/tool/handler/dlq/DLQListToolHandler.java | 87 +++
.../ai/tool/handler/dlq/DLQResendToolHandler.java | 80 ++
.../group/ConsumerGroupCreateToolHandler.java | 68 ++
.../group/ConsumerGroupDeleteToolHandler.java | 65 ++
.../group/ConsumerGroupListToolHandler.java | 57 ++
.../group/ConsumerGroupUpdateToolHandler.java | 72 ++
.../handler/group/GroupClientsToolHandler.java | 53 ++
.../handler/group/GroupDescribeToolHandler.java | 83 ++
.../handler/group/GroupProgressToolHandler.java | 53 ++
.../handler/group/GroupResetOffsetToolHandler.java | 104 +++
.../group/GroupSkipAccumulatedToolHandler.java | 78 ++
.../litetopic/LiteTopicCreateToolHandler.java | 75 ++
.../litetopic/LiteTopicListToolHandler.java | 50 ++
.../message/MessageQueryByIdToolHandler.java | 58 ++
.../message/MessageQueryByKeyToolHandler.java | 59 ++
.../message/MessageQueryByTopicToolHandler.java | 59 ++
.../handler/message/MessageResendToolHandler.java | 88 +++
.../handler/message/MessageTraceToolHandler.java | 83 ++
.../handler/metrics/MetricsQueryToolHandler.java | 80 ++
.../NameServerConfigDiffToolHandler.java | 23 +-
.../nameserver/NameserverListToolHandler.java | 72 ++
.../proxy/ProxyConfigUpdateToolHandler.java | 77 ++
.../handler/proxy/ProxyDescribeToolHandler.java | 53 ++
.../tool/handler/proxy/ProxyListToolHandler.java | 53 ++
.../tool/handler/topic/TopicCreateToolHandler.java | 69 ++
.../tool/handler/topic/TopicDeleteToolHandler.java | 66 ++
.../handler/topic/TopicDescribeToolHandler.java | 60 ++
.../tool/handler/topic/TopicListToolHandler.java | 57 ++
.../tool/handler/topic/TopicRouteToolHandler.java | 57 ++
.../tool/handler/topic/TopicSendToolHandler.java | 97 +++
.../tool/handler/topic/TopicUpdateToolHandler.java | 74 ++
.../ops/ai/tool/service/ToolDiscoveryService.java | 82 ++
.../ops/ai/tool/service/ToolExecutionService.java | 123 +++
.../ops/ai/tool/service/ToolSchemaValidator.java | 122 +++
.../ops/ai/tool/service/ToolTokenService.java | 220 ++++++
.../rocketmq/studio/ops/alert/AlertRepository.java | 2 +
.../rocketmq/studio/ops/alert/AlertService.java | 7 +
.../ops/alert/MybatisPlusAlertRepository.java | 7 +
.../rocketmq/studio/provider/InstanceProvider.java | 4 +
.../studio/provider/InstanceProviderRegistry.java | 10 +-
.../studio/provider/apache/AdminClient.java | 13 +-
.../provider/apache/ApacheInstanceProvider.java | 9 +-
.../provider/apache/RocketMQAdminClientImpl.java | 230 ++++--
.../provider/apache/RocketMQClusterProvider.java | 29 +-
.../apache/RocketMQDefaultClusterResolver.java | 86 ++
.../provider/apache/RocketMQMetadataProvider.java | 24 +-
server/src/main/resources/application.yml | 56 +-
.../src/main/resources/tool-catalog/manifest.yaml | 2 +
.../resources/tool-catalog/rmq-tools.schema.json | 8 +-
.../src/main/resources/tool-catalog/rmq-tools.yaml | 653 ----------------
.../src/main/resources/tool-catalog/tools/acl.yaml | 351 +++++++++
.../main/resources/tool-catalog/tools/alert.yaml | 69 ++
.../main/resources/tool-catalog/tools/broker.yaml | 396 ++++++++++
.../resources/tool-catalog/tools/capabilities.yaml | 36 +
.../main/resources/tool-catalog/tools/client.yaml | 182 +++++
.../main/resources/tool-catalog/tools/cluster.yaml | 61 ++
.../resources/tool-catalog/tools/dashboard.yaml | 126 +++
.../src/main/resources/tool-catalog/tools/dlq.yaml | 287 +++++++
.../main/resources/tool-catalog/tools/group.yaml | 870 +++++++++++++++++++++
.../resources/tool-catalog/tools/lite_topic.yaml | 179 +++++
.../main/resources/tool-catalog/tools/message.yaml | 371 +++++++++
.../main/resources/tool-catalog/tools/metrics.yaml | 94 +++
.../resources/tool-catalog/tools/nameserver.yaml | 155 ++++
.../main/resources/tool-catalog/tools/proxy.yaml | 198 +++++
.../main/resources/tool-catalog/tools/topic.yaml | 667 ++++++++++++++++
.../main/resources/tool-catalog/tools/user.yaml | 229 ++++++
.../studio/HealthProbeIntegrationTest.java | 2 +-
.../rocketmq/studio/StudioApplicationTest.java | 76 +-
.../rocketmq/studio/WebMvcAuthTestSupport.java | 48 ++
.../rocketmq/studio/auth/AuthControllerTest.java | 19 +-
.../studio/auth/AuthCorsIntegrationTest.java | 23 +-
...AuthCredentialAuthorizationIntegrationTest.java | 25 +-
.../rocketmq/studio/auth/AuthInterceptorTest.java | 17 +-
.../studio/auth/StudioUserControllerTest.java | 20 +-
.../broker/BrokerConfigDiffServiceTest.java | 15 +
.../cluster/broker/ClusterControllerTest.java | 20 +-
.../studio/cluster/broker/ClusterServiceTest.java | 77 +-
.../broker/RuntimeAdminClientResolverTest.java | 82 +-
.../cluster/client/ClientControllerTest.java | 16 +-
.../cluster/client/ProducerControllerTest.java | 16 +-
.../studio/cluster/k8s/K8sCertControllerTest.java | 16 +-
.../cluster/metrics/MetricsControllerTest.java | 18 +-
.../grafana/GrafanaDashboardControllerTest.java | 16 +-
.../NameServerConfigDiffServiceTest.java | 20 +
.../nameserver/NameServerControllerTest.java | 20 +-
.../cluster/proxy/ProxyAddressServiceTest.java | 42 +
.../cluster/proxy/ProxyCompatControllerTest.java | 16 +-
.../studio/cluster/proxy/ProxyControllerTest.java | 18 +-
.../studio/common/config/CorsConfigTest.java | 8 +-
.../exception/GlobalExceptionHandlerTest.java | 2 +-
.../studio/instance/InstanceControllerTest.java | 18 +-
.../studio/instance/InstanceResolverTest.java | 102 +++
.../studio/instance/acl/AclControllerTest.java | 18 +-
.../studio/instance/acl/AclServiceTest.java | 15 +-
.../studio/instance/dlq/DLQControllerTest.java | 16 +-
.../group/ConsumerGroupControllerTest.java | 20 +-
.../instance/message/MessageControllerTest.java | 16 +-
.../message/QueryHistoryControllerTest.java | 16 +-
.../instance/topic/LiteTopicControllerTest.java | 16 +-
.../studio/instance/topic/MetadataServiceTest.java | 158 +++-
.../studio/instance/topic/TopicControllerTest.java | 18 +-
.../rocketmq/studio/ops/OpsControllerTest.java | 16 +-
.../rocketmq/studio/ops/ai/AiControllerTest.java | 113 +--
.../rocketmq/studio/ops/ai/AiServiceTest.java | 87 +--
.../rocketmq/studio/ops/ai/LlmControllerTest.java | 15 +-
.../rocketmq/studio/ops/ai/ToolControllerTest.java | 168 ++++
.../ai/auth/McpCredentialAuthenticationTest.java | 374 +++++++++
.../studio/ops/ai/mcp/McpConfigurationTest.java | 71 ++
.../studio/ops/ai/mcp/McpToolRegistrarTest.java | 105 +++
.../ops/ai/tool/CapabilitiesToolHandlerTest.java | 51 +-
.../studio/ops/ai/tool/CapabilityResolverTest.java | 92 +++
.../ops/ai/tool/ClusterListToolHandlerTest.java | 78 +-
.../ai/tool/ConsumerGroupListToolHandlerTest.java | 75 --
.../ops/ai/tool/MessageQueryToolHandlerTest.java | 133 ----
.../ops/ai/tool/MessageTraceToolHandlerTest.java | 33 +-
.../ops/ai/tool/TestToolExecutionContexts.java | 26 +
.../studio/ops/ai/tool/ToolCatalogTest.java | 167 ----
.../studio/ops/ai/tool/ToolGatewayServiceTest.java | 865 --------------------
.../ops/ai/tool/TopicListToolHandlerTest.java | 78 --
.../ops/ai/tool/catalog/ToolCatalogTest.java | 117 +++
.../ai/tool/catalog/ToolCatalogTestSupport.java | 45 ++
.../ops/ai/tool/filter/ToolMutationFilterTest.java | 169 ++++
.../tool/handler/acl/AclCreateToolHandlerTest.java | 97 +++
.../tool/handler/acl/AclDeleteToolHandlerTest.java | 50 ++
.../tool/handler/acl/AclListToolHandlerTest.java | 82 ++
.../ai/tool/handler/acl/AclMutationPlanTest.java | 81 ++
.../tool/handler/acl/AclUpdateToolHandlerTest.java | 70 ++
.../broker/BrokerConfigUpdateToolHandlerTest.java | 78 ++
.../handler/broker/BrokerListToolHandlerTest.java | 70 ++
.../broker/NameserverListToolHandlerTest.java | 59 ++
.../client/ClientDescribeToolHandlerTest.java | 71 ++
.../handler/client/ClientListToolHandlerTest.java | 86 ++
.../dashboard/DashboardSummaryToolHandlerTest.java | 40 +
.../tool/handler/dlq/DLQClearToolHandlerTest.java | 44 ++
.../tool/handler/dlq/DLQListToolHandlerTest.java | 121 +++
.../tool/handler/dlq/DLQResendToolHandlerTest.java | 120 +++
.../group/ConsumerGroupReadToolHandlersTest.java | 147 ++++
.../tool/handler/group/GroupMutationPlanTest.java | 162 ++++
.../group/GroupResetOffsetToolHandlerTest.java | 50 ++
.../message/MessageQueryByIdToolHandlerTest.java | 80 ++
.../message/MessageQueryByKeyToolHandlerTest.java | 72 ++
.../MessageQueryByTopicToolHandlerTest.java | 82 ++
.../message/MessageResendToolHandlerTest.java | 88 +++
.../metrics/MetricsQueryToolHandlerTest.java | 92 +++
.../proxy/ProxyConfigUpdateToolHandlerTest.java | 50 ++
.../handler/proxy/ProxyListToolHandlerTest.java | 66 ++
.../tool/handler/topic/TopicMutationPlanTest.java | 156 ++++
.../ai/tool/service/ToolDiscoveryServiceTest.java | 133 ++++
.../tool/service/ToolExecutorInvocationTest.java | 175 +++++
.../ai/tool/service/ToolInstanceRoutingTest.java | 111 +++
.../tool/service/ToolOutputSchemaContractTest.java | 88 +++
.../tool/service/ToolTokenConfigurationTest.java | 168 ++++
.../ops/ai/tool/service/ToolTokenServiceTest.java | 132 ++++
.../ops/alert/AlertRuleAssetControllerTest.java | 16 +-
.../studio/ops/alert/AlertRuleControllerTest.java | 22 +-
.../studio/ops/alert/AlertSchemaMigrationTest.java | 8 +
.../ops/alert/AlertSilenceControllerTest.java | 8 +-
.../ops/alert/ClusterAlertRuleControllerTest.java | 22 +-
.../NativeAlertEvaluationTransactionTest.java | 16 +-
.../ops/alert/SystemAlertControllerTest.java | 18 +-
.../studio/ops/audit/AuditControllerTest.java | 16 +-
.../studio/ops/audit/AuditServiceTest.java | 4 +-
.../ops/dashboard/DashboardControllerTest.java | 16 +-
.../provider/InstanceProviderRegistryTest.java | 21 +-
.../apache/ApacheInstanceProviderTest.java | 20 +
.../apache/RocketMQAdminClientImplTest.java | 285 ++++++-
.../apache/RocketMQBrokerConfigServiceTest.java | 12 +-
.../apache/RocketMQClusterProviderTest.java | 26 +-
.../apache/RocketMQClusterResolverTest.java | 85 ++
.../apache/RocketMQMetadataProviderTest.java | 21 +
.../credential/CloudCredentialControllerTest.java | 15 +-
.../studio/settings/SettingsControllerTest.java | 16 +-
322 files changed, 18034 insertions(+), 4974 deletions(-)
diff --git a/server/pom.xml b/server/pom.xml
index 10855c3cc..b057278f8 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
- <version>3.5.3</version>
+ <version>4.1.0</version>
<relativePath/>
</parent>
@@ -18,15 +18,36 @@
<properties>
<java.version>21</java.version>
+ <spring-ai.version>2.0.0</spring-ai.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<rocketmq.version>5.5.0</rocketmq.version>
</properties>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.ai</groupId>
+ <artifactId>spring-ai-bom</artifactId>
+ <version>${spring-ai.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.springframework.ai</groupId>
+ <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-restclient</artifactId>
+ </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
@@ -42,12 +63,12 @@
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
- <version>2.0.4</version>
+ <version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
- <version>2.8.5</version>
+ <version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@@ -60,6 +81,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-webmvc-test</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
@@ -73,8 +99,13 @@
<!-- MyBatis-Plus + MySQL -->
<dependency>
<groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
- <version>3.5.7</version>
+ <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
+ <version>3.5.17</version>
+ </dependency>
+ <dependency>
+ <groupId>com.baomidou</groupId>
+ <artifactId>mybatis-plus-jsqlparser</artifactId>
+ <version>3.5.17</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
@@ -84,7 +115,6 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
- <scope>test</scope>
</dependency>
<!-- RocketMQ Admin Tools (kept from trunk #694) -->
<dependency>
diff --git a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
index 8e430788b..5be65010b 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
@@ -19,7 +19,8 @@ package org.apache.rocketmq.studio.auth;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import org.apache.rocketmq.studio.ops.ai.tool.ToolAccessPolicy;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
import org.apache.rocketmq.studio.settings.GeneralSettingsVO;
import org.apache.rocketmq.studio.settings.SettingsRepository;
import org.springframework.http.HttpMethod;
@@ -28,10 +29,14 @@ import org.springframework.http.MediaType;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.servlet.HandlerInterceptor;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
import java.util.Set;
public class AuthInterceptor implements HandlerInterceptor {
+ private static final String TOOL_EXECUTION_PREFIX = "/api/ai/tools/";
+ private static final String TOOL_EXECUTION_SUFFIX = "/execute";
private static final Set<String> READER_POST_PATHS = Set.of(
"/api/auth/logout",
"/api/auth/password",
@@ -42,19 +47,14 @@ public class AuthInterceptor implements HandlerInterceptor {
private final AuthProperties authProperties;
private final AuthService authService;
private final SettingsRepository settingsRepository;
- private final ToolAccessPolicy toolAccessPolicy;
+ private final ToolCatalog toolCatalog;
public AuthInterceptor(AuthProperties authProperties, AuthService authService,
- SettingsRepository settingsRepository) {
- this(authProperties, authService, settingsRepository, null);
- }
-
- public AuthInterceptor(AuthProperties authProperties, AuthService authService,
- SettingsRepository settingsRepository, ToolAccessPolicy toolAccessPolicy) {
+ SettingsRepository settingsRepository, ToolCatalog toolCatalog) {
this.authProperties = authProperties;
this.authService = authService;
this.settingsRepository = settingsRepository;
- this.toolAccessPolicy = toolAccessPolicy;
+ this.toolCatalog = toolCatalog;
}
@Override
@@ -62,9 +62,7 @@ public class AuthInterceptor implements HandlerInterceptor {
Object handler) throws Exception {
AuthenticatedUserContext.clear();
if (!isLoginRequired() || CorsUtils.isPreFlightRequest(request)
- || isPublicPath(requestPath(request)) || authService == null) {
- // Slice tests and minimal contexts may not provide AuthService; fall back to no
- // enforcement, matching the documented AuthWebConfig behaviour.
+ || isPublicPath(requestPath(request))) {
return true;
}
String authorization = AuthCookie.authorization(request, authProperties);
@@ -92,10 +90,7 @@ public class AuthInterceptor implements HandlerInterceptor {
* settings UI actually changes the enforced policy.
*/
private boolean isLoginRequired() {
- if (authProperties != null && authProperties.isLoginRequired()) {
- return true;
- }
- if (settingsRepository == null) {
+ if (authProperties.isLoginRequired()) {
return true;
}
try {
@@ -115,12 +110,34 @@ public class AuthInterceptor implements HandlerInterceptor {
return isAdminOnlyGetPath(path);
}
String normalizedPath = normalizePath(stripPathParameters(path));
- if (toolAccessPolicy != null && toolAccessPolicy.isToolExecutionPath(normalizedPath)) {
- return !toolAccessPolicy.isReaderAccessiblePath(normalizedPath);
+ if (isToolExecutionPath(normalizedPath)) {
+ return !isReaderAccessibleToolPath(normalizedPath);
}
return !HttpMethod.POST.matches(method) || !READER_POST_PATHS.contains(normalizePath(path));
}
+ private boolean isToolExecutionPath(String path) {
+ return path.startsWith(TOOL_EXECUTION_PREFIX)
+ && path.endsWith(TOOL_EXECUTION_SUFFIX);
+ }
+
+ private boolean isReaderAccessibleToolPath(String path) {
+ String encodedName = path.substring(
+ TOOL_EXECUTION_PREFIX.length(),
+ path.length() - TOOL_EXECUTION_SUFFIX.length());
+ if (encodedName.isBlank()) {
+ return false;
+ }
+ try {
+ String toolName = URLDecoder.decode(encodedName, StandardCharsets.UTF_8);
+ return toolCatalog.find(toolName)
+ .map(ToolDefinition::isLowRiskReadOnly)
+ .orElse(false);
+ } catch (IllegalArgumentException exception) {
+ return false;
+ }
+ }
+
private boolean isAdminOnlyGetPath(String path) {
String normalizedPath = normalizePath(stripPathParameters(path));
return "/api/llm/config".equals(normalizedPath)
diff --git a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthWebConfig.java b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthWebConfig.java
index 17d813147..ca3f929ad 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthWebConfig.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthWebConfig.java
@@ -18,9 +18,8 @@
package org.apache.rocketmq.studio.auth;
import lombok.RequiredArgsConstructor;
-import org.apache.rocketmq.studio.ops.ai.tool.ToolAccessPolicy;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
import org.apache.rocketmq.studio.settings.SettingsRepository;
-import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -35,20 +34,17 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@RequiredArgsConstructor
public class AuthWebConfig implements WebMvcConfigurer {
- private final ObjectProvider<AuthProperties> authPropertiesProvider;
- private final ObjectProvider<AuthService> authServiceProvider;
- private final ObjectProvider<SettingsRepository> settingsRepositoryProvider;
- private final ObjectProvider<ToolAccessPolicy> toolAccessPolicyProvider;
+ private final AuthProperties authProperties;
+ private final AuthService authService;
+ private final SettingsRepository settingsRepository;
+ private final ToolCatalog toolCatalog;
@Override
public void addInterceptors(InterceptorRegistry registry) {
- // Slice tests and minimal contexts may not provide any of these beans; when they are
- // missing the interceptor falls back to the static login-required property (effectively
- // no enforcement), matching the old conditional-registration behaviour.
- registry.addInterceptor(new AuthInterceptor(authPropertiesProvider.getIfAvailable(),
- authServiceProvider.getIfAvailable(),
- settingsRepositoryProvider.getIfAvailable(),
- toolAccessPolicyProvider.getIfAvailable()))
- .addPathPatterns("/api/**");
+ registry.addInterceptor(new AuthInterceptor(
+ authProperties, authService, settingsRepository,
+ toolCatalog))
+ .addPathPatterns("/api/**")
+ .excludePathPatterns("/api/mcp/**");
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/BrokerConfigDiffService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/BrokerConfigDiffService.java
index 771d37e56..e2587eaa4 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/BrokerConfigDiffService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/BrokerConfigDiffService.java
@@ -49,6 +49,11 @@ public class BrokerConfigDiffService {
private final ClusterService clusterService;
private final RocketMQBrokerConfigService brokerConfigService;
+ public BrokerConfigDiffVO compareForInstance(String instanceId) {
+ ClusterVO cluster = clusterService.requireSingleCluster(instanceId);
+ return compare(cluster.getId(), instanceId, collectBrokerTargets(cluster));
+ }
+
public BrokerConfigDiffVO compare(String clusterId, String instanceId) {
String normalizedClusterId = requireClusterId(clusterId);
String normalizedInstanceId = normalizeInstanceId(instanceId);
@@ -56,6 +61,10 @@ public class BrokerConfigDiffService {
? clusterService.getCluster(normalizedClusterId)
: clusterService.getCluster(normalizedClusterId, normalizedInstanceId);
List<BrokerTarget> brokers = collectBrokerTargets(cluster);
+ return compare(normalizedClusterId, normalizedInstanceId, brokers);
+ }
+
+ private BrokerConfigDiffVO compare(String normalizedClusterId, String normalizedInstanceId, List<BrokerTarget> brokers) {
if (brokers.isEmpty()) {
throw new BusinessException(409, "Cluster has no broker endpoints: " + normalizedClusterId);
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
index 141b1cef2..df2bc5b6f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
@@ -24,16 +24,24 @@ public interface ClusterProvider {
List<ClusterVO> discoverClusters();
default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
+ return instanceId == null || instanceId.isBlank() ? discoverClusters() : List.of();
}
default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
return List.of();
}
+ default List<org.apache.rocketmq.studio.cluster.proxy.ProxyVO> discoverProxies(String instanceId) {
+ throw new UnsupportedOperationException("Instance Proxy discovery is not supported");
+ }
+
+ default List<BrokerVO> discoverBrokers(String instanceId, String brokerName) {
+ throw new UnsupportedOperationException("Instance Broker discovery is not supported");
+ }
+
ClusterVO refreshClusterDetail(String clusterId);
default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+ return instanceId == null || instanceId.isBlank() ? refreshClusterDetail(clusterId) : null;
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
index 9cb0d077f..19ccafa6f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
@@ -130,6 +130,11 @@ public class ClusterService {
return List.of();
}
+ /**
+ * Verifies that a tool cluster target is confined to the authenticated Instance.
+ * A distinct cluster id must resolve through that Instance's provider and credentials;
+ * the global repository is deliberately not consulted at this security boundary.
+ */
public ClusterVO getCluster(String id) {
log.info("Getting cluster detail: {}", id);
ClusterVO live = clusterProvider.refreshClusterDetail(id);
@@ -150,6 +155,13 @@ public class ClusterService {
throw new BusinessException(503, "Cluster details are unavailable: " + id);
}
+ public List<ProxyVO> listProxiesForInstance(String instanceId) {
+ if (instanceId == null || instanceId.isBlank()) {
+ throw new BusinessException(400, "Instance is required for proxy discovery");
+ }
+ return clusterProvider.discoverProxies(instanceId);
+ }
+
public List<ProxyVO> listProxies(String clusterId) {
ClusterVO cluster = resolveCluster(clusterId);
if (cluster.getProxies() == null || cluster.getProxies().isEmpty()) {
@@ -238,9 +250,24 @@ public class ClusterService {
}
public ClusterConfigUpdateResultVO updateClusterConfig(UpdateConfigDTO command) {
+ return updateClusterConfig(command, null);
+ }
+
+ public ClusterConfigUpdateResultVO updateClusterConfig(UpdateConfigDTO command, String instanceId) {
+ if (instanceId != null) {
+ command.setInstanceId(instanceId);
+ }
+ return updateClusterConfigInternal(command, instanceId);
+ }
+
+ private ClusterConfigUpdateResultVO updateClusterConfigInternal(UpdateConfigDTO command, String instanceId) {
log.info("Updating cluster config for: {}", command.getId());
requireMatchingDefaultQueueNums(command);
- ClusterVO cluster = resolveCluster(command.getId(), command.getInstanceId());
+ ClusterVO cluster = instanceId == null ? resolveCluster(command.getId(), command.getInstanceId())
+ : requireSingleCluster(instanceId);
+ if (instanceId != null) {
+ command.setId(cluster.getId());
+ }
ClusterConfigVO config = copyConfig(cluster.getConfig());
applyConfig(command, config);
@@ -292,6 +319,46 @@ public class ClusterService {
.build();
}
+ public Map<String, ClusterConfigVO> readBrokerConfigs(String instanceId) {
+ Map<String, ClusterConfigVO> configs = new java.util.TreeMap<>();
+ ClusterVO cluster = requireSingleCluster(instanceId);
+ if (cluster.getBrokers() != null) {
+ for (BrokerVO broker : cluster.getBrokers()) {
+ configs.put(broker.getAddr(), brokerConfigService.getBrokerConfig(broker.getAddr(), instanceId));
+ }
+ }
+ return configs;
+ }
+
+ public Map<String, ClusterConfigVO> proposeBrokerConfigs(UpdateConfigDTO command, Map<String, ClusterConfigVO> current) {
+ requireMatchingDefaultQueueNums(command);
+ Map<String, ClusterConfigVO> proposed = new java.util.TreeMap<>();
+ current.forEach((address, config) -> {
+ ClusterConfigVO changed = copyConfig(config);
+ applyConfig(command, changed);
+ proposed.put(address, changed);
+ });
+ return proposed;
+ }
+
+ public ClusterVO requireSingleCluster(String instanceId) {
+ if (instanceId == null || instanceId.isBlank()) {
+ throw new BusinessException(400, "Instance is required for cluster discovery");
+ }
+ List<ClusterVO> clusters = clusterProvider.discoverClusters(instanceId);
+ if (clusters == null || clusters.isEmpty()) {
+ throw new BusinessException(503, "Cluster details are unavailable for instance: " + instanceId);
+ }
+ if (clusters.size() != 1) {
+ throw new BusinessException(409, "Instance exposes multiple Broker clusters: " + instanceId);
+ }
+ ClusterVO cluster = clusters.getFirst();
+ if (cluster.getId() == null || cluster.getId().isBlank()) {
+ throw new BusinessException(503, "Broker cluster name is unavailable for instance: " + instanceId);
+ }
+ return cluster;
+ }
+
private void applyConfig(UpdateConfigDTO command, ClusterConfigVO config) {
if (command.getFlushDiskType() != null) {
config.setFlushDiskType(parseFlushDiskType(command.getFlushDiskType()));
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RuntimeAdminClientResolver.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RuntimeAdminClientResolver.java
index f689e3acd..7876db3f8 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RuntimeAdminClientResolver.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RuntimeAdminClientResolver.java
@@ -14,7 +14,7 @@ import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.remoting.RPCHook;
import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
import org.apache.rocketmq.studio.instance.InstanceVO;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@@ -22,17 +22,34 @@ import org.springframework.util.StringUtils;
@Component
@RequiredArgsConstructor
public class RuntimeAdminClientResolver {
-
- private final InstanceRepository instanceRepository;
+ private final InstanceResolver instanceResolver;
private final MqAdminExtFactory adminFactory;
private final MqAdminProperties adminProperties;
private final MqClientPool clientPool;
+ public String configuredClusterName(String identifier) {
+ return instanceResolver.resolveClusterName(identifier);
+ }
+
+ public MqAdminProperties.Credential resolveCredential(InstanceVO instance) {
+ String credentialRef = credentialRef(instance);
+ if (!StringUtils.hasText(credentialRef)) {
+ throw new BusinessException(422, "Instance has no admin credential reference: " + instance.getName());
+ }
+ MqAdminProperties.Credential credential = adminProperties.getCredentials().get(credentialRef);
+ if (credential == null || !StringUtils.hasText(credential.getAccessKey())
+ || !StringUtils.hasText(credential.getSecretKey())) {
+ throw new BusinessException(422,
+ "Admin credential reference is not configured: " + credentialRef);
+ }
+ return credential;
+ }
+
public InstanceVO resolveInstance(String instanceId) {
if (!StringUtils.hasText(instanceId)) {
throw new BusinessException(400, "instanceId is required");
}
- return instanceRepository.findByIdentifier(instanceId)
+ return instanceResolver.findByIdentifier(instanceId)
.orElseThrow(() -> new BusinessException(404, "Instance not found: " + instanceId));
}
@@ -44,15 +61,6 @@ public class RuntimeAdminClientResolver {
return instance.getEndpoint().trim();
}
- /**
- * Resolves the ACL hook for short-lived runtime clients that cannot be created by
- * {@link MqAdminExtFactory}, such as a {@code DefaultMQPullConsumer}.
- */
- public RPCHook resolveCredentialHook(String instanceId) {
- InstanceVO instance = requireApacheInstance(resolveInstance(instanceId));
- return resolveCredential(credentialRef(instance));
- }
-
public <T> T execute(String instanceId, MqAdminExtFactory.AdminAction<T> action) {
return execute(resolveInstance(instanceId), action);
}
@@ -63,7 +71,7 @@ public class RuntimeAdminClientResolver {
throw new BusinessException(400, "Instance endpoint is required");
}
String credentialRef = credentialRef(instance);
- return adminFactory.execute(instance.getEndpoint().trim(), resolveCredential(credentialRef),
+ return adminFactory.execute(instance.getEndpoint().trim(), createCredentialHook(credentialRef),
credentialRef, action);
}
@@ -72,7 +80,7 @@ public class RuntimeAdminClientResolver {
MqClientPool.ClientAction<DefaultMQPullConsumer, T> action) {
InstanceVO instance = requireApacheInstance(resolveInstance(instanceId));
String credentialRef = credentialRef(instance);
- return clientPool.withPullConsumer(requireEndpoint(instance), resolveCredential(credentialRef),
+ return clientPool.withPullConsumer(requireEndpoint(instance), createCredentialHook(credentialRef),
credentialRef, action);
}
@@ -81,7 +89,7 @@ public class RuntimeAdminClientResolver {
MqClientPool.ClientAction<DefaultMQProducer, T> action) {
InstanceVO instance = requireApacheInstance(resolveInstance(instanceId));
String credentialRef = credentialRef(instance);
- return clientPool.withProducer(requireEndpoint(instance), resolveCredential(credentialRef),
+ return clientPool.withProducer(requireEndpoint(instance), createCredentialHook(credentialRef),
credentialRef, action);
}
@@ -97,7 +105,7 @@ public class RuntimeAdminClientResolver {
? instance.getAdminCredentialRef().trim() : null;
}
- private RPCHook resolveCredential(String credentialRef) {
+ private RPCHook createCredentialHook(String credentialRef) {
if (!StringUtils.hasText(credentialRef)) {
return null;
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientConnectionVO.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientConnectionVO.java
index 479dc2ce0..ea6140c13 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientConnectionVO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientConnectionVO.java
@@ -39,6 +39,7 @@ public class ClientConnectionVO {
private String address;
private ClientLanguage language;
private String version;
+ @com.fasterxml.jackson.annotation.JsonFormat(shape = com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING)
private LocalDateTime connectedAt;
private boolean partial;
private String clusterName;
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsService.java
index 2f6b5206f..a50e4f771 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsService.java
@@ -101,6 +101,24 @@ public class MetricsService {
return source.query(resolvedQuery);
}
+ /** Tool queries require a dedicated source explicitly bound to this Instance. */
+ public MetricDataVO queryInstance(String instanceId, MetricQueryDTO query) {
+ if (!StringUtils.hasText(instanceId)) throw badRequest("Instance is required for metrics query");
+ List<DataSourceVO> sources = settingsService.listDataSources().stream()
+ .filter(source -> source.getInstanceIds() != null && source.getInstanceIds().size() == 1
+ && instanceId.equals(source.getInstanceIds().get(0))).toList();
+ if (sources.size() != 1) throw new org.apache.rocketmq.studio.common.exception.BusinessException(409,
+ "Configure exactly one dedicated metrics data source bound only to Instance " + instanceId);
+ DataSourceVO source = sources.get(0);
+ if (!"none".equals(normalizeAuth(source.getAuth())))
+ throw new org.apache.rocketmq.studio.common.exception.BusinessException(501,
+ "Tool queries do not support data sources requiring per-request credentials");
+ MetricsDataSourceQueryRequest request = new MetricsDataSourceQueryRequest();
+ request.setInstanceId(instanceId);
+ request.setQuery(query);
+ return queryByDataSource(source.getKey(), request);
+ }
+
private void validateInstanceBinding(String dataSourceKey, DataSourceVO dataSource, String instanceId) {
List<String> bindings = dataSource.getInstanceIds();
if (bindings == null || bindings.isEmpty()) {
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
index 7179ed8ac..cdcc6ccbe 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
@@ -75,6 +75,14 @@ public class NameServerConfigDiffService {
private final MqAdminExtFactory adminFactory;
private final RuntimeAdminClientResolver runtimeAdminClientResolver;
+ public NameServerConfigDiffVO compareForInstance(String instanceId) {
+ if (instanceId == null || instanceId.isBlank()) {
+ throw new BusinessException(400, "Instance is required for NameServer config comparison");
+ }
+ String endpoint = runtimeAdminClientResolver.resolveEndpoint(instanceId);
+ return compare(instanceId, ClusterVO.builder().endpoint(endpoint).build(), instanceId);
+ }
+
public NameServerConfigDiffVO compare(String clusterId) {
String normalizedClusterId = requireClusterId(clusterId);
return compare(normalizedClusterId, clusterService.getCluster(normalizedClusterId), null);
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
index 16a0a1f2b..90eac1e27 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
@@ -168,6 +168,10 @@ public class ProxyAddressService {
synchronized (this) {
addrs = new ArrayList<>(proxyAddrs);
}
+ return buildTopology(addrs);
+ }
+
+ private List<ProxyTopologyVO> buildTopology(List<String> addrs) {
List<ProbeTask> tasks = new ArrayList<>();
for (String addr : addrs) {
Matcher matcher = PROXY_ADDR_PATTERN.matcher(addr);
@@ -302,6 +306,40 @@ public class ProxyAddressService {
String normalizedClusterId = normalizeClusterId(clusterId);
String normalized = normalizeProxyAddr(addr, "addr");
clusterService.requireProxy(normalizedClusterId, normalized);
+ reloadValidatedProxy(normalizedClusterId, normalized);
+ }
+
+ public ProxyTopologyVO previewReloadForInstance(String instanceId, String addr) {
+ ProxyVO proxy = requireInstanceProxy(instanceId, addr);
+ String normalized = normalizeProxyAddr(proxy.getAddr(), "addr");
+ Matcher matcher = PROXY_ADDR_PATTERN.matcher(normalized);
+ matcher.matches();
+ String host = matcher.group(1);
+ int grpcPort = proxy.getGrpcPort() > 0 ? proxy.getGrpcPort() : Integer.parseInt(matcher.group(2));
+ Integer remotingPort = proxy.getRemotingPort() > 0
+ ? Integer.valueOf(proxy.getRemotingPort()) : deriveRemotingPort(grpcPort);
+ ProbeTask task = new ProbeTask(normalized, grpcPort, remotingPort,
+ probeAsync(host, grpcPort), remotingPort != null ? probeAsync(host, remotingPort) : null);
+ awaitProbes(List.of(task));
+ return toTopologyVO(task);
+ }
+
+ public void reloadConfigForInstance(String instanceId, String addr) {
+ ProxyVO proxy = requireInstanceProxy(instanceId, addr);
+ reloadValidatedProxy(instanceId, normalizeProxyAddr(proxy.getAddr(), "addr"));
+ }
+
+ private ProxyVO requireInstanceProxy(String instanceId, String addr) {
+ String normalized = normalizeProxyAddr(addr, "addr");
+ List<ProxyVO> proxies = clusterService.listProxiesForInstance(instanceId);
+ return (proxies == null ? List.<ProxyVO>of() : proxies).stream()
+ .filter(proxy -> normalized.equals(proxy.getAddr()))
+ .findFirst()
+ .orElseThrow(() -> new BusinessException(404,
+ "Proxy not found in instance " + instanceId + ": " + normalized));
+ }
+
+ private void reloadValidatedProxy(String normalizedClusterId, String normalized) {
String url = "http://" + normalized + RELOAD_PATH;
try {
ResponseEntity<String> response = restTemplate.postForEntity(url, null, String.class);
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/common/config/LegacyJackson2Config.java
similarity index 53%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/common/config/LegacyJackson2Config.java
index 141b1cef2..87c1f2ad2 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/common/config/LegacyJackson2Config.java
@@ -14,26 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
+package org.apache.rocketmq.studio.common.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+public class LegacyJackson2Config {
+
+ @Bean
+ public ObjectMapper jackson2ObjectMapper() {
+ return new ObjectMapper()
+ .findAndRegisterModules()
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
- }
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
index 2fcbb55d6..dc27dbe31 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
@@ -17,11 +17,16 @@
package org.apache.rocketmq.studio.common.exception;
import lombok.Getter;
+import org.springframework.http.HttpStatus;
@Getter
public class BusinessException extends RuntimeException {
private final int code;
+ public BusinessException(HttpStatus status, String message) {
+ this(status.value(), message);
+ }
+
public BusinessException(int code, String message) {
super(message);
this.code = code;
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceResolver.java b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceResolver.java
new file mode 100644
index 000000000..d4f670d35
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceResolver.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.instance;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.provider.apache.RocketMQDefaultClusterResolver;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.Optional;
+
+@Component
+@RequiredArgsConstructor
+public class InstanceResolver {
+ private final InstanceRepository instanceRepository;
+ private final RocketMQDefaultClusterResolver defaultClusters;
+
+ public Optional<InstanceVO> findByName(String cluster) {
+ return instanceRepository.findByName(cluster).or(() -> defaultClusters.find(cluster));
+ }
+
+ public Optional<InstanceVO> findByIdentifier(String identifier) {
+ Optional<InstanceVO> registered = instanceRepository.findByIdentifier(identifier);
+ if (registered.isPresent() && (identifier.equals(registered.get().getName())
+ || instanceRepository.findByName(identifier).isPresent())) {
+ return registered;
+ }
+ return defaultClusters.find(identifier).or(() -> registered);
+ }
+
+ public String resolveClusterName(String identifier) {
+ if (!StringUtils.hasText(identifier) || instanceRepository.findByName(identifier).isPresent()) {
+ return null;
+ }
+ Optional<InstanceVO> configured = defaultClusters.find(identifier);
+ if (configured.isPresent()) {
+ return configured.get().getName();
+ }
+ if (instanceRepository.findByIdentifier(identifier).isPresent()) {
+ return null;
+ }
+ throw new BusinessException(404, "Instance not found: " + identifier);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
index 8e247cbf0..1b8fa95b4 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
@@ -25,6 +25,8 @@ public interface AclRepository {
PageResult<AclRuleVO> findRulePage(String principal, String resource, String scope,
String decision, String aclVersion, int page, int pageSize);
+ Optional<AclRuleVO> findRuleById(Long id);
+
AclRuleVO saveRule(AclRuleVO rule);
/**
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
index 97b4b8dbe..b9a9c967c 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
@@ -26,7 +26,7 @@ import org.apache.rocketmq.studio.common.util.CredentialUtils;
import org.apache.rocketmq.studio.common.util.EntityIds;
import org.apache.rocketmq.studio.audit.OperationAuditService;
import org.apache.rocketmq.studio.model.Acl2PolicyContext;
-import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
import org.apache.rocketmq.studio.instance.InstanceVO;
import org.apache.rocketmq.studio.provider.tencent.TencentAclService;
import lombok.RequiredArgsConstructor;
@@ -50,14 +50,14 @@ public class AclService {
private final AclRepository aclRepository;
private final OperationAuditService operationAuditService;
- private final InstanceRepository instanceRepository;
+ private final InstanceResolver instanceResolver;
private final TencentAclService tencentAclService;
public AclCapabilitiesVO capabilities(String instanceId) {
if (!StringUtils.hasText(instanceId)) {
throw new BusinessException(400, "instanceId is required");
}
- InstanceVO instance = instanceRepository.findByIdentifier(instanceId)
+ InstanceVO instance = instanceResolver.findByIdentifier(instanceId)
.orElseThrow(() -> new BusinessException(404, "Instance not found: " + instanceId));
if (instance.getVendor() == InstanceVendor.TENCENT) {
return new AclCapabilitiesVO(instance.getId(), instance.getVendor(), instance.getType(),
@@ -106,6 +106,32 @@ public class AclService {
return saved;
}
+ public AclRuleVO getRule(String id, String instanceId) {
+ if (!StringUtils.hasText(id)) {
+ throw new BusinessException(400, "ACL rule id is required");
+ }
+ if (isTencentInstance(instanceId)) {
+ return tencentAclService.listRules(instanceId, null).stream()
+ .filter(rule -> id.equals(rule.getPrincipal())
+ || rule.getId() != null && id.equals(rule.getId().toString()))
+ .findFirst()
+ .orElseThrow(() -> new BusinessException(404, "ACL rule not found: " + id));
+ }
+ return aclRepository.findRuleById(EntityIds.parseId(id))
+ .orElseThrow(() -> new BusinessException(404, "ACL rule not found: " + id));
+ }
+
+ public AclUserVO getUser(String id, String instanceId) {
+ if (!StringUtils.hasText(id)) {
+ throw new BusinessException(400, "ACL user id is required");
+ }
+ return listUsers(instanceId).stream()
+ .filter(user -> id.equals(user.getUsername())
+ || user.getId() != null && id.equals(user.getId().toString()))
+ .findFirst()
+ .orElseThrow(() -> new BusinessException(404, "ACL user not found: " + id));
+ }
+
public AclRuleVO updateRule(AclRuleVO rule, String instanceId) {
if (isTencentInstance(instanceId)) {
return tencentAclService.updateRule(instanceId, rule);
@@ -332,7 +358,7 @@ public class AclService {
if (!StringUtils.hasText(instanceId)) {
return false;
}
- return instanceRepository.findByIdentifier(instanceId)
+ return instanceResolver.findByIdentifier(instanceId)
.map(instance -> instance.getVendor() == InstanceVendor.TENCENT)
.orElse(false);
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
index 0b569c922..a7d7894df 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
@@ -67,6 +67,12 @@ public class MybatisPlusAclRepository implements AclRepository {
(int) mapperPage.getSize());
}
+ @Override
+ public Optional<AclRuleVO> findRuleById(Long id) {
+ return Optional.ofNullable(ruleMapper.selectById(id))
+ .map(MybatisPlusAclRepository::toRuleVO);
+ }
+
@Override
public AclRuleVO saveRule(AclRuleVO rule) {
RmqAclRule entity = toRuleEntity(rule);
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQProvider.java b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQProvider.java
index 3d36f7c60..186ddcd71 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQProvider.java
@@ -50,4 +50,4 @@ public interface DLQProvider {
*/
DLQExcelExportResultVO exportExcel(String instanceId, String groupName, Long startTime, Long endTime,
List<String> msgIds);
-}
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
index b2a552c91..50e23f040 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
@@ -20,6 +20,7 @@ import org.apache.rocketmq.studio.audit.OperationAuditConstants.Operation;
import org.apache.rocketmq.studio.audit.OperationAuditConstants.ResourceType;
import org.apache.rocketmq.studio.audit.OperationAuditConstants.Result;
import org.apache.rocketmq.studio.audit.OperationAuditService;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
import org.apache.rocketmq.studio.provider.apache.AdminClient;
import org.apache.rocketmq.studio.provider.apache.ConsumerLagResolver;
import org.apache.rocketmq.studio.provider.apache.MetadataProvider;
@@ -38,6 +39,8 @@ import org.apache.rocketmq.studio.instance.group.ConsumerGroupSettingsVO;
import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
import org.apache.rocketmq.studio.instance.group.ResetConsumerOffsetPreviewVO;
import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
+import org.apache.rocketmq.studio.instance.message.MessageRecordVO;
+import org.apache.rocketmq.studio.instance.message.MessageService;
import org.apache.rocketmq.studio.provider.InstanceProvider;
import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
@@ -48,7 +51,11 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
import java.util.function.Supplier;
@Slf4j
@@ -61,20 +68,20 @@ public class MetadataService {
private final MetadataProvider metadataProvider;
private final AdminClient adminClient;
private final InstanceProviderRegistry providerRegistry;
- private final org.apache.rocketmq.studio.instance.InstanceRepository instanceRepository;
+ private final InstanceResolver instanceResolver;
private final OperationAuditService operationAuditService;
+ private final MessageService messageService;
/**
- * External callers address instances by their globally unique instance ID (name);
- * internal storage keys rows by the numeric primary key. Resolve the identifier
- * before any DB-backed lookup; unknown identifiers pass through unchanged so the
- * downstream 404/empty semantics stay intact.
+ * Canonicalizes registered instance names and legacy numeric IDs, while preserving physical
+ * names from the configured connection. Metadata providers apply the corresponding storage
+ * scope separately; unresolved identifiers retain the downstream 404/empty semantics.
*/
String normalizeInstanceId(String instanceId) {
if (!StringUtils.hasText(instanceId)) {
return instanceId;
}
- return instanceRepository.findByIdentifier(instanceId)
+ return instanceResolver.findByIdentifier(instanceId)
.map(org.apache.rocketmq.studio.instance.InstanceVO::getName)
.orElse(instanceId);
}
@@ -93,7 +100,21 @@ public class MetadataService {
return metadataProvider.listTopics(
normalizeFilter(clusterId), normalizeFilter(type), normalizeFilter(search));
}
- return resolve(instanceId).listTopics(instanceId, normalizeFilter(type), normalizeFilter(search));
+ return resolve(instanceId).listTopics(instanceId, normalizeFilter(type), normalizeFilter(search)).stream()
+ .filter(topic -> !StringUtils.hasText(clusterId) || clusterId.equals(topic.getClusterId())).toList();
+ }
+
+ public Optional<TopicVO> findTopic(
+ String instanceId, String clusterId, String name) {
+ String topicName = requireName(name, "topic name");
+ return unique(listTopics(instanceId, clusterId, null, topicName).stream()
+ .filter(topic -> topicName.equals(topic.getName())).toList(), "Topic " + topicName);
+ }
+
+ public TopicVO getTopic(String instanceId, String clusterId, String name) {
+ String topicName = requireName(name, "topic name");
+ return findTopic(instanceId, clusterId, topicName)
+ .orElseThrow(() -> new BusinessException(404, "Topic not found: " + topicName));
}
public PageResult<TopicVO> listTopicsPage(String instanceId, String clusterId, String type,
@@ -115,40 +136,44 @@ public class MetadataService {
public TopicVO createTopic(TopicVO topic) {
+ return createTopic(topic == null ? null : topic.getInstanceId(), topic);
+ }
+
+ public TopicVO createTopic(String instanceId, TopicVO topic) {
requireTopic(topic);
if (SystemTopicFilter.isSystem(topic.getName())) {
throw new BusinessException(400, "System topics cannot be created: " + topic.getName());
}
- String instanceId = topic.getInstanceId();
+ topic.setInstanceId(instanceId);
InstanceProvider provider = resolve(instanceId);
return executeWithAudit(provider, Operation.CREATE_TOPIC, ResourceType.TOPIC, topic.getName(),
instanceId, topicDetail(topic), () -> provider.createTopic(instanceId, topic));
}
-
public TopicVO updateTopic(TopicVO topic) {
+ return updateTopic(topic == null ? null : topic.getInstanceId(), topic);
+ }
+
+ public TopicVO updateTopic(String instanceId, TopicVO topic) {
requireTopic(topic);
- String instanceId = topic.getInstanceId();
+ topic.setInstanceId(instanceId);
InstanceProvider provider = resolve(instanceId);
return executeWithAudit(provider, Operation.UPDATE_TOPIC, ResourceType.TOPIC, topic.getName(),
instanceId, topicDetail(topic), () -> provider.updateTopic(instanceId, topic));
}
-
public void deleteTopic(String name) {
deleteTopic(null, name);
}
public void deleteTopic(String instanceId, String name) {
- instanceId = normalizeInstanceId(instanceId);
+ String target = normalizeInstanceId(instanceId);
String topicName = requireName(name, "topic name");
- InstanceProvider provider = resolve(instanceId);
- String normalizedInstanceId = instanceId;
+ InstanceProvider provider = resolve(target);
executeWithAudit(provider, Operation.DELETE_TOPIC, ResourceType.TOPIC,
- topicName, instanceId, null, () -> provider.deleteTopic(normalizedInstanceId, topicName));
+ topicName, target, null, () -> provider.deleteTopic(target, topicName));
}
-
public List<BrokerRouteVO> getTopicRoutes(String name) {
return getTopicRoutes(null, name);
}
@@ -195,6 +220,43 @@ public class MetadataService {
return adminClient.sendMessage(request);
}
+ /**
+ * Re-publishes one stored message to a target topic. The original broker message is read
+ * through the instance-aware message service and only the application payload/properties are
+ * copied; broker offsets and delivery metadata are never reused.
+ */
+ public SendMessageVO resendMessage(String instanceId, String sourceTopic, String msgId, String targetTopic) {
+ MessageRecordVO original = findMessageForResend(instanceId, sourceTopic, msgId);
+ return resendMessage(instanceId, original, targetTopic);
+ }
+
+ public SendMessageVO resendMessage(String instanceId, MessageRecordVO original, String targetTopic) {
+ String destination = StringUtils.hasText(targetTopic) ? targetTopic.trim() : original.getTopic();
+ if (!StringUtils.hasText(destination)) {
+ throw new BusinessException(400, "target topic is required when the source message has no topic");
+ }
+ SendMessageDTO request = SendMessageDTO.builder()
+ .instanceId(normalizeInstanceId(instanceId))
+ .topic(destination)
+ .tag(original.getTag())
+ .key(original.getKey())
+ .body(original.getBody())
+ .properties(original.getProperties() == null ? null : Map.copyOf(original.getProperties()))
+ .build();
+ return sendMessage(request);
+ }
+
+ /** Loads the exact source message used by resend without exposing or publishing its body. */
+ public MessageRecordVO findMessageForResend(String instanceId, String sourceTopic, String msgId) {
+ String messageId = requireName(msgId, "message id");
+ List<MessageRecordVO> matches = messageService.queryMessages(
+ instanceId, normalizeFilter(sourceTopic), messageId, null, null, null, null);
+ return matches.stream()
+ .filter(message -> messageId.equals(message.getMsgId()))
+ .findFirst()
+ .orElseThrow(() -> new BusinessException(404, "Message not found: " + messageId));
+ }
+
// ── ConsumerGroupVO ───────────────────────────────────────────────
@@ -207,7 +269,8 @@ public class MetadataService {
if (!StringUtils.hasText(instanceId) && StringUtils.hasText(clusterId)) {
return metadataProvider.listConsumerGroups(normalizeFilter(clusterId), normalizeFilter(search));
}
- return resolve(instanceId).listConsumerGroups(instanceId, normalizeFilter(search));
+ return resolve(instanceId).listConsumerGroups(instanceId, normalizeFilter(search)).stream()
+ .filter(group -> !StringUtils.hasText(clusterId) || clusterId.equals(group.getClusterId())).toList();
}
public PageResult<ConsumerGroupVO> listConsumerGroupsPage(String instanceId, String clusterId, String search,
@@ -247,13 +310,41 @@ public class MetadataService {
* frontend keeps the existing row unchanged.
*/
public ConsumerGroupVO refreshConsumerGroup(String instanceId, String name) {
- instanceId = normalizeInstanceId(instanceId);
+ return findConsumerGroup(instanceId, name).orElse(null);
+ }
+
+ public Optional<ConsumerGroupVO> findConsumerGroup(String instanceId, String name) {
+ String normalizedInstanceId = normalizeInstanceId(instanceId);
String groupName = requireName(name, "consumer group name");
- return listConsumerGroups(instanceId, null, normalizeFilter(groupName))
- .stream()
- .filter(group -> groupName.equals(group.getName()))
- .findFirst()
- .orElse(null);
+ return unique(listConsumerGroups(normalizedInstanceId, null, groupName).stream()
+ .filter(group -> groupName.equals(group.getName())).toList(), "Consumer group " + groupName);
+ }
+
+ public List<ConsumerGroupVO> consumerGroupConfigurations(String instanceId, String name) {
+ List<ConsumerGroupVO> groups = listConsumerGroups(instanceId, null, name).stream()
+ .filter(group -> name.equals(group.getName())).toList();
+ if (groups.isEmpty()) throw new BusinessException(404, "Consumer group not found: " + name);
+ return groups;
+ }
+
+ /** Runtime statistics cover the Instance-wide group; repeated physical configurations are not summed. */
+ public ConsumerGroupVO consumerGroupRuntimeView(String instanceId, String name) {
+ List<ConsumerGroupVO> groups = consumerGroupConfigurations(instanceId, name);
+ if (groups.size() == 1) return groups.get(0);
+ ConsumerGroupVO runtime = getConsumerGroup(instanceId, name);
+ runtime.setInstanceId(normalizeInstanceId(instanceId));
+ return runtime;
+ }
+
+ public ConsumerGroupVO requireConsumerGroup(String instanceId, String name) {
+ return findConsumerGroup(instanceId, name)
+ .orElseThrow(() -> new BusinessException(404, "Consumer group not found: " + name));
+ }
+
+ private <T> Optional<T> unique(List<T> matches, String resource) {
+ if (matches.size() > 1) throw new BusinessException(409,
+ resource + " exists in multiple Broker clusters; a physical target is required for configuration changes");
+ return matches.stream().findFirst();
}
@@ -280,11 +371,24 @@ public class MetadataService {
public ConsumerGroupVO createConsumerGroup(ConsumerGroupVO group) {
- String instanceId = group == null ? null : group.getInstanceId();
+ return saveConsumerGroup(group == null ? null : group.getInstanceId(), group, Operation.CREATE_GROUP,
+ (provider, instanceId) -> provider.createConsumerGroup(instanceId, group));
+ }
+
+ public ConsumerGroupVO updateConsumerGroup(ConsumerGroupVO group) {
+ return saveConsumerGroup(group == null ? null : group.getInstanceId(), group, Operation.UPDATE_GROUP,
+ (provider, instanceId) -> provider.updateConsumerGroup(instanceId, group));
+ }
+
+ private ConsumerGroupVO saveConsumerGroup(String instanceId, ConsumerGroupVO group, String operation,
+ BiFunction<InstanceProvider, String, ConsumerGroupVO> mutation) {
+ if (group != null) {
+ group.setInstanceId(instanceId);
+ }
InstanceProvider provider = resolve(instanceId);
- return executeWithAudit(provider, Operation.CREATE_GROUP, ResourceType.GROUP,
+ return executeWithAudit(provider, operation, ResourceType.GROUP,
group == null ? null : group.getName(), instanceId, consumerGroupDetail(group),
- () -> provider.createConsumerGroup(instanceId, group));
+ () -> mutation.apply(provider, instanceId));
}
public ConsumerGroupSettingsVO getConsumerGroupSettings(String instanceId, String name) {
@@ -307,15 +411,17 @@ public class MetadataService {
}
public void deleteConsumerGroup(String instanceId, String name) {
- instanceId = normalizeInstanceId(instanceId);
+ String target = normalizeInstanceId(instanceId);
+ deleteConsumerGroup(target, name, (provider, groupName) -> provider.deleteConsumerGroup(target, groupName));
+ }
+
+ private void deleteConsumerGroup(String instanceId, String name, BiConsumer<InstanceProvider, String> mutation) {
String groupName = requireName(name, "consumer group name");
InstanceProvider provider = resolve(instanceId);
- String normalizedInstanceId = instanceId;
executeWithAudit(provider, Operation.DELETE_GROUP, ResourceType.GROUP,
- groupName, instanceId, null, () -> provider.deleteConsumerGroup(normalizedInstanceId, groupName));
+ groupName, instanceId, null, () -> mutation.accept(provider, groupName));
}
-
public void resetOffset(String name, long timestamp, String topic) {
resetOffset(null, name, timestamp, topic);
}
@@ -339,6 +445,35 @@ public class MetadataService {
() -> provider.resetOffset(normalizedInstanceId, groupName, timestamp, topicName));
}
+ /**
+ * Advances a consumer group's offsets to the current time. When topic is omitted every
+ * currently registered subscription is advanced independently.
+ */
+ public List<String> skipAccumulated(String instanceId, String name, String topic) {
+ String groupName = requireName(name, "consumer group name");
+ List<String> topics = resolveSkipAccumulatedTopics(instanceId, groupName, topic);
+ long timestamp = System.currentTimeMillis();
+ topics.forEach(subscriptionTopic -> resetOffset(instanceId, groupName, timestamp, subscriptionTopic));
+ return topics;
+ }
+
+ /** Resolves the exact topic scope used by skip-accumulated without changing offsets. */
+ public List<String> resolveSkipAccumulatedTopics(String instanceId, String name, String topic) {
+ String groupName = requireName(name, "consumer group name");
+ List<String> topics = StringUtils.hasText(topic)
+ ? List.of(topic.trim())
+ : getGroupSubscriptions(instanceId, groupName).stream()
+ .map(SubscriptionEntryVO::getTopic)
+ .filter(StringUtils::hasText)
+ .map(String::trim)
+ .distinct()
+ .toList();
+ if (topics.isEmpty()) {
+ throw new BusinessException(404, "No subscribed topics found for consumer group: " + groupName);
+ }
+ return topics;
+ }
+
/**
* Every metadata operation goes through the provider registry; a blank instance id keeps the
@@ -600,25 +735,18 @@ public class MetadataService {
private void executeWithAudit(InstanceProvider provider, String operation, String resourceType,
String resourceName, String instanceId, String detail, Runnable action) {
- if (provider.vendor() == InstanceVendor.APACHE) {
- action.run();
- return;
- }
- try {
+ executeWithAudit(provider, operation, resourceType, resourceName, instanceId, detail, () -> {
action.run();
- recordAudit(operation, resourceType, resourceName, instanceId, detail, Result.SUCCESS, null);
- } catch (RuntimeException failure) {
- recordAudit(operation, resourceType, resourceName, instanceId, detail, Result.FAILED,
- failure.getMessage());
- throw failure;
- }
+ return null;
+ });
}
private void recordAudit(String operation, String resourceType, String resourceName, String instanceId,
String detail, String result, String errorMessage) {
try {
- operationAuditService.record(operation, resourceType, resourceName, instanceId,
- detail, result, errorMessage);
+ var instance = instanceResolver.findByIdentifier(instanceId).orElse(null);
+ operationAuditService.record(operation, resourceType, resourceName,
+ instance == null ? instanceId : instance.getName(), detail, result, errorMessage);
} catch (Exception auditFailure) {
log.warn("Failed to record audit operation={} resource={}: {}", operation, resourceName,
auditFailure.getMessage());
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
index 4c675aac9..a2b9627bc 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
@@ -16,23 +16,15 @@
*/
package org.apache.rocketmq.studio.ops.ai;
-import org.apache.rocketmq.studio.common.domain.Result;
import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.common.domain.Result;
import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
@@ -49,27 +41,4 @@ public class AiController {
public Result<AiExecuteResultVO> execute(@RequestBody AiCommandDTO command) {
return Result.ok(aiService.execute(command));
}
-
- @GetMapping("/tools")
- public ResponseEntity<Result<List<AiToolVO>>> listTools(
- @RequestParam(required = false) String cluster) {
- List<AiToolVO> tools = cluster == null
- ? aiService.listTools()
- : aiService.listTools(cluster);
- return ResponseEntity.ok()
- .header("X-RMQ-Catalog-Version", aiService.catalogVersion())
- .header("X-RMQ-Catalog-Digest", aiService.catalogDigest())
- .header("X-RMQ-Minimum-Client-Version", aiService.minimumClientVersion())
- .body(Result.ok(tools));
- }
-
- @PostMapping("/tools/{name}/execute")
- public Result<Object> executeTool(
- @PathVariable String name,
- @RequestBody(required = false) Map<String, Object> input) {
- Map<String, Object> normalizedInput = input == null
- ? Collections.emptyMap()
- : input;
- return Result.ok(aiService.executeTool(name, normalizedInput));
- }
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiService.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiService.java
index 62ede2ee0..c30d27a9f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiService.java
@@ -23,16 +23,12 @@ import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-import java.util.List;
-import java.util.Map;
-
@Slf4j
@Service
@RequiredArgsConstructor
public class AiService {
private final LlmGateway llmGateway;
- private final McpServerRegistry mcpServerRegistry;
private final ObjectMapper objectMapper;
@@ -74,33 +70,4 @@ public class AiService {
.build();
}
}
-
-
- public List<AiToolVO> listTools() {
- log.debug("Listing available AI tools");
- return mcpServerRegistry.listTools();
- }
-
- public List<AiToolVO> listTools(String clusterId) {
- log.debug("Listing available AI tools for cluster: {}", clusterId);
- return mcpServerRegistry.listTools(clusterId);
- }
-
- public Object executeTool(String name, Map<String, Object> input) {
- AiPayloadGuard.validateToolInvocation(name, input, objectMapper);
- log.info("Executing registered AI tool: {}", name);
- return mcpServerRegistry.execute(name, input);
- }
-
- public String catalogVersion() {
- return mcpServerRegistry.catalogVersion();
- }
-
- public String catalogDigest() {
- return mcpServerRegistry.catalogDigest();
- }
-
- public String minimumClientVersion() {
- return mcpServerRegistry.minimumClientVersion();
- }
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolCallDTO.java
similarity index 75%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolCallDTO.java
index 770602ddd..486ce7709 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolCallDTO.java
@@ -21,21 +21,13 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
-import java.util.List;
+import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
-public class AiToolVO {
+public class AiToolCallDTO {
private String name;
- private String description;
- private Object parameters;
- private String riskLevel;
- private String permission;
- private List<String> requiredCapabilities;
- private Object outputSchema;
- private String viewHint;
- private boolean deprecated;
- private String replacement;
+ private Map<String, Object> arguments;
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
index 770602ddd..75c9d43ba 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
@@ -29,13 +29,17 @@ import java.util.List;
@AllArgsConstructor
public class AiToolVO {
private String name;
+ private String version;
+ private Object cli;
private String description;
private Object parameters;
private String riskLevel;
+ private String operationLevel;
private String permission;
private List<String> requiredCapabilities;
private Object outputSchema;
private String viewHint;
private boolean deprecated;
private String replacement;
+ private boolean implemented;
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpToolController.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpToolController.java
new file mode 100644
index 000000000..9ff75f9d6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpToolController.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.common.domain.Result;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthentication;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolExecutionService;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+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;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/mcp/tools")
+@RequiredArgsConstructor
+@ConditionalOnProperty(name = "spring.ai.mcp.server.enabled", havingValue = "true")
+public class McpToolController {
+
+ private final ToolExecutionService toolExecutor;
+ private final ObjectMapper objectMapper;
+
+ @PostMapping("/call")
+ public Result<Object> callTool(
+ @RequestBody(required = false) AiToolCallDTO call,
+ HttpServletRequest request) {
+ McpAuthentication authentication = authentication(request);
+ if (call == null) {
+ throw ToolError.TOOL_CALL_REQUIRED.exception();
+ }
+ AiPayloadGuard.validateToolInvocation(call.getName(), call.getArguments(), objectMapper);
+ log.info("Calling registered AI tool: {}", call.getName());
+ return Result.ok(toolExecutor.execute(
+ call.getName(), call.getArguments(), authentication));
+ }
+
+ private static McpAuthentication authentication(HttpServletRequest request) {
+ Object authentication = request.getAttribute(McpAuthentication.ATTRIBUTE);
+ if (authentication instanceof McpAuthentication mcpAuthentication) {
+ return mcpAuthentication;
+ }
+ throw ToolError.MCP_AUTHENTICATION_REQUIRED.exception();
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java
similarity index 57%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java
index 4c675aac9..9970b49ce 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java
@@ -16,10 +16,12 @@
*/
package org.apache.rocketmq.studio.ops.ai;
-import org.apache.rocketmq.studio.common.domain.Result;
+import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.common.domain.Result;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolDiscoveryService;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolExecutionService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -27,49 +29,37 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+@Slf4j
@RestController
-@RequestMapping("/api/ai")
+@RequestMapping("/api/ai/tools")
@RequiredArgsConstructor
-public class AiController {
+public class ToolController {
- private final AiService aiService;
-
- @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
- public SseEmitter chat(@RequestBody ChatDTO request) {
- return aiService.chat(request);
- }
+ private final ToolDiscoveryService toolDiscoveryService;
+ private final ToolExecutionService toolExecutor;
+ private final ObjectMapper objectMapper;
- @PostMapping("/execute")
- public Result<AiExecuteResultVO> execute(@RequestBody AiCommandDTO command) {
- return Result.ok(aiService.execute(command));
+ @GetMapping
+ public Result<List<AiToolVO>> listTools(
+ @RequestParam String cluster) {
+ return Result.ok(toolDiscoveryService.listTools(cluster));
}
- @GetMapping("/tools")
- public ResponseEntity<Result<List<AiToolVO>>> listTools(
- @RequestParam(required = false) String cluster) {
- List<AiToolVO> tools = cluster == null
- ? aiService.listTools()
- : aiService.listTools(cluster);
- return ResponseEntity.ok()
- .header("X-RMQ-Catalog-Version", aiService.catalogVersion())
- .header("X-RMQ-Catalog-Digest", aiService.catalogDigest())
- .header("X-RMQ-Minimum-Client-Version", aiService.minimumClientVersion())
- .body(Result.ok(tools));
- }
-
- @PostMapping("/tools/{name}/execute")
+ @PostMapping("/{name}/execute")
public Result<Object> executeTool(
@PathVariable String name,
@RequestBody(required = false) Map<String, Object> input) {
Map<String, Object> normalizedInput = input == null
? Collections.emptyMap()
: input;
- return Result.ok(aiService.executeTool(name, normalizedInput));
+ AiPayloadGuard.validateToolInvocation(name, normalizedInput, objectMapper);
+ log.info("Executing registered AI tool: {}", name);
+ return Result.ok(toolExecutor.execute(name, normalizedInput));
}
+
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolExceptionHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolExceptionHandler.java
new file mode 100644
index 000000000..45cca73db
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolExceptionHandler.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionException;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+
+import java.util.Map;
+
+@Slf4j
+@Order(Ordered.HIGHEST_PRECEDENCE)
+@RestControllerAdvice(assignableTypes = {ToolController.class, McpToolController.class})
+public class ToolExceptionHandler {
+
+ @ExceptionHandler(ToolExecutionException.class)
+ public ResponseEntity<Map<String, String>> handleToolExecutionException(
+ ToolExecutionException exception) {
+ if (exception.getCode() >= HttpStatus.INTERNAL_SERVER_ERROR.value()) {
+ log.error("Tool execution failed: code={}, message={}",
+ exception.getErrorCode(), exception.getMessage(), exception);
+ } else {
+ log.warn("Tool execution failed: code={}, message={}",
+ exception.getErrorCode(), exception.getMessage());
+ }
+ return error(exception);
+ }
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ public ResponseEntity<Map<String, String>> handleUnreadableBody() {
+ return error(ToolError.REQUEST_BODY_INVALID.exception());
+ }
+
+ @ExceptionHandler(MissingServletRequestParameterException.class)
+ public ResponseEntity<Map<String, String>> handleMissingParameter(
+ MissingServletRequestParameterException exception) {
+ return error(ToolError.REQUEST_PARAMETER_REQUIRED.exception(exception.getParameterName()));
+ }
+
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public ResponseEntity<Map<String, String>> handleInvalidParameter(
+ MethodArgumentTypeMismatchException exception) {
+ return error(ToolError.REQUEST_PARAMETER_INVALID.exception(exception.getName()));
+ }
+
+ private static ResponseEntity<Map<String, String>> error(ToolExecutionException exception) {
+ return ResponseEntity.status(exception.getCode()).body(Map.of(
+ "code", exception.getErrorCode(),
+ "message", exception.getMessage(),
+ "hint", exception.getHint()));
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
similarity index 60%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
index 141b1cef2..b5911beb6 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
@@ -14,26 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.auth;
+public record McpAuthentication(
+ String cluster,
+ String principal) {
-import java.util.List;
+ public static final String ATTRIBUTE = McpAuthentication.class.getName();
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
+ public McpAuthentication {
+ requireText(cluster, "cluster");
+ requireText(principal, "principal");
}
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+ private static void requireText(String value, String field) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(field + " must not be blank");
+ }
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationException.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationException.java
index 2fcbb55d6..bea10eb8a 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationException.java
@@ -14,16 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.auth;
-import lombok.Getter;
+public class McpAuthenticationException extends RuntimeException {
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
+ public McpAuthenticationException(String message) {
super(message);
- this.code = code;
+ }
+
+ public McpAuthenticationException(String message, Throwable cause) {
+ super(message, cause);
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationFilter.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationFilter.java
new file mode 100644
index 000000000..c019d2d96
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationFilter.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.auth;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Map;
+
+@RequiredArgsConstructor
+@Slf4j
+public class McpAuthenticationFilter extends OncePerRequestFilter {
+
+ private final McpAuthenticator authenticator;
+ private final ObjectMapper objectMapper;
+
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+ final McpAuthentication authentication;
+ try {
+ authentication = authenticator.authenticate(request);
+ } catch (McpAuthenticationException exception) {
+ log.warn("MCP authentication failed: path={}, remoteAddress={}, status={}, message={}",
+ request.getRequestURI(), request.getRemoteAddr(),
+ HttpStatus.UNAUTHORIZED.value(), exception.getMessage());
+ writeError(response, ToolError.MCP_AUTHENTICATION_FAILED, exception.getMessage());
+ return;
+ } catch (RuntimeException exception) {
+ log.error("MCP authentication failed unexpectedly: path={}, remoteAddress={}",
+ request.getRequestURI(), request.getRemoteAddr(), exception);
+ writeError(response, ToolError.MCP_AUTHENTICATION_INTERNAL_ERROR);
+ return;
+ }
+ request.setAttribute(McpAuthentication.ATTRIBUTE, authentication);
+ try {
+ AuthenticatedUserContext.setUser(null, authentication.principal(), false);
+ filterChain.doFilter(request, response);
+ } finally {
+ AuthenticatedUserContext.clear();
+ }
+ }
+
+ private void writeError(HttpServletResponse response, ToolError error, Object... arguments)
+ throws IOException {
+ response.setStatus(error.httpStatus().value());
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ if (error.httpStatus() == HttpStatus.UNAUTHORIZED) {
+ response.setHeader(HttpHeaders.WWW_AUTHENTICATE, McpAuthenticator.ALGORITHM);
+ }
+ objectMapper.writeValue(response.getOutputStream(), Map.of(
+ "code", error.code(),
+ "message", error.message(arguments),
+ "hint", error.hint()));
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticator.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticator.java
new file mode 100644
index 000000000..5d65ccb62
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticator.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.auth;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.rocketmq.studio.cluster.broker.MqAdminProperties;
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
+import org.apache.rocketmq.studio.instance.InstanceVO;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialVO;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriUtils;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.HexFormat;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class McpAuthenticator {
+
+ static final String ALGORITHM = "RMQ-HMAC-SHA256";
+ static final String HEADER_CLUSTER = "X-RMQ-Cluster";
+ static final String HEADER_TIMESTAMP = "X-RMQ-Timestamp";
+ static final String AUTHENTICATION_FAILED_MESSAGE = "MCP authentication failed.";
+ static final Duration MAX_CLOCK_SKEW = Duration.ofMinutes(5);
+ private static final Pattern AUTHORIZATION = Pattern.compile(
+ "^" + Pattern.quote(ALGORITHM)
+ + " Credential=([^,]+), Signature=([0-9a-f]{64})$");
+ private final InstanceResolver instanceResolver;
+ private final RuntimeAdminClientResolver adminClientResolver;
+ private final CloudCredentialRepository cloudCredentialRepository;
+ private final Clock clock;
+
+ public McpAuthenticator(
+ InstanceResolver instanceResolver,
+ RuntimeAdminClientResolver adminClientResolver,
+ CloudCredentialRepository cloudCredentialRepository) {
+ this(instanceResolver, adminClientResolver, cloudCredentialRepository, Clock.systemUTC());
+ }
+
+ McpAuthenticator(
+ InstanceResolver instanceResolver,
+ RuntimeAdminClientResolver adminClientResolver,
+ CloudCredentialRepository cloudCredentialRepository,
+ Clock clock) {
+ this.instanceResolver = instanceResolver;
+ this.adminClientResolver = adminClientResolver;
+ this.cloudCredentialRepository = cloudCredentialRepository;
+ this.clock = clock;
+ }
+
+ public McpAuthentication authenticate(HttpServletRequest request) {
+ Authorization authorization = authorization(request.getHeader(HttpHeaders.AUTHORIZATION));
+ String cluster = requiredHeader(request, HEADER_CLUSTER);
+ String timestamp = requiredHeader(request, HEADER_TIMESTAMP);
+ verifyTimestamp(timestamp);
+ InstanceVO instance = resolveInstance(cluster);
+ Credential credential = resolveCredential(instance);
+ verifySignature(request, authorization, credential, cluster, timestamp);
+ return new McpAuthentication(cluster, authorization.accessKey());
+ }
+
+ private Authorization authorization(String value) {
+ if (value == null || value.isBlank()) {
+ throw new McpAuthenticationException("MCP authentication is required.");
+ }
+ Matcher matcher = AUTHORIZATION.matcher(value);
+ if (!matcher.matches()) {
+ throw authenticationFailed();
+ }
+ final String accessKey;
+ try {
+ accessKey = UriUtils.decode(matcher.group(1), StandardCharsets.UTF_8);
+ } catch (IllegalArgumentException exception) {
+ throw authenticationFailed();
+ }
+ if (accessKey.isBlank() || !accessKey.equals(accessKey.trim())) {
+ throw authenticationFailed();
+ }
+ return new Authorization(accessKey, HexFormat.of().parseHex(matcher.group(2)));
+ }
+
+ private String requiredHeader(HttpServletRequest request, String name) {
+ String value = request.getHeader(name);
+ if (value == null) {
+ throw authenticationFailed();
+ }
+ if (value.isBlank() || !value.equals(value.trim())) {
+ throw authenticationFailed();
+ }
+ return value;
+ }
+
+ private InstanceVO resolveInstance(String cluster) {
+ try {
+ return instanceResolver.findByName(cluster).orElseThrow(McpAuthenticator::authenticationFailed);
+ } catch (BusinessException exception) {
+ if (exception.getCode() == 422) {
+ throw authenticationFailed(exception);
+ }
+ throw exception;
+ }
+ }
+
+ private Credential resolveCredential(InstanceVO instance) {
+ if (instance.getVendor() == null || instance.getVendor() == InstanceVendor.APACHE) {
+ try {
+ MqAdminProperties.Credential credential =
+ adminClientResolver.resolveCredential(instance);
+ return new Credential(credential.getAccessKey().trim(), credential.getSecretKey().trim());
+ } catch (BusinessException exception) {
+ throw authenticationFailed(exception);
+ }
+ }
+ if (instance.getCredentialId() == null) {
+ throw authenticationFailed();
+ }
+ CloudCredentialVO credential = cloudCredentialRepository.findById(instance.getCredentialId())
+ .orElseThrow(McpAuthenticator::authenticationFailed);
+
+ if (!StringUtils.hasText(credential.getAccessKey())
+ || !StringUtils.hasText(credential.getSecretKey())) {
+ throw authenticationFailed();
+ }
+ return new Credential(credential.getAccessKey().trim(), credential.getSecretKey().trim());
+ }
+
+ private static void verifySignature(HttpServletRequest request, Authorization authorization, Credential credential,
+ String cluster, String timestamp) {
+ if (!authorization.accessKey().equals(credential.accessKey())) {
+ throw authenticationFailed();
+ }
+ byte[] expected = hmac(credential.secretKey(),
+ canonicalRequest(authorization.accessKey(), cluster, timestamp,
+ request.getMethod(), requestTarget(request)));
+ if (!MessageDigest.isEqual(expected, authorization.signature())) {
+ throw authenticationFailed();
+ }
+ }
+
+ private void verifyTimestamp(String timestamp) {
+ final long requestTime;
+ try {
+ requestTime = Long.parseLong(timestamp);
+ } catch (NumberFormatException exception) {
+ throw authenticationFailed(exception);
+ }
+ long now = clock.millis();
+ long allowedSkew = MAX_CLOCK_SKEW.toMillis();
+ if (requestTime < now - allowedSkew || requestTime > now + allowedSkew) {
+ throw authenticationFailed();
+ }
+ }
+
+ private static byte[] hmac(String secretKey, String canonical) {
+ try {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
+ return mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8));
+ } catch (GeneralSecurityException exception) {
+ throw new IllegalStateException("HmacSHA256 is unavailable", exception);
+ }
+ }
+
+ private static String requestTarget(HttpServletRequest request) {
+ String query = request.getQueryString();
+ return request.getRequestURI() + (query == null ? "" : "?" + query);
+ }
+
+ static String canonicalRequest(
+ String accessKey,
+ String cluster,
+ String timestamp,
+ String method,
+ String requestTarget) {
+ return String.join("\n",
+ ALGORITHM,
+ accessKey,
+ cluster,
+ timestamp,
+ method,
+ requestTarget);
+ }
+
+ private static McpAuthenticationException authenticationFailed() {
+ return new McpAuthenticationException(AUTHENTICATION_FAILED_MESSAGE);
+ }
+
+ private static McpAuthenticationException authenticationFailed(Throwable cause) {
+ return new McpAuthenticationException(AUTHENTICATION_FAILED_MESSAGE, cause);
+ }
+
+ private record Authorization(String accessKey, byte[] signature) {
+ }
+
+ private record Credential(String accessKey, String secretKey) {
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpConfiguration.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpConfiguration.java
new file mode 100644
index 000000000..461a5743e
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpConfiguration.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.mcp;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.server.McpServerFeatures;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthenticationFilter;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthenticator;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthentication;
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolExecutionService;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
+import org.springframework.ai.mcp.server.webmvc.transport.WebMvcStreamableServerTransportProvider;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.function.ServerRequest;
+import tools.jackson.databind.json.JsonMapper;
+
+import java.util.List;
+import java.util.Map;
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnProperty(name = "spring.ai.mcp.server.enabled", havingValue = "true")
+public class McpConfiguration {
+
+ private static final String TOOL_CALL_ENDPOINT = "/api/mcp/tools/call";
+
+ @Bean
+ public McpAuthenticator mcpAuthenticator(
+ InstanceResolver instanceResolver,
+ RuntimeAdminClientResolver adminClientResolver,
+ CloudCredentialRepository cloudCredentialRepository) {
+ return new McpAuthenticator(instanceResolver, adminClientResolver, cloudCredentialRepository);
+ }
+
+ @Bean
+ public FilterRegistrationBean<McpAuthenticationFilter> mcpAuthenticationFilterRegistration(
+ McpAuthenticator authenticator,
+ ObjectMapper objectMapper,
+ McpServerStreamableHttpProperties serverProperties) {
+ FilterRegistrationBean<McpAuthenticationFilter> registration =
+ new FilterRegistrationBean<>(new McpAuthenticationFilter(authenticator, objectMapper));
+ registration.addUrlPatterns(
+ serverProperties.getMcpEndpoint(),
+ TOOL_CALL_ENDPOINT);
+ return registration;
+ }
+
+ @Bean
+ public WebMvcStreamableServerTransportProvider webMvcStreamableServerTransportProvider(
+ @Qualifier("mcpServerJsonMapper") JsonMapper jsonMapper,
+ McpServerStreamableHttpProperties serverProperties) {
+ return WebMvcStreamableServerTransportProvider.builder()
+ .jsonMapper(new JacksonMcpJsonMapper(jsonMapper))
+ .mcpEndpoint(serverProperties.getMcpEndpoint())
+ .keepAliveInterval(serverProperties.getKeepAliveInterval())
+ .disallowDelete(serverProperties.isDisallowDelete())
+ .contextExtractor(McpConfiguration::extractContext)
+ .build();
+ }
+
+ @Bean
+ public List<McpServerFeatures.SyncToolSpecification> toolSpecifications(
+ ToolCatalog toolCatalog,
+ ToolExecutionService toolExecutor,
+ ObjectMapper objectMapper) {
+ return McpToolRegistrar.toToolSpecifications(toolCatalog, toolExecutor, objectMapper);
+ }
+
+ static McpTransportContext extractContext(ServerRequest request) {
+ Object authentication = request.servletRequest()
+ .getAttribute(McpAuthentication.ATTRIBUTE);
+ if (!(authentication instanceof McpAuthentication mcpAuthentication)) {
+ return McpTransportContext.EMPTY;
+ }
+ return McpTransportContext.create(Map.of(
+ McpAuthentication.ATTRIBUTE, mcpAuthentication));
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrar.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrar.java
new file mode 100644
index 000000000..cd5074ab1
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrar.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.mcp;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthentication;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionException;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolRiskLevel;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolExecutionService;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+public class McpToolRegistrar {
+
+ private McpToolRegistrar() {
+ }
+
+ public static List<McpServerFeatures.SyncToolSpecification> toToolSpecifications(
+ ToolCatalog toolCatalog,
+ ToolExecutionService toolExecutor,
+ ObjectMapper objectMapper) {
+ return toolCatalog.list().stream()
+ .map(definition -> toolSpecification(definition, toolExecutor, objectMapper))
+ .toList();
+ }
+
+ public static McpServerFeatures.SyncToolSpecification toolSpecification(
+ ToolDefinition definition,
+ ToolExecutionService toolExecutor,
+ ObjectMapper objectMapper) {
+ BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> handler =
+ (exchange, request) -> callTool(
+ definition, exchange, request, toolExecutor, objectMapper);
+ return McpServerFeatures.SyncToolSpecification.builder()
+ .tool(tool(definition))
+ .callHandler(handler)
+ .build();
+ }
+
+ public static McpSchema.CallToolResult callTool(
+ ToolDefinition definition,
+ McpSyncServerExchange exchange,
+ McpSchema.CallToolRequest request,
+ ToolExecutionService toolExecutionService,
+ ObjectMapper objectMapper) {
+ McpAuthentication authentication = authentication(exchange);
+ if (authentication == null) {
+ return errorResult(ToolError.MCP_AUTHENTICATION_CONTEXT_MISSING.exception(), objectMapper);
+ }
+ try {
+ Object result = toolExecutionService.execute(
+ definition.name(), request.arguments(), authentication);
+ return McpSchema.CallToolResult.builder()
+ .content(List.of(McpSchema.TextContent.builder(serialize(result, objectMapper)).build()))
+ .structuredContent(result)
+ .isError(false)
+ .build();
+ } catch (ToolExecutionException exception) {
+ return errorResult(exception, objectMapper);
+ }
+ }
+
+ public static McpSchema.Tool tool(ToolDefinition definition) {
+ String description = definition.description();
+ if (!definition.requiredCapabilities().isEmpty()) {
+ description += "\nRequires all capabilities: "
+ + String.join(", ", definition.requiredCapabilities()) + ".";
+ }
+ return McpSchema.Tool.builder(definition.name(), definition.inputSchema())
+ .description(description)
+ .outputSchema(definition.outputSchema())
+ .annotations(toolAnnotations(definition))
+ .build();
+ }
+
+ public static McpSchema.ToolAnnotations toolAnnotations(ToolDefinition definition) {
+ boolean readOnly = definition.isReadOnly();
+ return McpSchema.ToolAnnotations.builder()
+ .title(definition.name())
+ .readOnlyHint(readOnly)
+ .destructiveHint(definition.riskLevel() == ToolRiskLevel.L3)
+ .idempotentHint(false)
+ .openWorldHint(true)
+ .build();
+ }
+
+ private static McpAuthentication authentication(McpSyncServerExchange exchange) {
+ McpTransportContext context = exchange == null ? null : exchange.transportContext();
+ Object authentication = context == null
+ ? null
+ : context.get(McpAuthentication.ATTRIBUTE);
+ return authentication instanceof McpAuthentication mcpAuthentication ? mcpAuthentication : null;
+ }
+
+ private static McpSchema.CallToolResult errorResult(
+ ToolExecutionException exception,
+ ObjectMapper objectMapper) {
+ Map<String, Object> error = Map.of(
+ "code", exception.getErrorCode(),
+ "message", exception.getMessage(),
+ "hint", exception.getHint());
+ return McpSchema.CallToolResult.builder()
+ .content(List.of(McpSchema.TextContent.builder(serialize(error, objectMapper)).build()))
+ .structuredContent(error)
+ .isError(true)
+ .build();
+ }
+
+ private static String serialize(Object value, ObjectMapper objectMapper) {
+ try {
+ return objectMapper.writeValueAsString(value);
+ } catch (JsonProcessingException e) {
+ throw new IllegalStateException("Unable to serialize MCP tool result", e);
+ }
+ }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/AlertRuleListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/AlertRuleListToolHandler.java
deleted file mode 100644
index b001b5c51..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/AlertRuleListToolHandler.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import lombok.RequiredArgsConstructor;
-import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
-import org.apache.rocketmq.studio.ops.alert.AlertService;
-import org.apache.rocketmq.studio.common.domain.PageResult;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class AlertRuleListToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.alert.rule.list";
-
- private final AlertService alertService;
-
- @Override
- public String name() {
- return NAME;
- }
-
- @Override
- public Object execute(Map<String, Object> input) {
- String search = (String) input.get("search");
- Boolean enabled = (Boolean) input.get("enabled");
- PageResult<AlertRuleVO> page = alertService.listRules(
- search, enabled, ToolListPagination.page(input), ToolListPagination.pageSize(input));
- return ToolListPagination.pagedResult(page, page.getItems().stream()
- .map(AlertRuleListToolHandler::safeProjection)
- .toList());
- }
-
- private static Map<String, Object> safeProjection(AlertRuleVO rule) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("id", rule.getId());
- result.put("name", require(rule.getName(), "name"));
- result.put("metric", require(rule.getMetric(), "metric"));
- result.put("operator", blankIfNull(rule.getOperator()));
- result.put("threshold", rule.getThreshold());
- result.put("thresholdUnit", blankIfNull(rule.getThresholdUnit()));
- result.put("duration", blankIfNull(rule.getDuration()));
- result.put("channels", copyList(rule.getChannels()));
- result.put("enabled", rule.isEnabled());
- result.put("description", blankIfNull(rule.getDescription()));
- return result;
- }
-
- private static String require(String value, String field) {
- if (value == null || value.isBlank()) {
- throw new IllegalStateException("Alert rule " + field + " is unavailable");
- }
- return value;
- }
-
- private static List<String> copyList(List<String> value) {
- return value == null ? List.of() : List.copyOf(value);
- }
-
- private static String blankIfNull(String value) {
- return value == null ? "" : value;
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilitiesToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilitiesToolHandler.java
deleted file mode 100644
index 9b06eb25b..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilitiesToolHandler.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.broker.ClusterService;
-import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class CapabilitiesToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.capabilities";
-
- private final ClusterService clusterService;
- private final CapabilityResolver capabilityResolver;
-
- @Override
- public String name() {
- return NAME;
- }
-
- @Override
- public Object execute(Map<String, Object> input) {
- String clusterId = (String) input.get("cluster");
- ClusterVO cluster = clusterService.getCluster(clusterId);
- List<String> capabilities = capabilityResolver.resolve(cluster);
-
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("cluster", blankIfNull(cluster.getId()));
- result.put("type", cluster.getType() == null ? "" : cluster.getType().name());
- result.put("version", blankIfNull(cluster.getVersion()));
- result.put("capabilities", capabilities);
- return result;
- }
-
- /**
- * The output schema declares cluster/type/version as required strings; providers that
- * do not populate them (notably the Apache runtime provider, which reports no cluster
- * version) must not emit nulls or schema validation turns the tool call into a 500.
- */
- private static String blankIfNull(String value) {
- return value == null ? "" : value;
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilityResolver.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilityResolver.java
deleted file mode 100644
index 551adc043..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilityResolver.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.broker.ClusterService;
-import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-
-@Component
-@RequiredArgsConstructor
-public class CapabilityResolver {
-
- private final ClusterService clusterService;
-
- public List<String> resolve(String clusterId) {
- return resolve(clusterService.getCluster(clusterId));
- }
-
- List<String> resolve(ClusterVO cluster) {
- if (cluster.getType() == null) {
- throw new BusinessException(
- 400, "Cluster type is unavailable: " + cluster.getId());
- }
- return switch (cluster.getType()) {
- case V4_DIRECT -> List.of(
- "REMOTING",
- "ROCKETMQ_4");
- case V5_PROXY_LOCAL -> List.of(
- "ACL_V2",
- "GRPC",
- "LITE_TOPIC",
- "LOCAL_PROXY",
- "POP",
- "REMOTING",
- "ROCKETMQ_5");
- case V5_PROXY_CLUSTER -> List.of(
- "ACL_V2",
- "CLUSTER_PROXY",
- "GRPC",
- "LITE_TOPIC",
- "POP",
- "REMOTING",
- "ROCKETMQ_5");
- };
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ClusterListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ClusterListToolHandler.java
deleted file mode 100644
index 67e6836c1..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ClusterListToolHandler.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.broker.ClusterService;
-import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class ClusterListToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.cluster.list";
-
- private final ClusterService clusterService;
-
- @Override
- public String name() {
- return NAME;
- }
-
- @Override
- public Object execute(Map<String, Object> input) {
- return clusterService.listClusters().stream()
- .map(ClusterListToolHandler::safeProjection)
- .toList();
- }
-
- private static Map<String, Object> safeProjection(ClusterVO cluster) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("id", blankIfNull(cluster.getId()));
- result.put("name", blankIfNull(cluster.getName()));
- result.put("type", requiredEnumName(cluster.getType(), "type", cluster.getId()));
- result.put("status", requiredEnumName(cluster.getStatus(), "status", cluster.getId()));
- result.put("version", blankIfNull(cluster.getVersion()));
- return result;
- }
-
- /**
- * The output schema declares id/name/version as required strings; providers that do
- * not populate them (notably the Apache runtime provider, which reports no cluster
- * version) must not emit nulls or schema validation turns the tool call into a 500.
- */
- private static String blankIfNull(String value) {
- return value == null ? "" : value;
- }
-
- private static String requiredEnumName(Enum<?> value, String field, String clusterId) {
- if (value == null) {
- throw new IllegalStateException(
- "Cluster " + field + " is unavailable: " + clusterId);
- }
- return value.name();
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ConsumerGroupListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ConsumerGroupListToolHandler.java
deleted file mode 100644
index 87792fed5..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ConsumerGroupListToolHandler.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
-import org.apache.rocketmq.studio.instance.topic.MetadataService;
-import org.apache.rocketmq.studio.provider.apache.ConsumerLagResolver;
-import org.apache.rocketmq.studio.common.domain.PageResult;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class ConsumerGroupListToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.group.list";
-
- private final MetadataService metadataService;
-
- @Override
- public String name() {
- return NAME;
- }
-
- @Override
- public Object execute(Map<String, Object> input) {
- String clusterId = (String) input.get("cluster");
- String search = (String) input.get("search");
- PageResult<ConsumerGroupVO> page = metadataService.listConsumerGroupsPage(
- null, clusterId, search, ToolListPagination.page(input), ToolListPagination.pageSize(input));
- return ToolListPagination.pagedResult(page, page.getItems().stream()
- .map(ConsumerGroupListToolHandler::safeProjection)
- .toList());
- }
-
- private static Map<String, Object> safeProjection(ConsumerGroupVO group) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("name", require(group.getName(), "name"));
- result.put("namespace", blankIfNull(group.getNamespace()));
- result.put("clusterId", blankIfNull(group.getClusterId()));
- result.put("subscriptionMode", requiredEnumName(
- group.getSubscriptionMode(), "subscriptionMode", group.getName()));
- result.put("consumeType", requiredEnumName(
- group.getConsumeType(), "consumeType", group.getName()));
- result.put("onlineInstances", group.getOnlineInstances());
- result.put("totalLag", group.getTotalLag() == ConsumerLagResolver.UNKNOWN ? null : group.getTotalLag());
- result.put("subscribedTopics", copyList(group.getSubscribedTopics()));
- result.put("retryMaxTimes", group.getRetryMaxTimes());
- return result;
- }
-
- private static String requiredEnumName(Enum<?> value, String field, String groupName) {
- if (value == null) {
- throw new IllegalStateException("Consumer group " + field
- + " is unavailable: " + groupName);
- }
- return value.name();
- }
-
- private static String require(String value, String field) {
- if (value == null || value.isBlank()) {
- throw new IllegalStateException("Consumer group " + field + " is unavailable");
- }
- return value;
- }
-
- private static List<String> copyList(List<String> value) {
- return value == null ? List.of() : List.copyOf(value);
- }
-
- private static String blankIfNull(String value) {
- return value == null ? "" : value;
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/DashboardSummaryToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/DashboardSummaryToolHandler.java
deleted file mode 100644
index 0d8708e7f..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/DashboardSummaryToolHandler.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.apache.rocketmq.studio.ops.dashboard.ClusterOverviewVO;
-import org.apache.rocketmq.studio.ops.dashboard.DashboardDataVO;
-import org.apache.rocketmq.studio.ops.dashboard.DashboardService;
-import org.apache.rocketmq.studio.ops.dashboard.DashboardStatsVO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class DashboardSummaryToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.dashboard.summary";
-
- private final DashboardService dashboardService;
-
- @Override
- public String name() {
- return NAME;
- }
-
- @Override
- public Object execute(Map<String, Object> input) {
- String clusterId = (String) input.get("cluster");
- DashboardDataVO dashboard = dashboardService.getDashboard();
- ClusterOverviewVO cluster = clusters(dashboard).stream()
- .filter(item -> clusterId.equals(item.getId()))
- .findFirst()
- .orElseThrow(() -> new BusinessException(
- 404, "Dashboard cluster not found: " + clusterId));
-
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("cluster", clusterProjection(cluster));
- result.put("stats", statsProjection(dashboard.getStats()));
- return result;
- }
-
- private static List<ClusterOverviewVO> clusters(DashboardDataVO dashboard) {
- if (dashboard == null || dashboard.getClusters() == null) {
- return List.of();
- }
- return dashboard.getClusters();
- }
-
- private static Map<String, Object> clusterProjection(ClusterOverviewVO cluster) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("id", cluster.getId());
- result.put("name", cluster.getName());
- result.put("type", requiredEnumName(cluster.getType(), "type", cluster.getId()));
- result.put("status", requiredEnumName(
- cluster.getStatus(), "status", cluster.getId()));
- result.put("brokers", cluster.getBrokers());
- result.put("proxies", cluster.getProxies());
- result.put("topics", cluster.getTopics());
- result.put("groups", cluster.getGroups());
- result.put("tpsIn", cluster.getTpsIn());
- result.put("tpsOut", cluster.getTpsOut());
- result.put("version", cluster.getVersion());
- result.put("throughput", cluster.getThroughput() == null
- ? List.of()
- : cluster.getThroughput());
- return result;
- }
-
- private static Map<String, Object> statsProjection(DashboardStatsVO stats) {
- DashboardStatsVO safeStats = stats == null ? new DashboardStatsVO() : stats;
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("totalClusters", safeStats.getTotalClusters());
- result.put("healthyClusters", safeStats.getHealthyClusters());
- result.put("totalBrokers", safeStats.getTotalBrokers());
- result.put("totalProxies", safeStats.getTotalProxies());
- result.put("totalNameServers", safeStats.getTotalNameServers());
- result.put("totalTopics", safeStats.getTotalTopics());
- result.put("totalConsumerGroups", safeStats.getTotalConsumerGroups());
- result.put("totalMessagesToday", safeStats.getTotalMessagesToday());
- result.put("messagesPerSecond", safeStats.getMessagesPerSecond());
- result.put("tpsIn", safeStats.getTpsIn());
- result.put("tpsOut", safeStats.getTpsOut());
- return result;
- }
-
- private static String requiredEnumName(Enum<?> value, String field, String clusterId) {
- if (value == null) {
- throw new IllegalStateException(
- "Cluster " + field + " is unavailable: " + clusterId);
- }
- return value.name();
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageQueryToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageQueryToolHandler.java
deleted file mode 100644
index 569bbc00b..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageQueryToolHandler.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.apache.rocketmq.studio.instance.message.MessageRecordVO;
-import org.apache.rocketmq.studio.instance.message.MessageService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.math.BigInteger;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Queries messages in a RocketMQ instance by topic, message id, business key or time range. Read-only
- * and safe for AI/MCP/CLI callers; delegates to the standard message service used by the web UI.
- */
-@Component
-@RequiredArgsConstructor
-public class MessageQueryToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.message.query";
-
- private final MessageService messageService;
-
- @Override
- public String name() {
- return NAME;
- }
-
- @Override
- public Object execute(Map<String, Object> input) {
- String instanceId = (String) input.get("cluster");
- String topic = (String) input.get("topic");
- String msgId = (String) input.get("msgId");
- String tag = (String) input.get("tag");
- String key = (String) input.get("key");
- Long startTime = asLong(input.get("startTime"));
- Long endTime = asLong(input.get("endTime"));
- return messageService.queryMessages(instanceId, topic, msgId, tag, key, startTime, endTime).stream()
- .map(MessageQueryToolHandler::safeProjection)
- .toList();
- }
-
- private static Map<String, Object> safeProjection(MessageRecordVO message) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("msgId", require(message.getMsgId(), "msgId"));
- result.put("topic", blankIfNull(message.getTopic()));
- result.put("tag", blankIfNull(message.getTag()));
- result.put("key", blankIfNull(message.getKey()));
- result.put("storeTime", message.getStoreTime());
- result.put("storeHost", blankIfNull(message.getStoreHost()));
- result.put("bornHost", blankIfNull(message.getBornHost()));
- result.put("body", blankIfNull(message.getBody()));
- result.put("bodyEncoding", blankIfNull(message.getBodyEncoding()));
- result.put("bodyTruncated", message.isBodyTruncated());
- result.put("size", message.getSize());
- return result;
- }
-
- private static Long asLong(Object value) {
- if (value == null) {
- return null;
- }
- if (value instanceof Number number) {
- return toEpochMillis(number);
- }
- try {
- return Long.parseLong(value.toString().trim());
- } catch (NumberFormatException ex) {
- throw new BusinessException(
- 400, "startTime and endTime must be integer epoch-milliseconds");
- }
- }
-
- /**
- * Convert a caller-supplied numeric timestamp to epoch-milliseconds without silent
- * overflow. Jackson may deliver out-of-range values as {@link BigInteger} (or as a
- * floating-point number); {@code Number.longValue()} would silently wrap these into a
- * nonsense, possibly negative, timestamp. Reject values that do not fit instead.
- */
- private static long toEpochMillis(Number number) {
- if (number instanceof BigInteger bigInteger) {
- try {
- return bigInteger.longValueExact();
- } catch (ArithmeticException ex) {
- throw new BusinessException(
- 400, "startTime and endTime must fit in a 64-bit epoch-milliseconds value");
- }
- }
- if (number instanceof Double || number instanceof Float) {
- double doubleValue = number.doubleValue();
- if (!Double.isFinite(doubleValue) || doubleValue > Long.MAX_VALUE
- || doubleValue < Long.MIN_VALUE) {
- throw new BusinessException(
- 400, "startTime and endTime must fit in a 64-bit epoch-milliseconds value");
- }
- }
- return number.longValue();
- }
-
- private static String require(String value, String field) {
- if (value == null || value.isBlank()) {
- throw new IllegalStateException("Message " + field + " is unavailable");
- }
- return value;
- }
-
- private static String blankIfNull(String value) {
- return value == null ? "" : value;
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageTraceToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageTraceToolHandler.java
deleted file mode 100644
index ef30a5e45..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageTraceToolHandler.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.instance.message.ConsumerStatusVO;
-import org.apache.rocketmq.studio.instance.message.MessageService;
-import org.apache.rocketmq.studio.instance.message.TraceNodeVO;
-import org.apache.rocketmq.studio.instance.message.TraceRecordVO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Returns the message trace timeline for one message id. Read-only and safe for AI/MCP/CLI callers.
- */
-@Component
-@RequiredArgsConstructor
-public class MessageTraceToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.message.trace";
-
- private final MessageService messageService;
-
- @Override
- public String name() {
- return NAME;
- }
-
- @Override
- public Object execute(Map<String, Object> input) {
- String instanceId = (String) input.get("cluster");
- String msgId = (String) input.get("msgId");
- String topic = (String) input.get("topic");
- TraceRecordVO trace = messageService.getMessageTrace(instanceId, msgId, topic);
- return project(msgId, trace);
- }
-
- private static Map<String, Object> project(String msgId, TraceRecordVO trace) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("msgId", msgId);
- result.put("nodes", trace.getNodes().stream()
- .map(MessageTraceToolHandler::projectNode)
- .toList());
- result.put("consumerStatus", trace.getConsumerStatus().stream()
- .map(MessageTraceToolHandler::projectConsumerStatus)
- .toList());
- return result;
- }
-
- private static Map<String, Object> projectNode(TraceNodeVO node) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("title", blankIfNull(node.getTitle()));
- result.put("timestamp", node.getTimestamp());
- result.put("status", blankIfNull(node.getStatus()));
- result.put("costTime", node.getCostTime());
- result.put("description", blankIfNull(node.getDescription()));
- return result;
- }
-
- private static Map<String, Object> projectConsumerStatus(ConsumerStatusVO status) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("group", blankIfNull(status.getGroup()));
- result.put("deliveryStatus",
- status.getDeliveryStatus() == null ? "" : status.getDeliveryStatus().name());
- result.put("consumeTime", status.getConsumeTime());
- result.put("retryCount", status.getRetryCount());
- return result;
- }
-
- private static String blankIfNull(String value) {
- return value == null ? "" : value;
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolAccessPolicy.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolAccessPolicy.java
deleted file mode 100644
index b23bfedc7..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolAccessPolicy.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.springframework.stereotype.Component;
-
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.util.Optional;
-import java.util.Set;
-
-@Component
-public class ToolAccessPolicy {
-
- private static final String TOOL_EXECUTION_PREFIX = "/api/ai/tools/";
- private static final String TOOL_EXECUTION_SUFFIX = "/execute";
- /**
- * These read-labeled tools still let callers steer RocketMQ remoting to user-influenced
- * broker addresses via msgId-derived lookups, so readers must not gain access before the
- * address-ownership validation bug is fixed.
- */
- private static final Set<String> READER_DENY_LIST = Set.of(
- "rmq.message.query",
- "rmq.message.trace");
-
- private final ToolCatalog catalog;
-
- public ToolAccessPolicy(ToolCatalog catalog) {
- this.catalog = catalog;
- }
-
- public boolean isToolExecutionPath(String requestPath) {
- return requestPath != null
- && requestPath.startsWith(TOOL_EXECUTION_PREFIX)
- && requestPath.endsWith(TOOL_EXECUTION_SUFFIX);
- }
-
- public boolean isReaderAccessiblePath(String requestPath) {
- return resolveExecutionDefinition(requestPath)
- .map(this::isReaderAccessible)
- .orElse(false);
- }
-
- public boolean isReaderAccessible(ToolDefinition definition) {
- return definition != null
- && definition.isLowRiskReadOnly()
- && !READER_DENY_LIST.contains(definition.name());
- }
-
- public void authorizeCurrentUser(ToolDefinition definition) {
- if (AuthenticatedUserContext.currentUserIsAdmin() || isReaderAccessible(definition)) {
- return;
- }
- throw new BusinessException(403, "Admin permission required");
- }
-
- private Optional<ToolDefinition> resolveExecutionDefinition(String requestPath) {
- if (!isToolExecutionPath(requestPath)) {
- return Optional.empty();
- }
- String encodedName = requestPath.substring(
- TOOL_EXECUTION_PREFIX.length(),
- requestPath.length() - TOOL_EXECUTION_SUFFIX.length());
- if (encodedName.isBlank()) {
- return Optional.empty();
- }
- String toolName = URLDecoder.decode(encodedName, StandardCharsets.UTF_8);
- return catalog.find(toolName);
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolDefinition.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolDefinition.java
deleted file mode 100644
index f466ed47f..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolDefinition.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-public record ToolDefinition(
- String name,
- Cli cli,
- String description,
- String riskLevel,
- String permission,
- List<String> requiredCapabilities,
- Map<String, Object> inputSchema,
- Map<String, Object> outputSchema,
- String viewHint,
- boolean deprecated,
- String replacement) {
-
- public ToolDefinition {
- requiredCapabilities = List.copyOf(requiredCapabilities);
- inputSchema = immutableMap(inputSchema);
- outputSchema = immutableMap(outputSchema);
- }
-
- public String getName() {
- return name;
- }
-
- public ToolPermission parsedPermission() {
- return ToolPermission.parse(permission);
- }
-
- public boolean isLowRiskReadOnly() {
- return "L1".equalsIgnoreCase(riskLevel) && parsedPermission().isReadOnly();
- }
-
- private static Map<String, Object> immutableMap(Map<String, Object> source) {
- Map<String, Object> copy = new LinkedHashMap<>();
- source.forEach((key, value) -> copy.put(key, immutableValue(value)));
- return Collections.unmodifiableMap(copy);
- }
-
- private static Object immutableValue(Object value) {
- if (value instanceof Map<?, ?> nestedMap) {
- Map<String, Object> copy = new LinkedHashMap<>();
- nestedMap.forEach((key, nestedValue) -> {
- if (!(key instanceof String stringKey)) {
- throw new IllegalArgumentException("JSON Schema keys must be strings");
- }
- copy.put(stringKey, immutableValue(nestedValue));
- });
- return Collections.unmodifiableMap(copy);
- }
- if (value instanceof List<?> nestedList) {
- List<Object> copy = new ArrayList<>(nestedList.size());
- nestedList.forEach(item -> copy.add(immutableValue(item)));
- return Collections.unmodifiableList(copy);
- }
- return value;
- }
-
- public record Cli(String resource, String verb) {
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayService.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayService.java
deleted file mode 100644
index 1f910151a..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayService.java
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.networknt.schema.Error;
-import com.networknt.schema.Schema;
-import com.networknt.schema.SchemaLocation;
-import com.networknt.schema.SchemaRegistry;
-import com.networknt.schema.SpecificationVersion;
-import com.networknt.schema.dialect.Dialects;
-import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.apache.rocketmq.studio.ops.ai.AiToolVO;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-@Service
-public class ToolGatewayService {
-
- private static final String L1 = "L1";
-
- private final ToolCatalog catalog;
- private final CapabilityResolver capabilityResolver;
- private final ObjectMapper objectMapper;
- private final Map<String, ToolHandler> handlers;
- private final Map<String, Schema> inputSchemas;
- private final Map<String, Schema> outputSchemas;
- private final ToolAccessPolicy toolAccessPolicy;
-
- public ToolGatewayService(
- ToolCatalog catalog,
- CapabilityResolver capabilityResolver,
- ObjectMapper objectMapper,
- ToolAccessPolicy toolAccessPolicy,
- List<ToolHandler> handlers) {
- this.catalog = catalog;
- this.capabilityResolver = capabilityResolver;
- this.objectMapper = objectMapper;
- this.toolAccessPolicy = toolAccessPolicy;
- this.handlers = registerHandlers(catalog, handlers);
-
- SchemaRegistry registry = SchemaRegistry.withDefaultDialect(
- SpecificationVersion.DRAFT_2020_12);
- SchemaRegistry metaSchemaRegistry = SchemaRegistry.withDialect(
- Dialects.getDraft202012());
- Schema metaSchema = metaSchemaRegistry.getSchema(
- SchemaLocation.of(Dialects.getDraft202012().getId()));
- this.inputSchemas = compileSchemas(catalog, registry, metaSchema, true);
- this.outputSchemas = compileSchemas(catalog, registry, metaSchema, false);
- }
-
- public List<AiToolVO> discover(String clusterId) {
- boolean clusterSelected = clusterId != null && !clusterId.isBlank();
- DiscoveryCapabilities discoveryCapabilities = resolveDiscoveryCapabilities(clusterId, clusterSelected);
-
- return catalog.list().stream()
- .filter(definition -> discoveryCapabilities.clusterCapabilitiesResolved()
- || !requiresCluster(definition))
- .filter(definition -> discoveryCapabilities.capabilities().containsAll(
- definition.requiredCapabilities()))
- .filter(this::isVisibleToCurrentUser)
- .map(ToolGatewayService::toView)
- .toList();
- }
-
- /**
- * A tool directory must remain available when the selected cluster cannot report its
- * architecture. In that case expose only tools that do not require cluster capabilities;
- * execution still resolves capabilities and returns the original diagnostic error.
- */
- private DiscoveryCapabilities resolveDiscoveryCapabilities(String clusterId, boolean clusterSelected) {
- if (!clusterSelected) {
- return new DiscoveryCapabilities(false, Collections.emptySet());
- }
- try {
- return new DiscoveryCapabilities(true, Set.copyOf(capabilityResolver.resolve(clusterId)));
- } catch (BusinessException ignored) {
- return new DiscoveryCapabilities(false, Collections.emptySet());
- }
- }
-
- private record DiscoveryCapabilities(boolean clusterCapabilitiesResolved, Set<String> capabilities) {
- }
-
- public Object execute(String name, Map<String, Object> input) {
- ToolDefinition definition = catalog.find(name)
- .orElseThrow(() -> new BusinessException(404, "Tool not found: " + name));
- ToolHandler handler = handlers.get(name);
- Map<String, Object> normalizedInput = input == null
- ? Collections.emptyMap()
- : input;
-
- validateInput(definition, normalizedInput);
- if (!L1.equals(definition.riskLevel())) {
- throw new BusinessException(
- 400, "Execution rejected; only L1 tools are enabled: " + name);
- }
- toolAccessPolicy.authorizeCurrentUser(definition);
- enforceCapabilities(definition, normalizedInput);
-
- Object output = handler.execute(normalizedInput);
- validateOutput(definition, output);
- return output;
- }
-
- private void validateInput(ToolDefinition definition, Map<String, Object> input) {
- List<Error> errors = sortedErrors(
- inputSchemas.get(definition.name()).validate(objectMapper.valueToTree(input)));
- if (!errors.isEmpty()) {
- throw new BusinessException(
- 400,
- "Tool input validation failed for " + definition.name() + ": " + errors);
- }
- }
-
- private void enforceCapabilities(
- ToolDefinition definition,
- Map<String, Object> input) {
- if (definition.requiredCapabilities().isEmpty()) {
- return;
- }
- Object cluster = input.get("cluster");
- if (!(cluster instanceof String clusterId) || clusterId.isBlank()) {
- throw new BusinessException(
- 400, "Tool requires a cluster for capability checks: " + definition.name());
- }
- Set<String> capabilities = Set.copyOf(capabilityResolver.resolve(clusterId));
- if (!capabilities.containsAll(definition.requiredCapabilities())) {
- throw new BusinessException(
- 400, "Cluster does not support tool: " + definition.name());
- }
- }
-
- private void validateOutput(ToolDefinition definition, Object output) {
- JsonNode outputNode = objectMapper.valueToTree(output);
- List<Error> errors = sortedErrors(
- outputSchemas.get(definition.name()).validate(outputNode));
- if (!errors.isEmpty()) {
- throw new IllegalStateException(
- "Tool output validation failed for " + definition.name() + ": " + errors);
- }
- }
-
- private static Map<String, ToolHandler> registerHandlers(
- ToolCatalog catalog,
- List<ToolHandler> handlers) {
- Map<String, ToolHandler> registered = new LinkedHashMap<>();
- for (ToolHandler handler : handlers) {
- if (registered.putIfAbsent(handler.name(), handler) != null) {
- throw new IllegalStateException(
- "Tool gateway contains duplicate handler: " + handler.name());
- }
- if (catalog.find(handler.name()).isEmpty()) {
- throw new IllegalStateException(
- "Tool handler is absent from catalog: " + handler.name());
- }
- }
-
- List<String> missing = catalog.list().stream()
- .map(ToolDefinition::name)
- .filter(name -> !registered.containsKey(name))
- .toList();
- if (!missing.isEmpty()) {
- throw new IllegalStateException(
- "Tool catalog contains missing handler: " + missing);
- }
- return Collections.unmodifiableMap(registered);
- }
-
- private Map<String, Schema> compileSchemas(
- ToolCatalog catalog,
- SchemaRegistry registry,
- Schema metaSchema,
- boolean input) {
- String schemaKind = input ? "input" : "output";
- Map<String, Schema> compiled = new LinkedHashMap<>();
- for (ToolDefinition definition : catalog.list()) {
- JsonNode schemaNode = objectMapper.valueToTree(
- input ? definition.inputSchema() : definition.outputSchema());
- List<Error> metaSchemaErrors = sortedErrors(metaSchema.validate(schemaNode));
- if (!metaSchemaErrors.isEmpty()) {
- throw new IllegalStateException(
- "Tool " + schemaKind + " schema is invalid for "
- + definition.name() + ": " + metaSchemaErrors);
- }
-
- try {
- Schema schema = registry.getSchema(schemaNode);
- schema.initializeValidators();
- compiled.put(definition.name(), schema);
- } catch (RuntimeException ex) {
- throw new IllegalStateException(
- "Tool " + schemaKind + " schema is invalid for "
- + definition.name(), ex);
- }
- }
- return Collections.unmodifiableMap(compiled);
- }
-
- private static boolean requiresCluster(ToolDefinition definition) {
- Object required = definition.inputSchema().get("required");
- return required instanceof List<?> fields && fields.contains("cluster");
- }
-
- private boolean isVisibleToCurrentUser(ToolDefinition definition) {
- return AuthenticatedUserContext.currentUserIsAdmin() || toolAccessPolicy.isReaderAccessible(definition);
- }
-
- private static AiToolVO toView(ToolDefinition definition) {
- return AiToolVO.builder()
- .name(definition.name())
- .description(definition.description())
- .parameters(definition.inputSchema())
- .riskLevel(definition.riskLevel())
- .permission(definition.permission())
- .requiredCapabilities(definition.requiredCapabilities())
- .outputSchema(definition.outputSchema())
- .viewHint(definition.viewHint())
- .deprecated(definition.deprecated())
- .replacement(definition.replacement())
- .build();
- }
-
- private static List<Error> sortedErrors(List<Error> errors) {
- List<Error> sorted = new ArrayList<>(errors);
- sorted.sort(Comparator.comparing(error -> error.getInstanceLocation().toString()));
- return sorted;
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolListPagination.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolListPagination.java
deleted file mode 100644
index 70d2930aa..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolListPagination.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import org.apache.rocketmq.studio.common.domain.PageResult;
-
-/** Shared pagination helpers for list-style AI tool handlers. */
-final class ToolListPagination {
-
- static final int DEFAULT_PAGE = 1;
- static final int DEFAULT_PAGE_SIZE = 20;
-
- private ToolListPagination() {}
-
- static Map<String, Object> pagedResult(PageResult<?> page, List<Map<String, Object>> items) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("items", items);
- result.put("total", page.getTotal());
- result.put("page", page.getPage());
- result.put("size", page.getSize());
- return result;
- }
-
- static int page(Map<String, Object> input) {
- return optionalPositiveInteger(input.get("page"), DEFAULT_PAGE, "page");
- }
-
- static int pageSize(Map<String, Object> input) {
- return optionalPositiveInteger(input.get("pageSize"), DEFAULT_PAGE_SIZE, "pageSize");
- }
-
- private static int optionalPositiveInteger(Object value, int defaultValue, String field) {
- if (value == null) {
- return defaultValue;
- }
- if (value instanceof Number number) {
- int result = number.intValue();
- if (result > 0) {
- return result;
- }
- }
- throw new IllegalArgumentException(field + " must be positive");
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/TopicListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/TopicListToolHandler.java
deleted file mode 100644
index 50f669f76..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/TopicListToolHandler.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.instance.topic.MetadataService;
-import org.apache.rocketmq.studio.instance.topic.TopicVO;
-import org.apache.rocketmq.studio.common.domain.PageResult;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class TopicListToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.topic.list";
-
- private final MetadataService metadataService;
-
- @Override
- public String name() {
- return NAME;
- }
-
- @Override
- public Object execute(Map<String, Object> input) {
- String clusterId = (String) input.get("cluster");
- String type = (String) input.get("type");
- String search = (String) input.get("search");
- PageResult<TopicVO> page = metadataService.listTopicsPage(
- null, clusterId, type, search, ToolListPagination.page(input), ToolListPagination.pageSize(input));
- return ToolListPagination.pagedResult(page, page.getItems().stream()
- .map(TopicListToolHandler::safeProjection)
- .toList());
- }
-
- private static Map<String, Object> safeProjection(TopicVO topic) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("name", require(topic.getName(), "name"));
- result.put("namespace", blankIfNull(topic.getNamespace()));
- result.put("clusterId", blankIfNull(topic.getClusterId()));
- result.put("type", requiredEnumName(topic.getType(), "type", topic.getName()));
- result.put("writeQueues", topic.getWriteQueues());
- result.put("readQueues", topic.getReadQueues());
- result.put("perm", requiredEnumName(topic.getPerm(), "perm", topic.getName()));
- result.put("messageCount", topic.getMessageCount());
- result.put("tps", topic.getTps());
- result.put("consumerGroupCount", topic.getConsumerGroupCount());
- return result;
- }
-
- private static String requiredEnumName(Enum<?> value, String field, String topicName) {
- if (value == null) {
- throw new IllegalStateException("Topic " + field + " is unavailable: " + topicName);
- }
- return value.name();
- }
-
- private static String require(String value, String field) {
- if (value == null || value.isBlank()) {
- throw new IllegalStateException("Topic " + field + " is unavailable");
- }
- return value;
- }
-
- private static String blankIfNull(String value) {
- return value == null ? "" : value;
- }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/CapabilityResolver.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/CapabilityResolver.java
new file mode 100644
index 000000000..4dd231836
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/CapabilityResolver.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.catalog;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
+import org.apache.rocketmq.studio.instance.InstanceVO;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.provider.InstanceCapability;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Component
+@RequiredArgsConstructor
+public class CapabilityResolver {
+
+ private final InstanceProviderRegistry providerRegistry;
+ private final InstanceResolver instanceResolver;
+
+ public Set<String> resolve(String cluster) {
+ if (cluster == null || cluster.isBlank()) {
+ throw ToolError.CAPABILITY_CLUSTER_REQUIRED.exception();
+ }
+ InstanceVO instance = instanceResolver.findByName(cluster)
+ .orElseThrow(() -> ToolError.INSTANCE_NOT_FOUND.exception(cluster));
+ return resolve(instance);
+ }
+
+ public Set<String> resolve(InstanceVO instance) {
+ InstanceVendor vendor = instance.getVendor() == null
+ ? InstanceVendor.APACHE : instance.getVendor();
+ InstanceType type = instance.getType() == null ? InstanceType.DIRECT : instance.getType();
+ Set<String> accessCapabilities = switch (type) {
+ case CLOUD -> Set.of("CLOUD_API");
+ case DIRECT -> Set.of(
+ "BROKER_ADMIN",
+ "CLUSTER_TOPOLOGY",
+ "NAMESERVER_ADMIN",
+ "REMOTING");
+ case PROXY_LOCAL, PROXY_CLUSTER -> Set.of(
+ "PROXY_DISCOVERY",
+ "BROKER_ADMIN",
+ "CLUSTER_TOPOLOGY",
+ "NAMESERVER_ADMIN",
+ "REMOTING");
+ };
+ Set<String> capabilities = new HashSet<>(accessCapabilities);
+ providerRegistry.forVendor(vendor).capabilities().stream()
+ .map(InstanceCapability::name)
+ .forEach(capabilities::add);
+ return Set.copyOf(capabilities);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalog.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/ToolCatalog.java
similarity index 54%
rename from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalog.java
rename to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/ToolCatalog.java
index d33d2a490..14152b918 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalog.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/ToolCatalog.java
@@ -14,8 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.ops.ai.tool;
+package org.apache.rocketmq.studio.ops.ai.tool.catalog;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.networknt.schema.Error;
@@ -24,18 +25,20 @@ import com.networknt.schema.Schema;
import com.networknt.schema.SchemaRegistry;
import com.networknt.schema.SpecificationVersion;
import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Comparator;
-import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -44,79 +47,87 @@ import java.util.Optional;
@Component
public class ToolCatalog {
- static final String CATALOG_RESOURCE = "classpath:tool-catalog/rmq-tools.yaml";
+ static final String MANIFEST_RESOURCE = "classpath:tool-catalog/manifest.yaml";
+ static final String SHARD_PATTERN = "classpath*:tool-catalog/tools/*.yaml";
static final String SCHEMA_RESOURCE = "classpath:tool-catalog/rmq-tools.schema.json";
- private static final String CLUSTER_LIST_TOOL = "rmq.cluster.list";
private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory());
@Getter
private final String version;
- @Getter
- private final String minimumClientVersion;
- @Getter
- private final String digest;
private final List<ToolDefinition> definitions;
private final Map<String, ToolDefinition> definitionsByName;
@Autowired
public ToolCatalog(ResourceLoader resourceLoader) {
- ToolCatalog loaded = load(
- resourceLoader.getResource(CATALOG_RESOURCE),
- resourceLoader.getResource(SCHEMA_RESOURCE));
+ ToolCatalog loaded = load(resourceLoader);
this.version = loaded.version;
- this.minimumClientVersion = loaded.minimumClientVersion;
- this.digest = loaded.digest;
this.definitions = loaded.definitions;
this.definitionsByName = loaded.definitionsByName;
}
private ToolCatalog(
String version,
- String minimumClientVersion,
- String digest,
List<ToolDefinition> definitions,
Map<String, ToolDefinition> definitionsByName) {
this.version = version;
- this.minimumClientVersion = minimumClientVersion;
- this.digest = digest;
this.definitions = definitions;
this.definitionsByName = definitionsByName;
}
- static ToolCatalog load(Resource catalogResource, Resource schemaResource) {
+ static ToolCatalog load(ResourceLoader resourceLoader) {
try {
- byte[] catalogBytes = catalogResource.getContentAsByteArray();
- String catalogYaml = new String(catalogBytes, StandardCharsets.UTF_8);
- String schemaJson = schemaResource.getContentAsString(StandardCharsets.UTF_8);
+ Resource manifestResource = resourceLoader.getResource(MANIFEST_RESOURCE);
+ Resource schemaResource = resourceLoader.getResource(SCHEMA_RESOURCE);
+ byte[] manifestBytes = manifestResource.getContentAsByteArray();
+ ManifestDocument manifest = YAML_MAPPER.readValue(manifestBytes, ManifestDocument.class);
+
+ ResourcePatternResolver resourcePatternResolver =
+ ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
+ Resource[] shards = resourcePatternResolver.getResources(SHARD_PATTERN);
+ List<Resource> shardResources = Arrays.stream(shards)
+ .sorted(Comparator.comparing(Resource::getFilename, Comparator.nullsFirst(Comparator.naturalOrder())))
+ .toList();
+ if (shardResources.isEmpty()) {
+ throw new IllegalStateException("Tool catalog contains no shards");
+ }
+ String schemaJson = schemaResource.getContentAsString(StandardCharsets.UTF_8);
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(
SpecificationVersion.DRAFT_2020_12);
Schema schema = registry.getSchema(schemaJson, InputFormat.JSON);
- List<Error> validationErrors = new ArrayList<>(
- schema.validate(catalogYaml, InputFormat.YAML));
- if (!validationErrors.isEmpty()) {
- validationErrors.sort(Comparator.comparing(
- error -> error.getInstanceLocation().toString()));
- throw new IllegalStateException(
- "Tool catalog schema validation failed: " + validationErrors);
+
+ List<ToolDefinition> allTools = new ArrayList<>();
+ for (Resource shard : shardResources) {
+ byte[] shardBytes = shard.getContentAsByteArray();
+ String shardYaml = new String(shardBytes, StandardCharsets.UTF_8);
+ List<Error> shardErrors = new ArrayList<>(
+ schema.validate(shardYaml, InputFormat.YAML));
+ if (!shardErrors.isEmpty()) {
+ shardErrors.sort(Comparator.comparing(
+ error -> error.getInstanceLocation().toString()));
+ throw new IllegalStateException(
+ "Tool catalog shard validation failed for " + shard.getFilename()
+ + ": " + shardErrors);
+ }
+ ShardDocument shardDoc = YAML_MAPPER.readValue(shardBytes, ShardDocument.class);
+ if (!manifest.version().equals(shardDoc.version())) {
+ throw new IllegalStateException(
+ "Tool catalog shard " + shard.getFilename()
+ + " version mismatch: expected " + manifest.version()
+ + ", got " + shardDoc.version());
+ }
+ allTools.addAll(shardDoc.tools());
}
- CatalogDocument document = YAML_MAPPER.readValue(catalogBytes, CatalogDocument.class);
- return validatedCatalog(document, sha256(catalogBytes));
+ CatalogDocument document = new CatalogDocument(manifest.version(), allTools);
+ return validatedCatalog(document);
} catch (IOException e) {
throw new IllegalStateException("Unable to load RocketMQ tool catalog", e);
}
}
- private static ToolCatalog validatedCatalog(CatalogDocument document, String digest) {
- int catalogMajor = majorVersion(document.version());
- int minimumClientMajor = majorVersion(document.minimumClientVersion());
- if (catalogMajor != minimumClientMajor) {
- throw new IllegalStateException(
- "Catalog and minimum client major versions must match");
- }
-
+ private static ToolCatalog validatedCatalog(CatalogDocument document) {
Map<String, ToolDefinition> byName = new LinkedHashMap<>();
for (ToolDefinition definition : document.tools()) {
if (byName.putIfAbsent(definition.name(), definition) != null) {
@@ -129,47 +140,48 @@ public class ToolCatalog {
List<ToolDefinition> immutableDefinitions = List.copyOf(byName.values());
return new ToolCatalog(
document.version(),
- document.minimumClientVersion(),
- digest,
immutableDefinitions,
Map.copyOf(byName));
}
private static void validateClusterConvention(ToolDefinition definition) {
- if (CLUSTER_LIST_TOOL.equals(definition.name())) {
- return;
- }
Object required = definition.inputSchema().get("required");
if (!(required instanceof List<?> requiredFields) || !requiredFields.contains("cluster")) {
throw new IllegalStateException(
- "Remote tool must require cluster: " + definition.name());
+ "Tool must require cluster: " + definition.name());
}
}
- private static int majorVersion(String version) {
- return Integer.parseInt(version.substring(0, version.indexOf('.')));
+ public List<ToolDefinition> list() {
+ return definitions;
}
- private static String sha256(byte[] bytes) {
- try {
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
- return HexFormat.of().formatHex(digest.digest(bytes));
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException("SHA-256 is unavailable", e);
+ public Optional<ToolDefinition> find(String name) {
+ return Optional.ofNullable(definitionsByName.get(name));
+ }
+
+ public ToolDefinition getDefinition(String name) {
+ if (name == null || name.isBlank()) {
+ throw ToolError.TOOL_NAME_REQUIRED.exception();
+ }
+ ToolDefinition definition = definitionsByName.get(name);
+ if (definition == null) {
+ throw ToolError.TOOL_NOT_FOUND.exception(name);
}
+ return definition;
}
- public List<ToolDefinition> list() {
- return definitions;
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record ManifestDocument(String version) {
}
- public Optional<ToolDefinition> find(String name) {
- return Optional.ofNullable(definitionsByName.get(name));
+ private record ShardDocument(
+ String version,
+ List<ToolDefinition> tools) {
}
private record CatalogDocument(
String version,
- String minimumClientVersion,
List<ToolDefinition> tools) {
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclListInput.java
similarity index 64%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclListInput.java
index 2fcbb55d6..ad7600051 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclListInput.java
@@ -14,16 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.acl;
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.PageRequest;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
+public record AclListInput(
+ String cluster,
+ String principal,
+ String resource,
+ String scope,
+ String decision,
+ String aclVersion,
+ PageRequest page) {
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ public AclListInput {
+ if (page == null) {
+ page = new PageRequest(0, 0);
+ }
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclMutationInput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclMutationInput.java
new file mode 100644
index 000000000..2c3ca71ae
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclMutationInput.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+
+import java.util.List;
+
+public record AclMutationInput(
+ String cluster,
+ String id,
+ String principal,
+ String resource,
+ String resourceType,
+ String resourcePattern,
+ List<String> actions,
+ String decision,
+ String scope,
+ String aclVersion) {
+
+ public AclRuleVO toRule(Long ruleId) {
+ return AclRuleVO.builder()
+ .id(ruleId)
+ .principal(principal)
+ .resource(resource)
+ .resourceType(resourceType)
+ .resourcePattern(resourcePattern)
+ .actions(actions)
+ .decision(decision)
+ .scope(scope)
+ .aclVersion(aclVersion)
+ .build();
+ }
+
+ public static AclMutationInput from(AclRuleVO rule) {
+ return new AclMutationInput(
+ null,
+ rule.getId() == null ? null : rule.getId().toString(),
+ rule.getPrincipal(),
+ rule.getResource(),
+ rule.getResourceType(),
+ rule.getResourcePattern(),
+ rule.getActions(),
+ rule.getDecision(),
+ rule.getScope(),
+ rule.getAclVersion());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclRuleItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclRuleItem.java
new file mode 100644
index 000000000..c49eb5dc6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclRuleItem.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+
+import java.util.List;
+
+public record AclRuleItem(
+ Long id,
+ String principal,
+ String resource,
+ String resourceType,
+ String resourcePattern,
+ List<String> actions,
+ String decision,
+ String scope,
+ String aclVersion,
+ String gmtCreate) {
+
+ public static AclRuleItem from(AclRuleVO rule) {
+ return new AclRuleItem(
+ rule.getId(),
+ rule.getPrincipal(),
+ rule.getResource(),
+ rule.getResourceType(),
+ rule.getResourcePattern(),
+ rule.getActions(),
+ rule.getDecision(),
+ rule.getScope(),
+ rule.getAclVersion(),
+ rule.getGmtCreate() == null ? null : rule.getGmtCreate().toString());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclUserItem.java
similarity index 58%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclUserItem.java
index 141b1cef2..5fcf60be6 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclUserItem.java
@@ -14,26 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.acl;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.acl.AclUserVO;
import java.util.List;
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
- }
-
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record AclUserItem(
+ String id,
+ String username,
+ boolean admin,
+ List<String> clusters) {
+
+ public static AclUserItem from(AclUserVO user) {
+ return new AclUserItem(
+ user.getId() == null ? null : user.getId().toString(),
+ user.getUsername(),
+ user.isAdmin(),
+ user.getClusters());
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/UserCreateInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/UserCreateInput.java
index 2fcbb55d6..191869020 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/UserCreateInput.java
@@ -14,16 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.acl;
-import lombok.Getter;
+import java.util.List;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record UserCreateInput(
+ String cluster,
+ String username,
+ Boolean admin,
+ List<String> clusters) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListInput.java
index 2fcbb55d6..8c7301805 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.alert;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record AlertRuleListInput(String cluster, String search, Boolean enabled) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListItem.java
new file mode 100644
index 000000000..203e187e2
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListItem.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.alert;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record AlertRuleListItem(
+ Long id,
+ String name,
+ String metric,
+ String operator,
+ double threshold,
+ String thresholdUnit,
+ String duration,
+ List<String> channels,
+ boolean enabled,
+ String description) {
+
+ public AlertRuleListItem {
+ channels = channels == null ? List.of() : channels;
+ }
+
+ public static AlertRuleListItem from(AlertRuleVO rule) {
+ return new AlertRuleListItem(
+ rule.getId(),
+ rule.getName(),
+ rule.getMetric(),
+ rule.getOperator(),
+ rule.getThreshold(),
+ rule.getThresholdUnit(),
+ rule.getDuration(),
+ rule.getChannels(),
+ rule.isEnabled(),
+ rule.getDescription());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigOutput.java
new file mode 100644
index 000000000..8d08b5c15
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigOutput.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.broker;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.cluster.config.BrokerConfigDiffVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record BrokerConfigOutput(
+ String cluster,
+ boolean complete,
+ boolean driftDetected,
+ int brokerCount,
+ int reachableBrokerCount,
+ List<String> comparedFields,
+ List<BrokerStatus> brokers,
+ List<ConfigDifference> differences) {
+
+ public static BrokerConfigOutput from(BrokerConfigDiffVO diff, String instanceId) {
+ return new BrokerConfigOutput(
+ instanceId,
+ diff.isComplete(),
+ diff.isDriftDetected(),
+ diff.getBrokerCount(),
+ diff.getReachableBrokerCount(),
+ diff.getComparedFields(),
+ diff.getBrokers() != null
+ ? diff.getBrokers().stream().map(BrokerStatus::from).toList()
+ : List.of(),
+ diff.getDifferences() != null
+ ? diff.getDifferences().stream().map(ConfigDifference::from).toList()
+ : List.of());
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record BrokerStatus(
+ String name,
+ String address,
+ boolean reachable,
+ String message) {
+
+ public static BrokerStatus from(BrokerConfigDiffVO.BrokerStatusVO broker) {
+ return new BrokerStatus(
+ broker.getName(),
+ broker.getAddress(),
+ broker.isReachable(),
+ broker.getMessage());
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record ConfigDifference(
+ String field,
+ String brokerProperty,
+ List<ConfigValue> values) {
+
+ public static ConfigDifference from(BrokerConfigDiffVO.ConfigDifferenceVO diff) {
+ return new ConfigDifference(
+ diff.getField(),
+ diff.getBrokerProperty(),
+ diff.getValues() != null
+ ? diff.getValues().stream().map(ConfigValue::from).toList()
+ : List.of());
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record ConfigValue(
+ String brokerName,
+ String address,
+ boolean configured,
+ String value) {
+
+ public static ConfigValue from(BrokerConfigDiffVO.ConfigValueVO value) {
+ return new ConfigValue(
+ value.getBrokerName(),
+ value.getAddress(),
+ value.isConfigured(),
+ value.getValue());
+ }
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateInput.java
similarity index 66%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateInput.java
index 2fcbb55d6..591bc0964 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateInput.java
@@ -14,16 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.broker;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record BrokerConfigUpdateInput(
+ String cluster,
+ String flushDiskType,
+ Boolean autoCreateTopicEnable,
+ Boolean autoCreateSubscriptionGroup,
+ Integer maxMessageSize,
+ Integer fileReservedTime,
+ Integer writeQueueNums,
+ Integer readQueueNums,
+ Integer brokerPermission) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateOutput.java
new file mode 100644
index 000000000..a6b41a12d
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateOutput.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.broker;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.cluster.config.BrokerConfigUpdateFailureVO;
+import org.apache.rocketmq.studio.cluster.config.ClusterConfigUpdateResultVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record BrokerConfigUpdateOutput(
+ String status,
+ List<String> successfulBrokers,
+ List<BrokerConfigUpdateFailure> failedBrokers) {
+
+ public static BrokerConfigUpdateOutput from(ClusterConfigUpdateResultVO result) {
+ return new BrokerConfigUpdateOutput(
+ result.getStatus() != null ? result.getStatus().name() : null,
+ result.getSuccessfulBrokers() != null ? result.getSuccessfulBrokers() : List.of(),
+ result.getFailedBrokers() != null
+ ? result.getFailedBrokers().stream().map(BrokerConfigUpdateFailure::from).toList()
+ : List.of());
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record BrokerConfigUpdateFailure(
+ String address,
+ String message) {
+
+ public static BrokerConfigUpdateFailure from(BrokerConfigUpdateFailureVO failure) {
+ return new BrokerConfigUpdateFailure(failure.getAddress(), failure.getMessage());
+ }
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerDescribeInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerDescribeInput.java
index 2fcbb55d6..06c329731 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerDescribeInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.broker;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record BrokerDescribeInput(String cluster, String broker) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientDescribeInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientDescribeInput.java
index 2fcbb55d6..33c110c72 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientDescribeInput.java
@@ -14,16 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.client;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record ClientDescribeInput(
+ String cluster,
+ String clientId,
+ String type) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientInstance.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientInstance.java
new file mode 100644
index 000000000..1dbadcf26
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientInstance.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.client;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.group.ConsumerInstanceVO;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ClientInstance(
+ String clientId,
+ String protocol,
+ String address,
+ List<String> subscribedTopics,
+ String lastHeartbeat,
+ Map<String, Long> topicLag) {
+
+ public static ClientInstance from(ConsumerInstanceVO instance) {
+ return new ClientInstance(
+ instance.getClientId(),
+ instance.getProtocol() != null ? instance.getProtocol().name() : "UNKNOWN",
+ instance.getAddress(),
+ Objects.requireNonNullElseGet(instance.getSubscribedTopics(), List::of),
+ instance.getLastHeartbeat() == null ? null : instance.getLastHeartbeat().toString(),
+ Objects.requireNonNullElseGet(instance.getTopicLag(), Map::of));
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientListInput.java
index 2fcbb55d6..b3dd11140 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientListInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.client;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record ClientListInput(String cluster, String type) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListInput.java
index 2fcbb55d6..e69e9e46d 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.cluster;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record ClusterListInput(String cluster, String status) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListItem.java
similarity index 50%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListItem.java
index 141b1cef2..ab2a9a5b6 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListItem.java
@@ -14,26 +14,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
- }
-
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+package org.apache.rocketmq.studio.ops.ai.tool.contract.cluster;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
+import org.apache.rocketmq.studio.common.domain.enums.ClusterStatus;
+import org.apache.rocketmq.studio.common.domain.enums.ClusterType;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ClusterListItem(
+ String id,
+ String name,
+ ClusterType type,
+ ClusterStatus status,
+ String version) {
+
+ public static ClusterListItem from(ClusterVO cluster) {
+ return new ClusterListItem(
+ cluster.getId(),
+ cluster.getName(),
+ cluster.getType(),
+ cluster.getStatus(),
+ cluster.getVersion());
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/BrokerClusterInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/BrokerClusterInput.java
index 2fcbb55d6..ebb2d4124 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/BrokerClusterInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record BrokerClusterInput(String cluster) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/CapabilitiesOutput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/CapabilitiesOutput.java
index 2fcbb55d6..d7cbca6e5 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/CapabilitiesOutput.java
@@ -14,16 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
-import lombok.Getter;
+import java.util.List;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record CapabilitiesOutput(
+ String cluster,
+ List<String> capabilities) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterInput.java
index 2fcbb55d6..94edc66ff 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record ClusterInput(String cluster) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterListOutput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterListOutput.java
index 2fcbb55d6..63f80c109 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterListOutput.java
@@ -14,16 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
-import lombok.Getter;
+import java.util.List;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record ClusterListOutput<T>(String cluster, List<T> items) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ListOutput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ListOutput.java
index 2fcbb55d6..1b1cb6b4c 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ListOutput.java
@@ -14,16 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
-import lombok.Getter;
+import java.util.List;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record ListOutput<T>(List<T> items) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/MutationOutput.java
similarity index 60%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/MutationOutput.java
index 2fcbb55d6..e18ec895a 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/MutationOutput.java
@@ -14,16 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
-import lombok.Getter;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record MutationOutput<R>(
+ Status status,
+ String cluster,
+ ToolPlan plan,
+ @JsonProperty("confirm_token") String confirmToken,
+ R result) {
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ public enum Status {
+ PLANNED,
+ EXECUTED
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageOutput.java
similarity index 60%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageOutput.java
index 141b1cef2..6536ad232 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageOutput.java
@@ -14,26 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
+import org.apache.rocketmq.studio.common.domain.PageResult;
import java.util.List;
-
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
- }
-
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+import java.util.function.Function;
+
+public record PageOutput<T>(
+ int page,
+ int pageSize,
+ long total,
+ List<T> items) {
+
+ public static <S, T> PageOutput<T> from(PageResult<S> page, Function<S, T> mapper) {
+ return new PageOutput<>(
+ page.getPage(),
+ page.getSize(),
+ page.getTotal(),
+ page.getItems().stream().map(mapper).toList());
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageRequest.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageRequest.java
index 2fcbb55d6..43cf22221 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageRequest.java
@@ -14,16 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
-import lombok.Getter;
+public record PageRequest(int page, int pageSize) {
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ public PageRequest {
+ if (page <= 0) {
+ page = 1;
+ }
+ if (pageSize <= 0) {
+ pageSize = 20;
+ }
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ResourceDeleteInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ResourceDeleteInput.java
index 2fcbb55d6..64938f65a 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ResourceDeleteInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record ResourceDeleteInput(String cluster, String id) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/TimeRange.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/TimeRange.java
index 2fcbb55d6..f1c7fd642 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/TimeRange.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record TimeRange(Long startTime, Long endTime) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQClearInput.java
similarity index 67%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQClearInput.java
index 2fcbb55d6..86effbf08 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQClearInput.java
@@ -1,6 +1,6 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
+ * contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.dlq;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record DLQClearInput(String cluster, String group) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListInput.java
similarity index 69%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListInput.java
index 2fcbb55d6..9c4a343b5 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListInput.java
@@ -14,16 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.dlq;
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.PageRequest;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.TimeRange;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record DLQListInput(
+ String cluster,
+ String group,
+ TimeRange time,
+ PageRequest page,
+ String search) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListOutput.java
new file mode 100644
index 000000000..318da783c
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListOutput.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.dlq;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record DLQListOutput(
+ String cluster,
+ String group,
+ Integer page,
+ Integer pageSize,
+ Long total,
+ java.util.List<?> items) {
+
+ public static DLQListOutput ofGroups(
+ String cluster,
+ Integer page,
+ Integer pageSize,
+ Long total,
+ java.util.List<DLQGroupItem> items) {
+ return new DLQListOutput(cluster, null, page, pageSize, total, items);
+ }
+
+ public static DLQListOutput ofMessages(
+ String cluster,
+ String group,
+ Integer page,
+ Integer pageSize,
+ Long total,
+ java.util.List<DLQMessageItem> items) {
+ return new DLQListOutput(cluster, group, page, pageSize, total, items);
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record DLQGroupItem(
+ String groupName,
+ String dlqTopic,
+ long messageCount,
+ int retryCount,
+ String status,
+ boolean statsAvailable,
+ String lastEnqueueTime) {
+
+ public static DLQGroupItem from(
+ org.apache.rocketmq.studio.instance.dlq.DLQGroupVO group) {
+ return new DLQGroupItem(
+ group.getGroupName(),
+ group.getDlqTopic(),
+ group.getMessageCount(),
+ group.getRetryCount(),
+ group.getStatus(),
+ group.isStatsAvailable(),
+ group.getLastEnqueueTime() != null
+ ? group.getLastEnqueueTime().toString() : null);
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record DLQMessageItem(
+ String msgId,
+ String topic,
+ int queueId,
+ long offset,
+ long storeTime,
+ String keys,
+ String body) {
+
+ public static DLQMessageItem from(
+ org.apache.rocketmq.studio.instance.dlq.DLQMessageVO message) {
+ return new DLQMessageItem(
+ message.getMsgId(),
+ message.getTopic(),
+ message.getQueueId(),
+ message.getOffset(),
+ message.getStoreTime(),
+ message.getKeys(),
+ message.getBody());
+ }
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendInput.java
index 2fcbb55d6..aa8a37008 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendInput.java
@@ -14,16 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.dlq;
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.TimeRange;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record DLQResendInput(
+ String cluster,
+ String group,
+ TimeRange time,
+ String targetTopic) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendOutput.java
similarity index 55%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendOutput.java
index 141b1cef2..c0e040177 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendOutput.java
@@ -14,26 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
- }
-
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+package org.apache.rocketmq.studio.ops.ai.tool.contract.dlq;
+
+import org.apache.rocketmq.studio.instance.dlq.DLQResendResultVO;
+
+public record DLQResendOutput(
+ int matched,
+ int resent,
+ int failed,
+ String outcome,
+ boolean scanIncomplete,
+ int failedQueueCount) {
+
+ public static DLQResendOutput from(DLQResendResultVO result) {
+ return new DLQResendOutput(
+ result.getMatched(),
+ result.getResent(),
+ result.getFailed(),
+ result.getOutcome(),
+ result.isScanIncomplete(),
+ result.getFailedQueueCount());
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupClientsOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupClientsOutput.java
new file mode 100644
index 000000000..433306e6d
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupClientsOutput.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.ConsumerInstanceVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.client.ClientInstance;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record GroupClientsOutput(
+ String cluster,
+ String group,
+ String topic,
+ int totalClients,
+ List<ClientInstance> clients) {
+
+ public static GroupClientsOutput from(
+ ConsumerGroupVO group,
+ String cluster,
+ String requestedGroup,
+ String topic) {
+ List<ClientInstance> clients = group.getInstances() == null ? List.of() : group.getInstances().stream()
+ .filter(java.util.Objects::nonNull)
+ .filter(instance -> matchesTopic(instance, topic))
+ .map(ClientInstance::from)
+ .toList();
+ return new GroupClientsOutput(
+ cluster,
+ StringUtils.hasText(group.getName()) ? group.getName() : requestedGroup,
+ StringUtils.hasText(topic) ? topic : null,
+ clients.size(),
+ clients);
+ }
+
+ static boolean matchesTopic(ConsumerInstanceVO instance, String topic) {
+ if (!StringUtils.hasText(topic)) {
+ return true;
+ }
+ List<String> subscribedTopics = instance.getSubscribedTopics();
+ if (subscribedTopics != null && subscribedTopics.contains(topic)) {
+ return true;
+ }
+ return instance.getTopicLag() != null && instance.getTopicLag().containsKey(topic);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeInput.java
index 2fcbb55d6..ad4925ba6 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record GroupDescribeInput(String cluster, String group, String namespace) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeOutput.java
new file mode 100644
index 000000000..8e9f51676
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeOutput.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
+import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.ConsumerInstanceVO;
+import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.client.ClientInstance;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.Objects;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record GroupDescribeOutput(
+ String cluster,
+ String group,
+ String namespace,
+ String instanceId,
+ SubscriptionMode subscriptionMode,
+ ConsumeType consumeType,
+ int onlineInstances,
+ long totalLag,
+ List<String> subscribedTopics,
+ String subscriptionDataType,
+ String deliveryOrderType,
+ Integer retryMaxTimes,
+ int delaySeconds,
+ List<Subscription> subscriptions,
+ List<ClientInstance> instances,
+ Health health,
+ List<GroupListItem> configurations) {
+
+ public static GroupDescribeOutput from(
+ ConsumerGroupVO source,
+ String cluster,
+ String requestedGroup,
+ List<SubscriptionEntryVO> subscriptionEntries,
+ Health health) {
+ return from(source, cluster, requestedGroup, subscriptionEntries, health, List.of(source));
+ }
+
+ public static GroupDescribeOutput from(ConsumerGroupVO source, String cluster, String requestedGroup,
+ List<SubscriptionEntryVO> subscriptionEntries, Health health,
+ List<ConsumerGroupVO> configurations) {
+ List<ConsumerInstanceVO> sourceInstances = source.getInstances();
+ List<ClientInstance> clients = sourceInstances == null
+ ? List.of()
+ : sourceInstances.stream()
+ .filter(Objects::nonNull)
+ .map(ClientInstance::from)
+ .toList();
+ List<String> subscribedTopics = source.getSubscribedTopics();
+ List<SubscriptionEntryVO> entries = subscriptionEntries == null
+ ? List.of()
+ : subscriptionEntries;
+ return new GroupDescribeOutput(
+ cluster,
+ StringUtils.hasText(source.getName()) ? source.getName() : requestedGroup,
+ source.getNamespace(),
+ source.getInstanceId(),
+ source.getSubscriptionMode(),
+ source.getConsumeType(),
+ source.getOnlineInstances(),
+ source.getTotalLag(),
+ subscribedTopics == null ? List.of() : subscribedTopics,
+ source.getSubscriptionDataType(),
+ source.getDeliveryOrderType(),
+ configurations.size() == 1 ? source.getRetryMaxTimes() : null,
+ source.getDelaySeconds(),
+ entries.stream()
+ .filter(Objects::nonNull)
+ .map(Subscription::from)
+ .toList(),
+ clients,
+ health,
+ configurations.stream().map(GroupListItem::from).toList());
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record Subscription(
+ String topic,
+ String expression,
+ String type,
+ String filterMode,
+ String consistency) {
+
+ static Subscription from(SubscriptionEntryVO source) {
+ return new Subscription(
+ source.getTopic(),
+ source.getExpression(),
+ source.getType(),
+ source.getFilterMode(),
+ source.getConsistency());
+ }
+ }
+
+ public record Health(String status, List<String> reasons) {
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupInput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupInput.java
new file mode 100644
index 000000000..00de33ba3
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupInput.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
+
+import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
+import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+
+public record GroupInput(
+ String cluster,
+ String group,
+ String namespace,
+ SubscriptionMode subscriptionMode,
+ ConsumeType consumeType,
+ String subscriptionDataType,
+ String deliveryOrderType,
+ Integer retryMaxTimes,
+ Integer delaySeconds) {
+
+ public ConsumerGroupVO toConsumerGroupVO() {
+ ConsumerGroupVO vo = new ConsumerGroupVO();
+ vo.setName(group);
+ vo.setNamespace(namespace);
+ vo.setInstanceId(cluster);
+ vo.setSubscriptionDataType(subscriptionDataType);
+ vo.setDeliveryOrderType(deliveryOrderType);
+ if (retryMaxTimes != null) {
+ vo.setRetryMaxTimes(retryMaxTimes);
+ }
+ if (delaySeconds != null) {
+ vo.setDelaySeconds(delaySeconds);
+ }
+ vo.setSubscriptionMode(subscriptionMode);
+ vo.setConsumeType(consumeType);
+ return vo;
+ }
+
+ public ConsumerGroupVO mergeWith(ConsumerGroupVO current) {
+ ConsumerGroupVO merged = new ConsumerGroupVO();
+ org.springframework.beans.BeanUtils.copyProperties(current, merged);
+ if (namespace != null) merged.setNamespace(namespace);
+ if (subscriptionMode != null) merged.setSubscriptionMode(subscriptionMode);
+ if (consumeType != null) merged.setConsumeType(consumeType);
+ if (subscriptionDataType != null) merged.setSubscriptionDataType(subscriptionDataType);
+ if (deliveryOrderType != null) merged.setDeliveryOrderType(deliveryOrderType);
+ if (retryMaxTimes != null) merged.setRetryMaxTimes(retryMaxTimes);
+ if (delaySeconds != null) merged.setDelaySeconds(delaySeconds);
+ return merged;
+ }
+
+ public static GroupInput from(ConsumerGroupVO group) {
+ return new GroupInput(
+ group.getInstanceId(),
+ group.getName(),
+ group.getNamespace(),
+ group.getSubscriptionMode(),
+ group.getConsumeType(),
+ group.getSubscriptionDataType(),
+ group.getDeliveryOrderType(),
+ group.getRetryMaxTimes(),
+ group.getDelaySeconds());
+ }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListInput.java
index 2fcbb55d6..771cf6ddb 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record GroupListInput(String cluster, String search) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListItem.java
new file mode 100644
index 000000000..1790529b9
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListItem.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
+import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record GroupListItem(
+ String name,
+ String namespace,
+ String clusterId,
+ SubscriptionMode subscriptionMode,
+ ConsumeType consumeType,
+ int retryMaxTimes,
+ int onlineInstances,
+ long totalLag,
+ List<String> subscribedTopics) {
+
+ public static GroupListItem from(ConsumerGroupVO group) {
+ return new GroupListItem(
+ group.getName(),
+ group.getNamespace(),
+ group.getClusterId(),
+ group.getSubscriptionMode(),
+ group.getConsumeType(),
+ group.getRetryMaxTimes(),
+ group.getOnlineInstances(),
+ group.getTotalLag(),
+ group.getSubscribedTopics() != null ? group.getSubscribedTopics() : List.of());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupProgressOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupProgressOutput.java
new file mode 100644
index 000000000..a4719e036
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupProgressOutput.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record GroupProgressOutput(
+ String cluster,
+ String group,
+ String topic,
+ long totalLag,
+ List<QueueProgress> queues) {
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record QueueProgress(
+ String broker,
+ int queueId,
+ long brokerOffset,
+ long consumerOffset,
+ long lag) {
+
+ public static QueueProgress from(QueueProgressVO queue) {
+ if (queue == null) {
+ return null;
+ }
+ return new QueueProgress(
+ queue.getBroker(),
+ queue.getQueueId(),
+ queue.getBrokerOffset(),
+ queue.getConsumerOffset(),
+ queue.getDiffTotal());
+ }
+ }
+
+ public static GroupProgressOutput from(String cluster, String group, String topic, List<QueueProgressVO> progress) {
+ List<QueueProgressVO> filtered = progress;
+ if (topic != null && !topic.isBlank()) {
+ filtered = (progress == null ? List.<QueueProgressVO>of() : progress).stream()
+ .filter(q -> topic.equals(q.getTopic()))
+ .toList();
+ }
+ List<QueueProgress> queues = (filtered == null ? List.<QueueProgressVO>of() : filtered).stream()
+ .map(QueueProgress::from)
+ .toList();
+ long totalLag = queues.stream().anyMatch(queue -> queue.lag() == -1L)
+ ? -1L
+ : queues.stream().mapToLong(QueueProgress::lag).sum();
+ return new GroupProgressOutput(
+ cluster,
+ group,
+ topic,
+ totalLag,
+ queues);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupResetOffsetInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupResetOffsetInput.java
index 2fcbb55d6..b85469bbc 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupResetOffsetInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record GroupResetOffsetInput(String cluster, String group, String topic, long timestamp) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupTopicInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupTopicInput.java
index 2fcbb55d6..d6a9caca4 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupTopicInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record GroupTopicInput(String cluster, String group, String topic) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/ResetOffsetOutput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/ResetOffsetOutput.java
index 2fcbb55d6..11f9dd0b0 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/ResetOffsetOutput.java
@@ -14,16 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
-import lombok.Getter;
+public record ResetOffsetOutput(String group, String topic, long timestamp, boolean reset) {
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ public ResetOffsetOutput(String group, String topic, long timestamp) {
+ this(group, topic, timestamp, true);
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/SkipAccumulatedOutput.java
similarity index 67%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/SkipAccumulatedOutput.java
index 2fcbb55d6..72e9075ec 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/SkipAccumulatedOutput.java
@@ -1,6 +1,6 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
+ * contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
@@ -14,16 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
-import lombok.Getter;
+import java.util.List;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record SkipAccumulatedOutput(String group, List<String> topics) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/DlqMessageItem.java
similarity index 51%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/DlqMessageItem.java
index 141b1cef2..3404bc4e1 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/DlqMessageItem.java
@@ -14,26 +14,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
- }
-
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.dlq.DLQMessageVO;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record DlqMessageItem(
+ String msgId,
+ String topic,
+ String key,
+ long storeTime,
+ int queueId,
+ long offset,
+ String body) {
+
+ public static DlqMessageItem from(DLQMessageVO message) {
+ return new DlqMessageItem(
+ message.getMsgId(),
+ message.getTopic(),
+ message.getKeys(),
+ message.getStoreTime(),
+ message.getQueueId(),
+ message.getOffset(),
+ message.getBody());
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageItem.java
new file mode 100644
index 000000000..e3bca48a1
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageItem.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.message.MessageRecordVO;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record MessageItem(
+ String msgId,
+ String topic,
+ String tag,
+ String key,
+ long storeTime,
+ String storeHost,
+ String bornHost,
+ String body,
+ String bodyEncoding,
+ boolean bodyTruncated,
+ int size) {
+
+ public static MessageItem from(MessageRecordVO message) {
+ return new MessageItem(
+ message.getMsgId(),
+ message.getTopic(),
+ message.getTag(),
+ message.getKey(),
+ message.getStoreTime(),
+ message.getStoreHost(),
+ message.getBornHost(),
+ message.getBody(),
+ message.getBodyEncoding(),
+ message.isBodyTruncated(),
+ message.getSize());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByIdInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByIdInput.java
index 2fcbb55d6..815f098ff 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByIdInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record MessageQueryByIdInput(String cluster, String msgId, String topic) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByKeyInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByKeyInput.java
index 2fcbb55d6..053c7bb4e 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByKeyInput.java
@@ -14,16 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record MessageQueryByKeyInput(
+ String cluster,
+ String key,
+ String topic,
+ Long startTime,
+ Long endTime) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByTopicInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByTopicInput.java
index 2fcbb55d6..57a50067c 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByTopicInput.java
@@ -14,16 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record MessageQueryByTopicInput(
+ String cluster,
+ String topic,
+ String tag,
+ Long startTime,
+ Long endTime) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendInput.java
similarity index 67%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendInput.java
index 2fcbb55d6..fb92f5b8e 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendInput.java
@@ -1,6 +1,6 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
+ * contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
@@ -14,16 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record MessageResendInput(
+ String cluster,
+ String msgId,
+ String sourceTopic,
+ String targetTopic) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendOutput.java
similarity index 65%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendOutput.java
index 2fcbb55d6..48e448619 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendOutput.java
@@ -1,6 +1,6 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
+ * contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
@@ -14,16 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
-import lombok.Getter;
+import com.fasterxml.jackson.annotation.JsonProperty;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
+public record MessageResendOutput(
+ String originalMsgId,
+ String newMsgId,
+ String targetTopic) {
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ @JsonProperty("resent")
+ public boolean resent() {
+ return true;
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageTraceOutput.java
similarity index 58%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageTraceOutput.java
index 141b1cef2..c3695b506 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageTraceOutput.java
@@ -14,26 +14,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
+import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.List;
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record MessageTraceOutput(
+ String msgId,
+ List<Node> nodes,
+ List<ConsumerStatus> consumerStatus) {
+
+ public record Node(
+ String title,
+ long timestamp,
+ String status,
+ long costTime,
+ String description) {
}
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+ public record ConsumerStatus(
+ String group,
+ String deliveryStatus,
+ long consumeTime,
+ int retryCount) {
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditItem.java
new file mode 100644
index 000000000..04e66c082
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditItem.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.ops;
+
+import org.apache.rocketmq.studio.ops.audit.AuditRecordVO;
+
+public record AuditItem(
+ Long id,
+ String timestamp,
+ String operator,
+ String operationType,
+ String resourceType,
+ String target,
+ String clusterId,
+ String detail,
+ String result,
+ String errorMessage) {
+
+ public static AuditItem from(AuditRecordVO record) {
+ return new AuditItem(
+ record.getId(),
+ record.getTimestamp() == null ? null : record.getTimestamp().toString(),
+ record.getOperator(),
+ record.getOperationType(),
+ record.getResourceType(),
+ record.getTarget(),
+ record.getClusterId(),
+ record.getDetail(),
+ record.getResult(),
+ record.getErrorMessage());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditListInput.java
similarity index 62%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditListInput.java
index 2fcbb55d6..e4f1bfc7d 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditListInput.java
@@ -14,16 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.ops;
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.PageRequest;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
+public record AuditListInput(
+ String search,
+ String operationType,
+ String resourceType,
+ String cluster,
+ String startDate,
+ String endDate,
+ String result,
+ PageRequest page) {
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ public AuditListInput {
+ if (page == null) {
+ page = new PageRequest(0, 0);
+ }
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/MetricsQueryInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/MetricsQueryInput.java
index 2fcbb55d6..fb14c9839 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/MetricsQueryInput.java
@@ -14,16 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.ops;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record MetricsQueryInput(
+ String cluster,
+ String metric,
+ String profileId,
+ String semanticMetric,
+ long start,
+ long end,
+ String step) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/PlanDescription.java
similarity index 58%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/PlanDescription.java
index 141b1cef2..c75033934 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/PlanDescription.java
@@ -14,26 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
-
+package org.apache.rocketmq.studio.ops.ai.tool.contract.plan;
import java.util.List;
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
+/** Fixed operation text; each preview supplies its own state and computed effects. */
+public record PlanDescription(String summary, List<String> impact, List<String> warnings) {
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
+ public PlanDescription {
+ impact = List.copyOf(impact);
+ warnings = List.copyOf(warnings);
}
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+ public ToolPlan.Builder builder(Object... args) {
+ ToolPlan.Builder builder = ToolPlan.builder(summary.formatted(args));
+ impact.forEach(builder::impact);
+ warnings.forEach(builder::warning);
+ return builder;
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/ToolPlan.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/ToolPlan.java
new file mode 100644
index 000000000..baed1d321
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/ToolPlan.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.plan;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public record ToolPlan(
+ String summary,
+ List<String> impact,
+ Map<String, Object> before,
+ Map<String, Object> after,
+ List<String> warnings) {
+
+ private static final ObjectMapper STATE_MAPPER = new ObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ public ToolPlan {
+ impact = List.copyOf(impact);
+ before = immutableState(before);
+ after = immutableState(after);
+ warnings = List.copyOf(warnings);
+ }
+
+ public ToolPlan withWarning(String warning) {
+ List<String> updated = new ArrayList<>(warnings);
+ updated.add(warning);
+ return new ToolPlan(summary, impact, before, after, updated);
+ }
+
+ public <T> T before(Class<T> stateType) {
+ return STATE_MAPPER.convertValue(before, stateType);
+ }
+
+ public <T> T after(Class<T> stateType) {
+ return STATE_MAPPER.convertValue(after, stateType);
+ }
+
+ public static Builder builder(String summary) {
+ return new Builder(summary);
+ }
+
+ private static <K> Map<K, Object> immutableState(Map<K, ?> state) {
+ Map<K, Object> snapshot = new LinkedHashMap<>();
+ state.forEach((key, value) -> snapshot.put(key, immutableValue(value)));
+ return Collections.unmodifiableMap(snapshot);
+ }
+
+ private static Object immutableValue(Object value) {
+ if (value instanceof Map<?, ?> map) {
+ return immutableState(map);
+ }
+ if (value instanceof List<?> list) {
+ return list.stream().map(ToolPlan::immutableValue).toList();
+ }
+ return value;
+ }
+
+ private static Map<String, Object> objectState(Object value) {
+ if (value == null) {
+ return Map.of();
+ }
+ Map<String, Object> state = STATE_MAPPER.convertValue(
+ value, new TypeReference<LinkedHashMap<String, Object>>() { });
+ state.values().removeIf(Objects::isNull);
+ return state;
+ }
+
+ public static final class Builder {
+
+ private final String summary;
+ private Map<String, Object> before = Map.of();
+ private Map<String, Object> after = Map.of();
+ private final List<String> impact = new ArrayList<>();
+ private final List<String> warnings = new ArrayList<>();
+
+ private Builder(String summary) {
+ this.summary = summary;
+ }
+
+ public Builder before(Object value) {
+ before = objectState(value);
+ return this;
+ }
+
+ public Builder after(Object value) {
+ after = objectState(value);
+ return this;
+ }
+
+ public Builder impact(String value) {
+ impact.add(value);
+ return this;
+ }
+
+ public Builder warning(String value) {
+ warnings.add(value);
+ return this;
+ }
+
+ public Builder warnings(List<String> values) {
+ warnings.addAll(values);
+ return this;
+ }
+
+ public Builder warningIf(boolean condition, String value) {
+ return condition ? warning(value) : this;
+ }
+
+ public ToolPlan build() {
+ return new ToolPlan(summary, impact, before, after, warnings);
+ }
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateInput.java
index 2fcbb55d6..6de5fdeb1 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.proxy;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record ProxyConfigUpdateInput(String cluster, String addr) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateOutput.java
similarity index 69%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateOutput.java
index 2fcbb55d6..28bb8561a 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateOutput.java
@@ -14,16 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.proxy;
-import lombok.Getter;
+import com.fasterxml.jackson.annotation.JsonInclude;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ProxyConfigUpdateOutput(
+ boolean reloaded,
+ String addr) {
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ public static ProxyConfigUpdateOutput of(String addr) {
+ return new ProxyConfigUpdateOutput(true, addr);
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/LiteTopicListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/LiteTopicListInput.java
index 2fcbb55d6..f68bb4bf8 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/LiteTopicListInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record LiteTopicListInput(String cluster, String pattern) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeInput.java
index 2fcbb55d6..603f29e29 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record TopicDescribeInput(String cluster, String topic, String namespace) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeOutput.java
new file mode 100644
index 000000000..60937d26b
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeOutput.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+import org.apache.rocketmq.studio.instance.topic.BrokerRouteVO;
+import org.apache.rocketmq.studio.instance.topic.TopicConsumerVO;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+
+import java.util.List;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicDescribeOutput(
+ String cluster,
+ String name,
+ String namespace,
+ String clusterId,
+ TopicType type,
+ int writeQueues,
+ int readQueues,
+ TopicPerm perm,
+ long messageCount,
+ double tps,
+ int consumerGroupCount,
+ String remark,
+ List<ConsumerGroup> consumerGroups,
+ List<Route> routes) {
+
+ public static TopicDescribeOutput from(
+ TopicVO topic, String cluster,
+ List<TopicConsumerVO> consumers,
+ List<BrokerRouteVO> routes) {
+ List<ConsumerGroup> groups = consumers == null
+ ? List.of()
+ : consumers.stream().map(ConsumerGroup::from).toList();
+ List<Route> routeList = routes == null
+ ? List.of()
+ : routes.stream().map(Route::from).toList();
+ return new TopicDescribeOutput(
+ cluster,
+ topic.getName(),
+ topic.getNamespace(),
+ topic.getClusterId(),
+ topic.getType(),
+ topic.getWriteQueues(),
+ topic.getReadQueues(),
+ topic.getPerm(),
+ topic.getMessageCount(),
+ topic.getTps(),
+ topic.getConsumerGroupCount(),
+ topic.getRemark(),
+ groups,
+ routeList);
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record ConsumerGroup(
+ String group,
+ String consumeType,
+ String messageModel,
+ double consumeTps,
+ long diffTotal,
+ boolean metricsAvailable) {
+
+ static ConsumerGroup from(TopicConsumerVO source) {
+ return new ConsumerGroup(
+ source.getGroup(),
+ source.getConsumeType() != null ? source.getConsumeType().name() : null,
+ source.getMessageModel(),
+ source.getConsumeTps(),
+ source.getDiffTotal(),
+ source.isMetricsAvailable());
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record Route(
+ String brokerName,
+ String brokerAddr,
+ String masterAddr,
+ Map<Long, String> brokerAddrs,
+ List<Long> brokerIds,
+ int replicaCount,
+ int writeQueues,
+ int readQueues,
+ String perm,
+ int permCode,
+ boolean readable,
+ boolean writable,
+ int topicSysFlag) {
+
+ static Route from(BrokerRouteVO source) {
+ return new Route(
+ source.getBrokerName(),
+ source.getBrokerAddr(),
+ source.getMasterAddr(),
+ source.getBrokerAddrs(),
+ source.getBrokerIds(),
+ source.getReplicaCount(),
+ source.getWriteQueues(),
+ source.getReadQueues(),
+ source.getPerm() != null ? source.getPerm().name() : null,
+ source.getPermCode(),
+ source.isReadable(),
+ source.isWritable(),
+ source.getTopicSysFlag());
+ }
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicInput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicInput.java
new file mode 100644
index 000000000..665d9e730
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicInput.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+
+public record TopicInput(
+ String cluster,
+ String topic,
+ String namespace,
+ TopicType type,
+ Integer writeQueues,
+ Integer readQueues,
+ TopicPerm perm,
+ String remark) {
+
+ private static final int DEFAULT_QUEUE_COUNT = 8;
+ private static final TopicType DEFAULT_TYPE = TopicType.NORMAL;
+ private static final TopicPerm DEFAULT_PERM = TopicPerm.RW;
+
+ public TopicVO toTopicVO() {
+ TopicVO vo = new TopicVO();
+ vo.setName(topic);
+ vo.setInstanceId(cluster);
+ vo.setNamespace(namespace);
+ vo.setRemark(remark);
+ vo.setWriteQueues(writeQueues != null ? writeQueues : DEFAULT_QUEUE_COUNT);
+ vo.setReadQueues(readQueues != null ? readQueues : DEFAULT_QUEUE_COUNT);
+ vo.setType(type != null ? type : DEFAULT_TYPE);
+ vo.setPerm(perm != null ? perm : DEFAULT_PERM);
+ return vo;
+ }
+
+ public static TopicInput from(TopicVO topic) {
+ return new TopicInput(
+ topic.getInstanceId(),
+ topic.getName(),
+ topic.getNamespace(),
+ topic.getType(),
+ topic.getWriteQueues(),
+ topic.getReadQueues(),
+ topic.getPerm(),
+ topic.getRemark());
+ }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListInput.java
index 2fcbb55d6..f58e4ca96 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record TopicListInput(String cluster, String type, String search) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListItem.java
new file mode 100644
index 000000000..838af6f22
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListItem.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicListItem(
+ String name,
+ String namespace,
+ String clusterId,
+ TopicType type,
+ int writeQueues,
+ int readQueues,
+ TopicPerm perm,
+ long messageCount,
+ double tps,
+ int consumerGroupCount) {
+
+ public static TopicListItem from(TopicVO topic) {
+ return new TopicListItem(
+ topic.getName(),
+ topic.getNamespace(),
+ topic.getClusterId(),
+ topic.getType(),
+ topic.getWriteQueues(),
+ topic.getReadQueues(),
+ topic.getPerm(),
+ topic.getMessageCount(),
+ topic.getTps(),
+ topic.getConsumerGroupCount());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicOutput.java
new file mode 100644
index 000000000..b2908d8d7
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicOutput.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicOutput(
+ String name,
+ String namespace,
+ String cluster,
+ String type,
+ int writeQueues,
+ int readQueues,
+ String perm,
+ String remark) {
+
+ public static TopicOutput from(TopicVO topic) {
+ return new TopicOutput(
+ topic.getName(),
+ topic.getNamespace(),
+ topic.getClusterId(),
+ topic.getType() != null ? topic.getType().name() : null,
+ topic.getWriteQueues(),
+ topic.getReadQueues(),
+ topic.getPerm() != null ? topic.getPerm().name() : null,
+ topic.getRemark());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteInput.java
index 2fcbb55d6..0840ac56d 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteInput.java
@@ -14,16 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record TopicRouteInput(String cluster, String topic, String namespace) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteItem.java
new file mode 100644
index 000000000..9c8befb96
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteItem.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.topic.BrokerRouteVO;
+
+import java.util.List;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicRouteItem(
+ String brokerName,
+ String brokerAddr,
+ String masterAddr,
+ Map<Long, String> brokerAddrs,
+ List<Long> brokerIds,
+ int replicaCount,
+ int writeQueues,
+ int readQueues,
+ String perm,
+ int permCode,
+ boolean readable,
+ boolean writable,
+ int topicSysFlag) {
+
+ public static TopicRouteItem from(BrokerRouteVO source) {
+ return new TopicRouteItem(
+ source.getBrokerName(),
+ source.getBrokerAddr(),
+ source.getMasterAddr(),
+ source.getBrokerAddrs(),
+ source.getBrokerIds(),
+ source.getReplicaCount(),
+ source.getWriteQueues(),
+ source.getReadQueues(),
+ source.getPerm() != null ? source.getPerm().name() : null,
+ source.getPermCode(),
+ source.isReadable(),
+ source.isWritable(),
+ source.getTopicSysFlag());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendInput.java
index 2fcbb55d6..59341b88c 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendInput.java
@@ -14,16 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
-
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+public record TopicSendInput(
+ String cluster,
+ String topic,
+ String tag,
+ String key,
+ String body,
+ String properties) {
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendOutput.java
similarity index 59%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendOutput.java
index 2fcbb55d6..d8c0c267e 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendOutput.java
@@ -14,16 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
-import lombok.Getter;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.topic.SendMessageVO;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicSendOutput(
+ String msgId,
+ String offsetMsgId,
+ long sendTime) {
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ public static TopicSendOutput from(SendMessageVO source) {
+ return new TopicSendOutput(
+ source.getMsgId(),
+ source.getOffsetMsgId(),
+ source.getSendTime());
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicUpdateInput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicUpdateInput.java
new file mode 100644
index 000000000..71afab917
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicUpdateInput.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+import org.springframework.beans.BeanUtils;
+
+/** Optional values stay absent; the input schema rejects explicit JSON nulls. */
+public record TopicUpdateInput(
+ String cluster,
+ String topic,
+ String namespace,
+ TopicType type,
+ Integer writeQueues,
+ Integer readQueues,
+ TopicPerm perm,
+ String remark) {
+
+ /** Applies supplied values to a copy of the current configuration, without creation defaults. */
+ public TopicVO mergeWith(TopicVO current) {
+ TopicVO merged = new TopicVO();
+ BeanUtils.copyProperties(current, merged);
+ if (namespace != null) {
+ merged.setNamespace(namespace);
+ }
+ if (type != null) {
+ merged.setType(type);
+ }
+ if (writeQueues != null) {
+ merged.setWriteQueues(writeQueues);
+ }
+ if (readQueues != null) {
+ merged.setReadQueues(readQueues);
+ }
+ if (perm != null) {
+ merged.setPerm(perm);
+ }
+ if (remark != null) {
+ merged.setRemark(remark);
+ }
+ return merged;
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolDefinition.java
similarity index 52%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolDefinition.java
index 141b1cef2..a54ae2f82 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolDefinition.java
@@ -14,26 +14,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
-
+package org.apache.rocketmq.studio.ops.ai.tool.core;
import java.util.List;
-
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
+import java.util.Map;
+
+public record ToolDefinition(
+ String name,
+ Cli cli,
+ String description,
+ ToolRiskLevel riskLevel,
+ String permission,
+ List<String> requiredCapabilities,
+ Map<String, Object> inputSchema,
+ Map<String, Object> outputSchema,
+ String viewHint,
+ boolean deprecated,
+ String replacement) {
+
+ public boolean isLowRiskReadOnly() {
+ return isReadOnly() && permission.endsWith(":read");
}
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
+ public boolean isReadOnly() {
+ return riskLevel == ToolRiskLevel.L1;
}
- ClusterVO refreshClusterDetail(String clusterId);
+ public boolean requiresReason() {
+ return riskLevel == ToolRiskLevel.L3;
+ }
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+ public record Cli(String resource, String verb) {
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java
new file mode 100644
index 000000000..74fcebf68
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.core;
+
+import org.springframework.http.HttpStatus;
+
+/**
+ * Tool error scenarios and their HTTP status, public error code and recovery hint.
+ * Multiple scenarios may share the same public code.
+ */
+public enum ToolError {
+
+ TOKEN_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "UNAVAILABLE",
+ "Tool confirmation tokens are unavailable because studio.ai.token-secret is not configured.",
+ "Configure studio.ai.token-secret or STUDIO_AI_TOKEN_SECRET with a base64-encoded key of at least 32 bytes."),
+
+ L3_DISABLED(HttpStatus.FORBIDDEN, "PERMISSION_DENIED",
+ "L3 tools are disabled by server policy.",
+ "Ask an administrator to enable studio.ai.allow-l3-tools before retrying."),
+
+ L3_APPROVAL_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "L3 tool is blocked by default and requires break_glass=true before execution: %s",
+ "Set break_glass=true after reviewing the preview plan."),
+
+ L3_REASON_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Tool requires a non-empty reason before L3 execution: %s",
+ "Provide a non-empty reason that explains the L3 operation."),
+
+ CONFIRMATION_TOKEN_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Tool requires confirm_token from a matching preview: %s",
+ "Run the tool with --dry-run, then retry without --dry-run and with the returned confirm_token."),
+
+ CONFIRMATION_TOKEN_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Tool confirm_token is invalid, expired, or does not match the tool, caller, Instance or preview input. Tool: %s",
+ "Run with --dry-run again and retry with its fresh token without changing the operation input."),
+
+ TOOL_NAME_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Tool name is required",
+ "Call tools/list and provide one of the returned tool names."),
+
+ TOOL_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+ "Tool not found: %s",
+ "Call tools/list to discover the available tool names."),
+
+ CLUSTER_TYPE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "TOOL_CAPABILITY_UNSUPPORTED",
+ "Cluster type is unavailable: %s",
+ "Refresh the cluster metadata or select a cluster with a known type."),
+
+ CAPABILITY_CLUSTER_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Capability lookup requires a cluster",
+ "Select a cluster and retry the tool call."),
+
+ INSTANCE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+ "Instance not found: %s",
+ "List the configured Instances and select an existing Instance."),
+
+ TOOL_CLUSTER_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "cluster must be a non-empty target name",
+ "Set cluster to a registered instance name or a configured physical cluster name."),
+
+ TOOL_TARGET_MISMATCH(HttpStatus.FORBIDDEN, "PERMISSION_DENIED",
+ "cluster does not match the authenticated cluster",
+ "Use the same cluster for authentication and tool arguments."),
+
+ TOOL_CAPABILITY_UNSUPPORTED(HttpStatus.BAD_REQUEST, "TOOL_CAPABILITY_UNSUPPORTED",
+ "Instance target does not support tool: %s. Call rmq.capabilities or select another target.",
+ "Call rmq.capabilities for the selected Instance or select another target."),
+
+ ADMIN_REQUIRED(HttpStatus.FORBIDDEN, "PERMISSION_DENIED",
+ "Admin permission required for tool: %s",
+ "Log in as an admin user, or use a low-risk read-only tool that does not require admin permission."),
+
+ INPUT_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Tool input validation failed for %s: %s",
+ "Correct the reported fields according to the tool input schema and retry."),
+
+ UNEXPECTED_EXECUTION_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR",
+ "Tool execution failed unexpectedly.",
+ "Retry once; if the failure persists, contact an administrator."),
+
+ ACL_ID_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "ACL id must be numeric: %s",
+ "Use the numeric ACL id returned by rmq.acl.list."),
+
+ ROLE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+ "Role not found: %s",
+ "List the roles for the selected Instance and choose an existing role."),
+
+ DLQ_GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+ "DLQ group not found: %s",
+ "Call rmq.dlq.list and select a group with retained dead-letter messages."),
+
+ DLQ_STATISTICS_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "UNAVAILABLE",
+ "DLQ statistics are unavailable for group: %s",
+ "Restore broker connectivity and preview again before clearing the DLQ."),
+
+ OFFSET_RESET_PREVIEW_UNSAFE(HttpStatus.CONFLICT, "CONFLICT",
+ "Consumer offset reset preview is incomplete or unsafe for group: %s",
+ "Inspect the preview warnings and target, correct them, then preview again."),
+
+ RESEND_TARGET_TOPIC_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "target topic is required when the source message has no topic",
+ "Provide targetTopic explicitly and retry the preview."),
+
+ PROXY_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+ "Proxy not found in cluster %s: %s",
+ "Call rmq.proxy.list and select an address returned for this cluster."),
+
+ MESSAGE_PROPERTIES_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "properties must be a JSON object string of string key-value pairs",
+ "Provide properties as a JSON string such as {\"key1\":\"value1\"} and retry."),
+
+ TOOL_PAYLOAD_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "%s",
+ "Correct the tool name or arguments and retry the tool call."),
+
+ TOOL_CALL_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Tool call request is required",
+ "Provide a JSON object containing name and arguments."),
+
+ MCP_AUTHENTICATION_REQUIRED(HttpStatus.UNAUTHORIZED, "UNAUTHENTICATED",
+ "MCP authentication is required",
+ "Configure valid MCP credentials and retry the tool call."),
+
+ MCP_AUTHENTICATION_CONTEXT_MISSING(HttpStatus.UNAUTHORIZED, "UNAUTHENTICATED",
+ "MCP transport authentication context is missing.",
+ "Configure valid MCP credentials and retry the tool call."),
+
+ MCP_AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "UNAUTHENTICATED",
+ "%s",
+ "Configure valid MCP credentials and retry the MCP request."),
+
+ MCP_AUTHENTICATION_INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR",
+ "MCP authentication failed unexpectedly.",
+ "Retry once; if the failure persists, contact an administrator."),
+
+ REQUEST_BODY_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Tool request body is invalid.",
+ "Correct the tool request and retry."),
+
+ REQUEST_PARAMETER_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Missing required tool parameter: %s",
+ "Correct the tool request and retry."),
+
+ REQUEST_PARAMETER_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+ "Invalid tool parameter: %s",
+ "Correct the tool request and retry."),
+
+ EXECUTION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "TOOL_EXECUTION_FAILED",
+ "%s",
+ "Review the failure message; no specific recovery action is available.");
+
+ private final HttpStatus httpStatus;
+ private final String code;
+ private final String messageTemplate;
+ private final String hint;
+
+ ToolError(HttpStatus httpStatus, String code, String messageTemplate, String hint) {
+ this.httpStatus = httpStatus;
+ this.code = code;
+ this.messageTemplate = messageTemplate;
+ this.hint = hint;
+ }
+
+ public ToolExecutionException exception(Object... arguments) {
+ return new ToolExecutionException(httpStatus, code, message(arguments), hint);
+ }
+
+ public HttpStatus httpStatus() {
+ return httpStatus;
+ }
+
+ public String code() {
+ return code;
+ }
+
+ public String message(Object... arguments) {
+ return messageTemplate.formatted(arguments);
+ }
+
+ public String hint() {
+ return hint;
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionContext.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionContext.java
new file mode 100644
index 000000000..f1c73c695
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionContext.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.core;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+public record ToolExecutionContext(
+ String cluster,
+ ToolDefinition definition,
+ Map<String, Object> input,
+ String principal) {
+
+ private static final ObjectMapper INPUT_MAPPER = new ObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ private static final Set<String> CONTROL_FIELDS = Set.of(
+ "break_glass",
+ "confirm_token",
+ "dry_run",
+ "reason");
+
+ public ToolExecutionContext {
+ input = input == null
+ ? Map.of()
+ : Collections.unmodifiableMap(new LinkedHashMap<>(input));
+ }
+
+ public static ToolExecutionContext of(String cluster, ToolDefinition definition, Map<String, Object> input) {
+ return new ToolExecutionContext(cluster, definition, input, null);
+ }
+
+ public static ToolExecutionContext of(
+ String cluster,
+ ToolDefinition definition,
+ Map<String, Object> input,
+ String principal) {
+ return new ToolExecutionContext(cluster, definition, input, principal);
+ }
+
+ public String operationType() {
+ return definition.cli().verb().replace('-', '_').toUpperCase(Locale.ROOT) + "_" + resourceType();
+ }
+
+ public String resourceType() {
+ return definition.cli().resource().replace('-', '_').toUpperCase(Locale.ROOT);
+ }
+
+ /**
+ * Returns business fields without changing their values.
+ */
+ public Map<String, Object> businessInput() {
+ if (input.isEmpty()) {
+ return Map.of();
+ }
+ Map<String, Object> business = new LinkedHashMap<>(input);
+ CONTROL_FIELDS.forEach(business::remove);
+ return Collections.unmodifiableMap(business);
+ }
+
+ /**
+ * Converts business fields into the tool's input record.
+ */
+ @SuppressWarnings("unchecked")
+ public <I> I convertInput(Class<I> inputType) {
+ Map<String, Object> business = businessInput();
+ if (inputType == Map.class) {
+ return (I) business;
+ }
+ return INPUT_MAPPER.convertValue(business, inputType);
+ }
+
+ /**
+ * Resolves the {@code dry_run} control field from the raw input.
+ */
+ public boolean dryRun() {
+ return booleanInput("dry_run");
+ }
+
+ public boolean breakGlass() {
+ return booleanInput("break_glass");
+ }
+
+ public String reason() {
+ return input.get("reason") instanceof String text ? text : null;
+ }
+
+ public String confirmToken() {
+ return input.get("confirm_token") instanceof String text ? text : null;
+ }
+
+ private boolean booleanInput(String field) {
+ Object value = input.get(field);
+ if (value instanceof Boolean bool) {
+ return bool;
+ }
+ return value instanceof String text && Boolean.parseBoolean(text);
+ }
+
+ /**
+ * Returns the cluster name, or throws when it is missing or blank.
+ */
+ public String requireCluster() {
+ if (cluster == null || cluster.isBlank()) {
+ throw ToolError.TOOL_CLUSTER_REQUIRED.exception();
+ }
+ return cluster;
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionException.java
new file mode 100644
index 000000000..9688c9140
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionException.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.core;
+
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.springframework.http.HttpStatus;
+
+public class ToolExecutionException extends BusinessException {
+
+ private final String errorCode;
+ private final String hint;
+
+ public ToolExecutionException(
+ HttpStatus httpStatus,
+ String errorCode,
+ String message,
+ String hint) {
+ super(httpStatus, message);
+ this.errorCode = errorCode;
+ this.hint = hint;
+ }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ public String getHint() {
+ return hint;
+ }
+
+ public static ToolExecutionException from(BusinessException exception) {
+ if (exception instanceof ToolExecutionException toolException) {
+ return toolException;
+ }
+ HttpStatus status = HttpStatus.resolve(exception.getCode());
+ if (status == null) {
+ status = HttpStatus.INTERNAL_SERVER_ERROR;
+ }
+ ToolExecutionException translated = new ToolExecutionException(
+ status,
+ ToolError.EXECUTION_FAILED.code(),
+ exception.getMessage(),
+ ToolError.EXECUTION_FAILED.hint());
+ translated.initCause(exception);
+ return translated;
+ }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionResult.java
similarity index 54%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionResult.java
index 141b1cef2..0513c5279 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionResult.java
@@ -14,26 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
- }
-
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
- }
-
- ClusterVO refreshClusterDetail(String clusterId);
+package org.apache.rocketmq.studio.ops.ai.tool.core;
+
+/**
+ * Carries both the business output and the final context after a filter chain
+ * completes. This replaces the shared {@code AtomicReference} that previously
+ * allowed upstream filters to read the latest context via {@code latestContext()}.
+ * Each filter receives the result from {@code chain.proceed()}, reads the
+ * updated context directly, and returns a new result to its caller.
+ */
+public record ToolExecutionResult(Object output, ToolExecutionContext context) {
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
+ public static ToolExecutionResult of(Object output, ToolExecutionContext context) {
+ return new ToolExecutionResult(output, context);
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolHandler.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolHandler.java
index 2fcbb55d6..2bf8ed224 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolHandler.java
@@ -14,16 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.core;
-import lombok.Getter;
+public interface ToolHandler<I, O> {
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
+ String name();
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
- }
+ Class<I> inputType();
+
+ O execute(I input, ToolExecutionContext context);
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolInvocation.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolInvocation.java
new file mode 100644
index 000000000..d4bba4abb
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolInvocation.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.core;
+
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+
+/**
+ * A tool call and its request context. Input conversion is deferred until the
+ * preview or terminal invocation so failures remain inside the validation and audit chain.
+ */
+public record ToolInvocation(ToolExecutionContext context, ToolHandler<?, ?> handler) {
+
+ public Object execute() {
+ return invoke(handler, context);
+ }
+
+ public ToolPlan preview() {
+ if (!(handler instanceof MutationToolHandler<?, ?> mutation)) {
+ throw new IllegalStateException("Mutation tool requires MutationToolHandler: "
+ + context.definition().name());
+ }
+ return preview(mutation, context);
+ }
+
+ private static <I> ToolPlan preview(MutationToolHandler<I, ?> handler, ToolExecutionContext context) {
+ I input = context.convertInput(handler.inputType());
+ return handler.preview(input, context);
+ }
+
+ private static <I, O> O invoke(ToolHandler<I, O> handler, ToolExecutionContext context) {
+ I input = context.convertInput(handler.inputType());
+ return handler.execute(input, context);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolRiskLevel.java
similarity index 69%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolRiskLevel.java
index 2fcbb55d6..76678fc4f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolRiskLevel.java
@@ -14,16 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.core;
-import lombok.Getter;
+public enum ToolRiskLevel {
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
+ L1("L1_READ_ONLY"),
+ L2("L2_MUTATION"),
+ L3("L3_HIGH_RISK");
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ private final String operationLevel;
+
+ ToolRiskLevel(String operationLevel) {
+ this.operationLevel = operationLevel;
+ }
+
+ public String operationLevel() {
+ return operationLevel;
}
+
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolAuditFilter.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolAuditFilter.java
new file mode 100644
index 000000000..ed307f1b0
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolAuditFilter.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.filter;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
+import org.apache.rocketmq.studio.ops.audit.AuditService;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class ToolAuditFilter implements ToolExecutionFilter {
+
+ private final AuditService auditService;
+
+ @Override
+ public Type type() {
+ return Type.AUDIT;
+ }
+
+ @Override
+ public Object filter(ToolInvocation invocation, Chain chain) {
+ ToolExecutionContext context = invocation.context();
+ Object output;
+ try {
+ output = chain.proceed(invocation);
+ } catch (Exception exception) {
+ this.record(context, "FAILED", exception.getMessage());
+ throw exception;
+ }
+ this.record(context, "SUCCESS", null);
+ return output;
+ }
+
+ private void record(ToolExecutionContext context, String result, String errorMessage) {
+ auditService.record(context.operationType(),
+ context.resourceType(),
+ context.definition().name(),
+ context.cluster(),
+ errorMessage,
+ result
+ );
+ }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolCapabilityFilter.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolCapabilityFilter.java
new file mode 100644
index 000000000..dd0b45e4b
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolCapabilityFilter.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.filter;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.CapabilityResolver;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+@Component
+@RequiredArgsConstructor
+public class ToolCapabilityFilter implements ToolExecutionFilter {
+
+ private final CapabilityResolver capabilityResolver;
+
+ @Override
+ public Type type() {
+ return Type.CAPABILITY;
+ }
+
+ @Override
+ public Object filter(ToolInvocation invocation, Chain chain) {
+ ToolExecutionContext context = invocation.context();
+ ToolDefinition definition = context.definition();
+ if (definition.requiredCapabilities().isEmpty()) {
+ return chain.proceed(invocation);
+ }
+
+ Set<String> capabilities = capabilityResolver.resolve(context.cluster());
+ if (!capabilities.containsAll(definition.requiredCapabilities())) {
+ throw ToolError.TOOL_CAPABILITY_UNSUPPORTED.exception(definition.name());
+ }
+ return chain.proceed(invocation);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolExecutionFilter.java
similarity index 66%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolExecutionFilter.java
index 2fcbb55d6..c1da3f5e8 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolExecutionFilter.java
@@ -14,16 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.filter;
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
-@Getter
-public class BusinessException extends RuntimeException {
- private final int code;
+public interface ToolExecutionFilter {
- public BusinessException(int code, String message) {
- super(message);
- this.code = code;
+ enum Type {
+ AUDIT,
+ VALIDATION,
+ CAPABILITY,
+ MUTATION
+ }
+
+ Type type();
+
+ Object filter(ToolInvocation invocation, Chain chain);
+
+ interface Chain {
+ Object proceed(ToolInvocation invocation);
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolFilterChain.java
similarity index 50%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolFilterChain.java
index 44b4a2fbf..93e0589e4 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolFilterChain.java
@@ -14,30 +14,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.ops.ai.tool;
+package org.apache.rocketmq.studio.ops.ai.tool.filter;
-import org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
-
-import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
import org.springframework.stereotype.Component;
-import java.util.Map;
+import java.util.Comparator;
+import java.util.List;
@Component
-@RequiredArgsConstructor
-public class NameServerConfigDiffToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.nameserver.config.diff";
-
- private final NameServerConfigDiffService configDiffService;
-
- @Override
- public String name() {
- return NAME;
+public final class ToolFilterChain {
+
+ private final ToolExecutionFilter.Chain chain;
+
+ public ToolFilterChain(List<ToolExecutionFilter> filters) {
+ List<ToolExecutionFilter> ordered = filters.stream()
+ .sorted(Comparator.comparing(ToolExecutionFilter::type))
+ .toList();
+ ToolExecutionFilter.Chain action = ToolInvocation::execute;
+ for (ToolExecutionFilter filter : ordered.reversed()) {
+ ToolExecutionFilter.Chain next = action;
+ action = invocation -> filter.filter(invocation, next);
+ }
+ this.chain = action;
}
- @Override
- public Object execute(Map<String, Object> input) {
- return configDiffService.compare((String) input.get("cluster"));
+ public Object execute(ToolInvocation invocation) {
+ return chain.proceed(invocation);
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilter.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilter.java
new file mode 100644
index 000000000..12013ac1b
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilter.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.filter;
+
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.MutationOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolTokenService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ToolMutationFilter implements ToolExecutionFilter {
+
+ private static final String L3_WARNING =
+ "This high-risk operation requires break-glass approval and an explicit reason.";
+
+ private final ToolTokenService tokenService;
+ private final boolean l3Enabled;
+
+ public ToolMutationFilter(
+ ToolTokenService tokenService,
+ @Value("${studio.ai.allow-l3-tools:false}") boolean l3Enabled) {
+ this.tokenService = tokenService;
+ this.l3Enabled = l3Enabled;
+ }
+
+ @Override
+ public Type type() {
+ return Type.MUTATION;
+ }
+
+ @Override
+ public Object filter(ToolInvocation invocation, Chain chain) {
+ ToolExecutionContext context = invocation.context();
+ if (context.definition().isReadOnly()) {
+ return chain.proceed(invocation);
+ }
+ if (!context.dryRun()) {
+ if (context.definition().requiresReason()) {
+ verifyL3Requirements(context);
+ }
+ tokenService.verify(context);
+ }
+
+ ToolPlan plan = invocation.preview();
+ if (context.definition().requiresReason()) {
+ plan = plan.withWarning(L3_WARNING);
+ }
+ if (context.dryRun()) {
+ String token = tokenService.issue(context);
+ return new MutationOutput<>(MutationOutput.Status.PLANNED, context.cluster(), plan, token, null);
+ }
+
+ Object result = chain.proceed(invocation);
+ return new MutationOutput<>(MutationOutput.Status.EXECUTED, context.cluster(), plan, null, result);
+ }
+
+ private void verifyL3Requirements(ToolExecutionContext context) {
+ ToolDefinition definition = context.definition();
+ if (!l3Enabled) {
+ throw ToolError.L3_DISABLED.exception();
+ }
+ if (!context.breakGlass()) {
+ throw ToolError.L3_APPROVAL_REQUIRED.exception(definition.name());
+ }
+ String reason = context.reason();
+ if (reason == null || reason.isBlank()) {
+ throw ToolError.L3_REASON_REQUIRED.exception(definition.name());
+ }
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolValidationFilter.java
similarity index 54%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolValidationFilter.java
index 44b4a2fbf..b6d2acb54 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolValidationFilter.java
@@ -14,30 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
+package org.apache.rocketmq.studio.ops.ai.tool.filter;
import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolSchemaValidator;
import org.springframework.stereotype.Component;
-import java.util.Map;
-
@Component
@RequiredArgsConstructor
-public class NameServerConfigDiffToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.nameserver.config.diff";
+public class ToolValidationFilter implements ToolExecutionFilter {
- private final NameServerConfigDiffService configDiffService;
+ private final ToolSchemaValidator schemaValidator;
@Override
- public String name() {
- return NAME;
+ public Type type() {
+ return Type.VALIDATION;
}
@Override
- public Object execute(Map<String, Object> input) {
- return configDiffService.compare((String) input.get("cluster"));
+ public Object filter(ToolInvocation invocation, Chain chain) {
+ ToolExecutionContext context = invocation.context();
+ schemaValidator.validateInput(context.definition(), context.input());
+ Object result = chain.proceed(invocation);
+ schemaValidator.validateOutput(context.definition(), result);
+ return result;
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/CapabilitiesToolHandler.java
similarity index 51%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/CapabilitiesToolHandler.java
index 44b4a2fbf..176a1a8a7 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/CapabilitiesToolHandler.java
@@ -14,30 +14,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
+package org.apache.rocketmq.studio.ops.ai.tool.handler;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.CapabilityResolver;
import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.CapabilitiesOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
import org.springframework.stereotype.Component;
-import java.util.Map;
-
@Component
@RequiredArgsConstructor
-public class NameServerConfigDiffToolHandler implements ToolHandler {
+public class CapabilitiesToolHandler implements ToolHandler<ClusterInput, CapabilitiesOutput> {
- private static final String NAME = "rmq.nameserver.config.diff";
-
- private final NameServerConfigDiffService configDiffService;
+ private final CapabilityResolver capabilityResolver;
@Override
public String name() {
- return NAME;
+ return "rmq.capabilities";
}
@Override
- public Object execute(Map<String, Object> input) {
- return configDiffService.compare((String) input.get("cluster"));
+ public Class<ClusterInput> inputType() {
+ return ClusterInput.class;
}
+
+ @Override
+ public CapabilitiesOutput execute(ClusterInput input, ToolExecutionContext context) {
+ return new CapabilitiesOutput(context.cluster(),
+ capabilityResolver.resolve(context.cluster()).stream().sorted().toList());
+ }
+
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/MutationToolHandler.java
similarity index 56%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/MutationToolHandler.java
index 141b1cef2..16ae3bcac 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/MutationToolHandler.java
@@ -14,26 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.tool.handler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
-import java.util.List;
+public abstract class MutationToolHandler<I, R>
+ implements ToolHandler<I, R> {
-public interface ClusterProvider {
+ private final Class<I> inputType;
- List<ClusterVO> discoverClusters();
-
- default List<ClusterVO> discoverClusters(String instanceId) {
- return discoverClusters();
+ protected MutationToolHandler(Class<I> inputType) {
+ this.inputType = inputType;
}
- default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
- return List.of();
+ @Override
+ public final Class<I> inputType() {
+ return inputType;
}
- ClusterVO refreshClusterDetail(String clusterId);
-
- default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
- return refreshClusterDetail(clusterId);
- }
+ public abstract ToolPlan preview(I input, ToolExecutionContext context);
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclCreateToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclCreateToolHandler.java
new file mode 100644
index 000000000..dfcb924df
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclCreateToolHandler.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclMutationInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class AclCreateToolHandler extends MutationToolHandler<AclMutationInput, AclRuleVO> {
+
+ private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+ "create ACL rule '%s:%s' in cluster '%s'.",
+ List.of("Adds a rule to the ACL policy evaluated for broker requests."),
+ List.of("The new ACL rule changes permissions for subsequent broker requests."));
+
+ private final AclService aclService;
+
+ public AclCreateToolHandler(AclService aclService) {
+ super(AclMutationInput.class);
+ this.aclService = aclService;
+ }
+
+ @Override
+ public String name() {
+ return "rmq.acl.create";
+ }
+
+ @Override
+ public ToolPlan preview(AclMutationInput input, ToolExecutionContext context) {
+ return PLAN_DESCRIPTION.builder(input.principal(), input.resource(), context.cluster())
+ .after(AclMutationInput.from(input.toRule(null)))
+ .build();
+ }
+
+ @Override
+ public AclRuleVO execute(AclMutationInput input, ToolExecutionContext context) {
+ return aclService.createRule(input.toRule(null), context.cluster());
+ }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclDeleteToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclDeleteToolHandler.java
new file mode 100644
index 000000000..59fe4367f
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclDeleteToolHandler.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ResourceDeleteInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclMutationInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class AclDeleteToolHandler extends MutationToolHandler<ResourceDeleteInput, Void> {
+
+ private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+ "delete ACL rule '%s' in cluster '%s'.",
+ List.of("Permanently removes the selected ACL rule."),
+ List.of("Removing this rule changes permissions for subsequent broker requests."));
+
+ private final AclService aclService;
+
+ public AclDeleteToolHandler(AclService aclService) {
+ super(ResourceDeleteInput.class);
+ this.aclService = aclService;
+ }
+
+ @Override
+ public String name() {
+ return "rmq.acl.delete";
+ }
+
+ @Override
+ public ToolPlan preview(ResourceDeleteInput input, ToolExecutionContext context) {
+ AclMutationInput before = AclMutationInput.from(
+ aclService.getRule(input.id(), context.cluster()));
+ return PLAN_DESCRIPTION.builder(input.id(), context.cluster())
+ .before(before)
+ .build();
+ }
+
+ @Override
+ public Void execute(ResourceDeleteInput input, ToolExecutionContext context) {
+ aclService.deleteRule(input.id(), context.cluster());
+ return null;
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclListToolHandler.java
new file mode 100644
index 000000000..5ec41fa3f
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclListToolHandler.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclRuleItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.PageOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclListInput;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class AclListToolHandler implements ToolHandler<AclListInput, PageOutput<AclRuleItem>> {
+
+ private final AclService aclService;
+
+ @Override
+ public String name() {
+ return "rmq.acl.list";
+ }
+
+ @Override
+ public Class<AclListInput> inputType() {
+ return AclListInput.class;
+ }
+
+ @Override
+ public PageOutput<AclRuleItem> execute(AclListInput input, ToolExecutionContext context) {
+ PageResult<AclRuleVO> result = aclService.listRules(
+ input.principal(), input.resource(), input.scope(),
+ input.decision(), input.aclVersion(), context.cluster(),
+ input.page().page(), input.page().pageSize());
+ return PageOutput.from(result, AclRuleItem::from);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java
new file mode 100644
index 000000000..0f278fed7
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclMutationInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class AclUpdateToolHandler extends MutationToolHandler<AclMutationInput, AclRuleVO> {
+
+ private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+ "update ACL rule '%s' in cluster '%s'.",
+ List.of("Replaces the selected ACL rule."),
+ List.of("The updated ACL rule changes permissions for subsequent broker requests."));
+
+ private final AclService aclService;
+
+ public AclUpdateToolHandler(AclService aclService) {
+ super(AclMutationInput.class);
+ this.aclService = aclService;
+ }
+
+ @Override
+ public String name() {
+ return "rmq.acl.update";
+ }
+
+ @Override
+ public ToolPlan preview(AclMutationInput input, ToolExecutionContext context) {
+ String instanceId = context.cluster();
+ AclMutationInput before = AclMutationInput.from(aclService.getRule(input.id(), instanceId));
+ AclMutationInput after = AclMutationInput.from(input.toRule(parseId(input.id())));
+ return PLAN_DESCRIPTION.builder(input.id(), context.cluster())
+ .before(before)
+ .after(after)
+ .warningIf(before.equals(after),
+ "The requested ACL rule already matches the current state.")
+ .build();
+ }
+
+ @Override
+ public AclRuleVO execute(AclMutationInput input, ToolExecutionContext context) {
+ return aclService.updateRule(input.toRule(parseId(input.id())), context.cluster());
+ }
+
+ private static Long parseId(String id) {
+ if (id == null || id.isBlank()) {
+ throw ToolError.ACL_ID_INVALID.exception(id);
+ }
+ try {
+ return Long.parseLong(id);
+ } catch (NumberFormatException e) {
+ throw ToolError.ACL_ID_INVALID.exception(id);
+ }
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserCreateToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserCreateToolHandler.java
new file mode 100644
index 000000000..b8d18ddde
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserCreateToolHandler.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.instance.acl.AclUserVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclUserItem;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.UserCreateInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class UserCreateToolHandler extends MutationToolHandler<UserCreateInput, AclUserItem> {
+
+ private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+ "create user '%s' in cluster '%s'.",
+ List.of("Creates an ACL identity; credentials are generated only during apply."),
+ List.of());
+
+ private final AclService aclService;
+
+ public UserCreateToolHandler(AclService aclService) {
+ super(UserCreateInput.class);
+ this.aclService = aclService;
+ }
+
+ @Override
+ public String name() {
+ return "rmq.user.create";
+ }
+
+ @Override
+ public ToolPlan preview(UserCreateInput input, ToolExecutionContext context) {
+ String instanceId = context.cluster();
+ AclUserItem before = aclService.listUsers(instanceId).stream()
+ .filter(user -> input.username().equals(user.getUsername()))
+ .findFirst().map(AclUserItem::from).orElse(null);
+ AclUserItem after = new AclUserItem(
+ null, input.username(), Boolean.TRUE.equals(input.admin()), input.clusters());
+ return PLAN_DESCRIPTION.builder(input.username(), context.cluster())
+ .before(before)
+ .after(after)
+ .build();
+ }
+
+ @Override
+ public AclUserItem execute(UserCreateInput input, ToolExecutionContext context) {
+ AclUserVO user = AclUserVO.builder()
+ .username(input.username())
+ .admin(Boolean.TRUE.equals(input.admin()))
+ .clusters(input.clusters())
+ .build();
+ return AclUserItem.from(aclService.createUser(user, context.cluster()));
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserDeleteToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserDeleteToolHandler.java
new file mode 100644
index 000000000..91f78db41
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserDeleteToolHandler.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclUserItem;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ResourceDeleteInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class UserDeleteToolHandler extends MutationToolHandler<ResourceDeleteInput, Void> {
+
+ private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+ "delete user '%s' in cluster '%s'.",
+ List.of("Deletes the ACL identity and its generated credentials."),
+ List.of());
+
+ private final AclService aclService;
+
+ public UserDeleteToolHandler(AclService aclService) {
+ super(ResourceDeleteInput.class);
+ this.aclService = aclService;
+ }
+
+ @Override
+ public String name() {
+ return "rmq.user.delete";
+ }
+
+ @Override
+ public ToolPlan preview(ResourceDeleteInput input, ToolExecutionContext context) {
+ AclUserItem before = AclUserItem.from(aclService.getUser(
+ input.id(), context.cluster()));
+ return PLAN_DESCRIPTION.builder(input.id(), context.cluster())
+ .before(before)
+ .build();
+ }
+
+ @Override
+ public Void execute(ResourceDeleteInput input, ToolExecutionContext context) {
+ aclService.deleteUser(input.id(), context.cluster());
+ return null;
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserListToolHandler.java
similarity index 50%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserListToolHandler.java
index 44b4a2fbf..81d53ee2f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserListToolHandler.java
@@ -14,30 +14,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
+package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclUserItem;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
import org.springframework.stereotype.Component;
-import java.util.Map;
-
@Component
@RequiredArgsConstructor
-public class NameServerConfigDiffToolHandler implements ToolHandler {
-
- private static final String NAME = "rmq.nameserver.config.diff";
-
- private final NameServerConfigDiffService configDiffService;
+public class UserListToolHandler implements ToolHandler<ClusterInput, ListOutput<AclUserItem>> {
+ private final AclService aclService;
@Override
public String name() {
- return NAME;
+ return "rmq.user.list";
+ }
+
+ @Override
+ public Class<ClusterInput> inputType() {
+ return ClusterInput.class;
}
@Override
- public Object execute(Map<String, Object> input) {
- return configDiffService.compare((String) input.get("cluster"));
+ public ListOutput<AclUserItem> execute(
+ ClusterInput input, ToolExecutionContext context) {
+ return new ListOutput<>(aclService.listUsers(context.cluster()).stream()
+ .map(AclUserItem::from).toList());
}
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/alert/AlertRuleListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/alert/AlertRuleListToolHandler.java
new file mode 100644
index 000000000..c7eccf10c
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/alert/AlertRuleListToolHandler.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.alert;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.alert.AlertRuleListInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.alert.AlertRuleListItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
+import org.apache.rocketmq.studio.ops.alert.AlertService;
+import org.springframework.stereotype.Component;
+
+import java.util.Locale;
+
+@Component
+@RequiredArgsConstructor
+public class AlertRuleListToolHandler
+ implements ToolHandler<AlertRuleListInput, ListOutput<AlertRuleListItem>> {
+
+ private final AlertService alertService;
+
+ @Override
+ public String name() {
+ return "rmq.alert.rule.list";
+ }
+
+ @Override
+ public Class<AlertRuleListInput> inputType() {
+ return AlertRuleListInput.class;
+ }
+
+ @Override
+ public ListOutput<AlertRuleListItem> execute(
+ AlertRuleListInput input, ToolExecutionContext context) {
+ return new ListOutput<>(alertService.findRules(context.cluster()).stream()
+ .filter(rule -> matchesEnabled(rule, input.enabled()))
+ .filter(rule -> matchesSearch(rule, input.search()))
+ .map(AlertRuleListItem::from)
+ .toList());
+ }
+
+ private static boolean matchesEnabled(AlertRuleVO rule, Boolean enabled) {
+ return enabled == null || rule.isEnabled() == enabled;
+ }
+
+ private static boolean matchesSearch(AlertRuleVO rule, String search) {
+ if (search == null || search.isBlank()) {
+ return true;
+ }
+ String normalizedSearch = search.trim().toLowerCase(Locale.ROOT);
+ return contains(rule.getName(), normalizedSearch)
+ || contains(rule.getMetric(), normalizedSearch)
+ || contains(rule.getDescription(), normalizedSearch);
+ }
+
+ private static boolean contains(String value, String normalizedSearch) {
+ return value != null && value.toLowerCase(Locale.ROOT).contains(normalizedSearch);
+ }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigToolHandler.java
new file mode 100644
index 000000000..1441f1005
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigToolHandler.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.broker;
+
+import org.apache.rocketmq.studio.cluster.broker.BrokerConfigDiffService;
+import org.apache.rocketmq.studio.cluster.config.BrokerConfigDiffVO;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerConfigOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.BrokerClusterInput;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class BrokerConfigToolHandler implements ToolHandler<BrokerClusterInput, BrokerConfigOutput> {
+
+ private final BrokerConfigDiffService brokerConfigDiffService;
+
+ @Override
+ public String name() {
+ return "rmq.broker.config";
+ }
+
+ @Override
+ public Class<BrokerClusterInput> inputType() {
+ return BrokerClusterInput.class;
+ }
+
+ @Override
+ public BrokerConfigOutput execute(
+ BrokerClusterInput input, ToolExecutionContext context) {
+ BrokerConfigDiffVO diff = brokerConfigDiffService.compareForInstance(context.cluster());
+ return BrokerConfigOutput.from(diff, context.cluster());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigUpdateToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigUpdateToolHandler.java
new file mode 100644
index 000000000..be88e0fd6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigUpdateToolHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.broker;
+
+import org.apache.rocketmq.studio.cluster.broker.ClusterService;
+import org.apache.rocketmq.studio.cluster.config.ClusterConfigUpdateResultVO;
+import org.apache.rocketmq.studio.cluster.config.UpdateConfigDTO;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerConfigUpdateInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerConfigUpdateOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class BrokerConfigUpdateToolHandler extends MutationToolHandler<BrokerConfigUpdateInput, BrokerConfigUpdateOutput> {
+
+ private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+ "update Broker configuration in Instance '%s'.",
+ List.of(),
+ List.of());
+
+ private final ClusterService clusterService;
+
+ public BrokerConfigUpdateToolHandler(ClusterService clusterService) {
+ super(BrokerConfigUpdateInput.class);
+ this.clusterService = clusterService;
+ }
+
+ @Override
+ public String name() {
+ return "rmq.broker.config_update";
+ }
+
+ @Override
+ public ToolPlan preview(BrokerConfigUpdateInput input, ToolExecutionContext context) {
+ var current = clusterService.readBrokerConfigs(context.cluster());
+ var proposed = clusterService.proposeBrokerConfigs(toCommand(input, context.cluster()), current);
+ return PLAN_DESCRIPTION.builder(context.cluster())
+ .before(java.util.Map.of("brokers", current))
+ .after(java.util.Map.of("brokers", proposed))
+ .impact("Updates the requested configuration fields on " + current.size() + " master Broker(s).")
+ .build();
+ }
+
+ @Override
+ public BrokerConfigUpdateOutput execute(
+ BrokerConfigUpdateInput input, ToolExecutionContext context) {
+ ClusterConfigUpdateResultVO result = clusterService.updateClusterConfig(
+ toCommand(input, context.cluster()), context.cluster());
+ return BrokerConfigUpdateOutput.from(result);
+ }
+
+ private static UpdateConfigDTO toCommand(BrokerConfigUpdateInput input, String instanceId) {
+ return UpdateConfigDTO.builder()
+ .instanceId(instanceId)
+ .flushDiskType(input.flushDiskType())
+ .autoCreateTopicEnable(input.autoCreateTopicEnable())
+ .autoCreateSubscriptionGroup(input.autoCreateSubscriptionGroup())
+ .maxMessageSize(input.maxMessageSize())
+ .fileReservedTime(input.fileReservedTime())
+ .writeQueueNums(input.writeQueueNums())
+ .readQueueNums(input.readQueueNums())
+ .brokerPermission(input.brokerPermission())
+ .build();
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerDescribeToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerDescribeToolHandler.java
new file mode 100644
index 000000000..cbb49ecba
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerDescribeToolHandler.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.broker;
+
+import org.apache.rocketmq.studio.cluster.broker.BrokerVO;
+import org.apache.rocketmq.studio.cluster.broker.ClusterProvider;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerDescribeInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class BrokerDescribeToolHandler
+ implements ToolHandler<BrokerDescribeInput, ClusterListOutput<BrokerVO>> {
+
+ private final ClusterProvider clusterProvider;
+
+ @Override
+ public String name() {
+ return "rmq.broker.describe";
+ }
+
+ @Override
+ public Class<BrokerDescribeInput> inputType() {
+ return BrokerDescribeInput.class;
+ }
+
+ @Override
+ public ClusterListOutput<BrokerVO> execute(
+ BrokerDescribeInput input, ToolExecutionContext context) {
+ List<BrokerVO> matched = clusterProvider.discoverBrokers(context.cluster(), input.broker());
+ return new ClusterListOutput<>(context.cluster(), matched);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerListToolHandler.java
new file mode 100644
index 000000000..c465553b8
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerListToolHandler.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.broker;
+
+import org.apache.rocketmq.studio.cluster.broker.ClusterProvider;
+import org.apache.rocketmq.studio.cluster.broker.BrokerVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class BrokerListToolHandler
+ implements ToolHandler<ClusterInput, ClusterListOutput<BrokerVO>> {
+
+ private final ClusterProvider clusterProvider;
+
+ @Override
+ public String name() {
+ return "rmq.broker.list";
+ }
+
+ @Override
+ public Class<ClusterInput> inputType() {
+ return ClusterInput.class;
+ }
+
+ @Override
+ public ClusterListOutput<BrokerVO> execute(
+ ClusterInput input, ToolExecutionContext context) {
+ List<BrokerVO> brokers = clusterProvider.discoverBrokers(context.cluster(), null);
+ return new ClusterListOutput<>(context.cluster(), brokers);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerImpl.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerRuntimeInfoToolHandler.java
similarity index 50%
rename from server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerImpl.java
rename to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerRuntimeInfoToolHandler.java
index ebaf55f0e..afb1cd63d 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerImpl.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerRuntimeInfoToolHandler.java
@@ -14,50 +14,40 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.rocketmq.studio.ops.ai;
+package org.apache.rocketmq.studio.ops.ai.tool.handler.broker;
-import org.apache.rocketmq.studio.ops.ai.tool.ToolCatalog;
-import org.apache.rocketmq.studio.ops.ai.tool.ToolGatewayService;
+import org.apache.rocketmq.studio.cluster.broker.ClusterProvider;
+import org.apache.rocketmq.studio.cluster.broker.BrokerVO;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
-import java.util.Map;
@Component
@RequiredArgsConstructor
-public class McpServerImpl implements McpServerRegistry {
+public class BrokerRuntimeInfoToolHandler
+ implements ToolHandler<ClusterInput, BrokerRuntimeInfoToolHandler.Output> {
- private final ToolGatewayService toolGatewayService;
- private final ToolCatalog toolCatalog;
+ private final ClusterProvider clusterProvider;
@Override
- public List<AiToolVO> listTools() {
- return toolGatewayService.discover(null);
+ public String name() {
+ return "rmq.broker.runtime_info";
}
@Override
- public List<AiToolVO> listTools(String clusterId) {
- return toolGatewayService.discover(clusterId);
+ public Class<ClusterInput> inputType() {
+ return ClusterInput.class;
}
@Override
- public Object execute(String name, Map<String, Object> input) {
- return toolGatewayService.execute(name, input);
+ public Output execute(
+ ClusterInput input, ToolExecutionContext context) {
+ return new Output(context.cluster(), clusterProvider.discoverBrokers(context.cluster(), null));
}
- @Override
- public String catalogVersion() {
- return toolCatalog.getVersion();
- }
-
- @Override
- public String catalogDigest() {
- return toolCatalog.getDigest();
- }
-
- @Override
- public String minimumClientVersion() {
- return toolCatalog.getMinimumClientVersion();
- }
+ public record Output(String cluster, List<BrokerVO> brokers) { }
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientDescribeToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientDescribeToolHandler.java
new file mode 100644
index 000000000..790df4ed0
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientDescribeToolHandler.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.client;
+
+import org.apache.rocketmq.studio.cluster.client.ClientConnectionVO;
+import org.apache.rocketmq.studio.cluster.client.ClientService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.client.ClientDescribeInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class ClientDescribeToolHandler
+ implements ToolHandler<ClientDescribeInput, ClusterListOutput<ClientConnectionVO>> {
+
+ private final ClientService clientService;
+
+ @Override
+ public String name() {
+ return "rmq.client.describe";
+ }
+
+ @Override
+ public Class<ClientDescribeInput> inputType() {
+ return ClientDescribeInput.class;
+ }
+
+ @Override
+ public ClusterListOutput<ClientConnectionVO> execute(
+ ClientDescribeInput input, ToolExecutionContext context) {
+ String instanceId = context.cluster();
+ String clusterFilter = null;
+ return describe(input, instanceId, clusterFilter, context.cluster());
+ }
+
+ private ClusterListOutput<ClientConnectionVO> describe(
+ ClientDescribeInput input, String instanceId, String clusterFilter, String cluster) {
+ List<ClientConnectionVO> connections = clientService.listConnections(
+ instanceId, clusterFilter, input.type());
+ List<ClientConnectionVO> matched = connections.stream()
+ .filter(c -> input.clientId().equals(c.getClientId()))
+ .toList();
+ return new ClusterListOutput<>(cluster, matched);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientListToolHandler.java
new file mode 100644
index 000000000..066e7b2a6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientListToolHandler.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.client;
+
+import org.apache.rocketmq.studio.cluster.client.ClientConnectionVO;
+import org.apache.rocketmq.studio.cluster.client.ClientService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.client.ClientListInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class ClientListToolHandler
+ implements ToolHandler<ClientListInput, ClusterListOutput<ClientConnectionVO>> {
+
+ private final ClientService clientService;
+
+ @Override
+ public String name() {
+ return "rmq.client.list";
+ }
+
+ @Override
+ public Class<ClientListInput> inputType() {
+ return ClientListInput.class;
+ }
+
+ @Override
+ public ClusterListOutput<ClientConnectionVO> execute(
+ ClientListInput input, ToolExecutionContext context) {
+ String instanceId = context.cluster();
+ String clusterFilter = null;
+ return list(input, instanceId, clusterFilter, context.cluster());
+ }
+
+ private ClusterListOutput<ClientConnectionVO> list(
+ ClientListInput input, String instanceId, String clusterFilter, String cluster) {
+ List<ClientConnectionVO> connections = clientService.listConnections(
+ instanceId, clusterFilter, input.type());
+ return new ClusterListOutput<>(cluster, connections);
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/cluster/ClusterListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/cluster/ClusterListToolHandler.java
new file mode 100644
index 000000000..f376cea7e
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/cluster/ClusterListToolHandler.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.cluster;
+
+import org.apache.rocketmq.studio.ops.ai.tool.contract.cluster.ClusterListItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.cluster.ClusterListInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+
+import org.apache.rocketmq.studio.cluster.broker.ClusterService;
+import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class ClusterListToolHandler
+ implements ToolHandler<ClusterListInput, ListOutput<ClusterListItem>> {
+
+ private final ClusterService clusterService;
+
+ @Override
+ public String name() {
+ return "rmq.cluster.list";
+ }
+
+ @Override
+ public Class<ClusterListInput> inputType() {
+ return ClusterListInput.class;
+ }
+
+ @Override
+ public ListOutput<ClusterListItem> execute(
+ ClusterListInput input, ToolExecutionContext context) {
+ return new ListOutput<>(clusterService.listClusters(context.cluster()).stream()
+ .filter(cluster -> matchesStatus(cluster, input.status()))
+ .map(ClusterListItem::from)
+ .toList());
+ }
+
+ private static boolean matchesStatus(ClusterVO cluster, String status) {
+ return status == null
+ || cluster.getStatus() != null
+ && cluster.getStatus().name().equalsIgnoreCase(status.trim());
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dashboard/DashboardSummaryToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dashboard/DashboardSummaryToolHandler.java
new file mode 100644
index 000000000..c4b986647
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dashboard/DashboardSummaryToolHandler.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.dashboard;
+
+import org.apache.rocketmq.studio.ops.dashboard.ClusterOverviewVO;
+import org.apache.rocketmq.studio.ops.dashboard.DashboardDataVO;
+import org.apache.rocketmq.studio.ops.dashboard.DashboardService;
+import org.apache.rocketmq.studio.ops.dashboard.DashboardStatsVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class DashboardSummaryToolHandler
+ implements ToolHandler<ClusterInput, DashboardSummaryToolHandler.Output> {
+
+ private final DashboardService dashboardService;
+
+ @Override
+ public String name() {
+ return "rmq.dashboard.summary";
+ }
+
+ @Override
+ public Class<ClusterInput> inputType() {
+ return ClusterInput.class;
+ }
+
+ @Override
+ public Output execute(ClusterInput input, ToolExecutionContext context) {
+ DashboardDataVO dashboard = dashboardService.getDashboard(context.cluster());
+ return new Output(context.cluster(), clusters(dashboard).stream().map(DashboardSummaryToolHandler::clusterProjection).toList(),
+ statsProjection(dashboard == null ? null : dashboard.getStats()));
+ }
+
+ private static List<ClusterOverviewVO> clusters(DashboardDataVO dashboard) {
+ if (dashboard == null || dashboard.getClusters() == null) {
+ return List.of();
+ }
+ return dashboard.getClusters();
+ }
+
+ private static Cluster clusterProjection(ClusterOverviewVO cluster) {
+ return new Cluster(
+ cluster.getId(),
+ cluster.getName(),
+ cluster.getType() == null ? null : cluster.getType().name(),
+ cluster.getStatus() == null ? null : cluster.getStatus().name(),
+ cluster.getBrokers(),
+ cluster.getProxies(),
+ cluster.getTopics(),
+ cluster.getGroups(),
+ cluster.getTpsIn(),
+ cluster.getTpsOut(),
+ cluster.getVersion(),
+ cluster.getThroughput() == null ? List.of() : cluster.getThroughput());
+ }
+
+ private static Stats statsProjection(DashboardStatsVO stats) {
+ DashboardStatsVO safeStats = stats == null ? new DashboardStatsVO() : stats;
+ return new Stats(
+ safeStats.getTotalClusters(),
+ safeStats.getHealthyClusters(),
+ safeStats.getTotalBrokers(),
+ safeStats.getTotalProxies(),
+ safeStats.getTotalNameServers(),
+ safeStats.getTotalTopics(),
+ safeStats.getTotalConsumerGroups(),
+ safeStats.getTotalMessagesToday(),
+ safeStats.getMessagesPerSecond(),
+ safeStats.getTpsIn(),
+ safeStats.getTpsOut());
+ }
+
+ public record Output(String cluster, List<Cluster> clusters, Stats stats) {
+ }
+
+ public record Cluster(
+ String id,
+ String name,
+ String type,
+ String status,
+ int brokers,
+ Integer proxies,
+ int topics,
+ int groups,
+ long tpsIn,
+ long tpsOut,
+ String version,
+ List<Integer> throughput) {
+ }
+
+ public record Stats(
+ int totalClusters,
+ int healthyClusters,
+ int totalBrokers,
+ Integer totalProxies,
+ Integer totalNameServers,
+ int totalTopics,
+ int totalConsumerGroups,
+ long totalMessagesToday,
+ long messagesPerSecond,
+ long tpsIn,
+ long tpsOut) {
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQClearToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQClearToolHandler.java
new file mode 100644
index 000000000..0dec3362a
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQClearToolHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.dlq;
+
+import org.apache.rocketmq.studio.instance.topic.MetadataService;
+import org.apache.rocketmq.studio.instance.dlq.DLQGroupVO;
+import org.apache.rocketmq.studio.instance.dlq.DLQService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.dlq.DLQClearInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Component
+public class DLQClearToolHandler extends MutationToolHandler<DLQClearInput, Void> {
+
+ private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+ "clear DLQ for consumer group '%s' in cluster '%s'.",
+ List.of("Deletes the physical dead-letter topic and its retained messages."),
+ List.of("All retained dead-letter messages for this group will be deleted."));
+
+ private final MetadataService metadataService;
+ private final DLQService dlqService;
+
+ public DLQClearToolHandler(MetadataService metadataService, DLQService dlqService) {
+ super(DLQClearInput.class);
+ this.metadataService = metadataService;
+ this.dlqService = dlqService;
+ }
+
+ @Override
+ public String name() {
+ return "rmq.dlq.clear";
+ }
+
+ @Override
+ public ToolPlan preview(DLQClearInput input, ToolExecutionContext context) {
+ String instanceId = context.cluster();
+ String groupName = input.group();
+ DLQGroupVO group = dlqService.listDLQGroups(instanceId, groupName, 1, 100)
+ .getItems().stream()
+ .filter(item -> groupName.equals(item.getGroupName()))
+ .findFirst()
+ .orElseThrow(() -> ToolError.DLQ_GROUP_NOT_FOUND.exception(groupName));
+ if (!group.isStatsAvailable()) {
+ throw ToolError.DLQ_STATISTICS_UNAVAILABLE.exception(groupName);
+ }
+ Map<String, Object> before = new LinkedHashMap<>();
+ before.put("group", group.getGroupName());
+ before.put("topic", group.getDlqTopic());
+ before.put("messageCount", group.getMessageCount());
+ before.put("statsAvailable", group.isStatsAvailable());
+ before.put("status", group.getStatus());
+ return PLAN_DESCRIPTION.builder(groupName, context.cluster())
+ .before(before)
+ .build();
+ }
+
+ @Override
+ public Void execute(DLQClearInput input, ToolExecutionContext context) {
+ metadataService.deleteTopic(context.cluster(), "%DLQ%" + input.group());
+ return null;
+ }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQListToolHandler.java
new file mode 100644
index 000000000..e557d7de6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQListToolHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai.tool.handler.dlq;
+
+import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.instance.dlq.DLQGroupVO;
+import org.apache.rocketmq.studio.instance.dlq.DLQMessageVO;
+import org.apache.rocketmq.studio.instance.dlq.DLQService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.dlq.DLQListInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.dlq.DLQListOutput;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class DLQListToolHandler implements ToolHandler<DLQListInput, DLQListOutput> {
+
+ private final DLQService dlqService;
+
+ @Override
+ public String name() {
+ return "rmq.dlq.list";
+ }
+
+ @Override
+ public Class<DLQListInput> inputType() {
+ return DLQListInput.class;
+ }
+
+ @Override
+ public DLQListOutput execute(DLQListInput input, ToolExecutionContext context) {
+ int page = input.page() != null ? input.page().page() : 1;
+ int pageSize = input.page() != null ? input.page().pageSize() : 20;
+ Long startTime = input.time() != null ? input.time().startTime() : null;
+ Long endTime = input.time() != null ? input.time().endTime() : null;
+ if (input.group() == null || input.group().isBlank()) {
+ return listGroups(
+ context.cluster(), input.search(), page, pageSize);
+ }
+ return listMessages(
+ context.cluster(), input.group(), startTime, endTime,
+ page, pageSize);
+ }
+
+ private DLQListOutput listGroups(String instanceId, String search, int page, int pageSize) {
+ PageResult<DLQGroupVO> result = dlqService.listDLQGroups(instanceId, search, page, pageSize);
+ return DLQListOutput.ofGroups(
+ instanceId,
+ result.getPage(),
+ result.getSize(),
+ result.getTotal(),
+ result.getItems().stream()
+ .map(DLQListOutput.DLQGroupItem::from)
+ .toList());
+ }
+
+ private DLQListOutput listMessages(String instanceId, String group, Long startTime, Long endTime,
+ int page, int pageSize) {
+ PageResult<DLQMessageVO> result = dlqService.listMessages(instanceId, group, startTime, endTime,
... 18471 lines suppressed ...