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 f07dfbf9 feat: add AI resource tools (#642)
f07dfbf9 is described below

commit f07dfbf964e17b68689b73f4d53721631b3d0b0a
Author: aias00 <[email protected]>
AuthorDate: Tue Jul 28 07:27:49 2026 -0700

    feat: add AI resource tools (#642)
    
    * [Studio] Add AI resource tools
    
    * fix: normalize AI draft model
---
 .../ops/ai/tool/ConsumerGroupListToolHandler.java  |  88 ++++++++
 .../ops/ai/tool/DashboardSummaryToolHandler.java   | 111 ++++++++++
 .../studio/ops/ai/tool/TopicListToolHandler.java   |  82 +++++++
 .../src/main/resources/tool-catalog/rmq-tools.yaml | 229 ++++++++++++++++++++
 .../studio/ops/ai/tool/ToolCatalogTest.java        |   7 +-
 .../studio/ops/ai/tool/ToolGatewayServiceTest.java | 236 ++++++++++++++++++++-
 web/src/pages/ai/chatDraft.test.ts                 |   8 +-
 web/src/pages/ai/chatDraft.ts                      |   3 +-
 8 files changed, 754 insertions(+), 10 deletions(-)

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
new file mode 100644
index 00000000..db7fd84c
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ConsumerGroupListToolHandler.java
@@ -0,0 +1,88 @@
+/*
+ * 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 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");
+        return metadataService.listConsumerGroups(clusterId, search).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());
+        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
new file mode 100644
index 00000000..0d8708e7
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/DashboardSummaryToolHandler.java
@@ -0,0 +1,111 @@
+/*
+ * 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/TopicListToolHandler.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/TopicListToolHandler.java
new file mode 100644
index 00000000..6eb730c2
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/TopicListToolHandler.java
@@ -0,0 +1,82 @@
+/*
+ * 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 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");
+        return metadataService.listTopics(clusterId, type, search).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/resources/tool-catalog/rmq-tools.yaml 
b/server/src/main/resources/tool-catalog/rmq-tools.yaml
index 4327667c..1ce2d105 100644
--- a/server/src/main/resources/tool-catalog/rmq-tools.yaml
+++ b/server/src/main/resources/tool-catalog/rmq-tools.yaml
@@ -74,3 +74,232 @@ tools:
             type: string
     viewHint: object
     deprecated: false
+  - name: rmq.dashboard.summary
+    cli:
+      resource: dashboard
+      verb: summary
+    description: Get the Studio dashboard summary for one RocketMQ cluster.
+    riskLevel: L1
+    permission: dashboard:read
+    requiredCapabilities: []
+    inputSchema:
+      type: object
+      required:
+        - cluster
+      additionalProperties: false
+      properties:
+        cluster:
+          type: string
+          minLength: 1
+    outputSchema:
+      type: object
+      required:
+        - cluster
+        - stats
+      additionalProperties: false
+      properties:
+        cluster:
+          type: object
+          required:
+            - id
+            - name
+            - type
+            - status
+            - brokers
+            - proxies
+            - topics
+            - groups
+            - tpsIn
+            - tpsOut
+            - version
+            - throughput
+          additionalProperties: false
+          properties:
+            id:
+              type: string
+            name:
+              type: string
+            type:
+              type: string
+            status:
+              type: string
+            brokers:
+              type: integer
+            proxies:
+              type: integer
+            topics:
+              type: integer
+            groups:
+              type: integer
+            tpsIn:
+              type: integer
+            tpsOut:
+              type: integer
+            version:
+              type: string
+            throughput:
+              type: array
+              items:
+                type: integer
+        stats:
+          type: object
+          required:
+            - totalClusters
+            - healthyClusters
+            - totalBrokers
+            - totalProxies
+            - totalNameServers
+            - totalTopics
+            - totalConsumerGroups
+            - totalMessagesToday
+            - messagesPerSecond
+            - tpsIn
+            - tpsOut
+          additionalProperties: false
+          properties:
+            totalClusters:
+              type: integer
+            healthyClusters:
+              type: integer
+            totalBrokers:
+              type: integer
+            totalProxies:
+              type: integer
+            totalNameServers:
+              type: integer
+            totalTopics:
+              type: integer
+            totalConsumerGroups:
+              type: integer
+            totalMessagesToday:
+              type: integer
+            messagesPerSecond:
+              type: integer
+            tpsIn:
+              type: integer
+            tpsOut:
+              type: integer
+    viewHint: object
+    deprecated: false
+  - name: rmq.topic.list
+    cli:
+      resource: topic
+      verb: list
+    description: List RocketMQ topics in one Studio cluster.
+    riskLevel: L1
+    permission: topic:read
+    requiredCapabilities:
+      - REMOTING
+    inputSchema:
+      type: object
+      required:
+        - cluster
+      additionalProperties: false
+      properties:
+        cluster:
+          type: string
+          minLength: 1
+        type:
+          type: string
+          minLength: 1
+        search:
+          type: string
+          minLength: 1
+    outputSchema:
+      type: array
+      items:
+        type: object
+        required:
+          - name
+          - namespace
+          - clusterId
+          - type
+          - writeQueues
+          - readQueues
+          - perm
+          - messageCount
+          - tps
+          - consumerGroupCount
+        additionalProperties: false
+        properties:
+          name:
+            type: string
+          namespace:
+            type: string
+          clusterId:
+            type: string
+          type:
+            type: string
+          writeQueues:
+            type: integer
+          readQueues:
+            type: integer
+          perm:
+            type: string
+          messageCount:
+            type: integer
+          tps:
+            type: number
+          consumerGroupCount:
+            type: integer
+    viewHint: table
+    deprecated: false
+  - name: rmq.group.list
+    cli:
+      resource: group
+      verb: list
+    description: List RocketMQ consumer groups in one Studio cluster.
+    riskLevel: L1
+    permission: group:read
+    requiredCapabilities:
+      - REMOTING
+    inputSchema:
+      type: object
+      required:
+        - cluster
+      additionalProperties: false
+      properties:
+        cluster:
+          type: string
+          minLength: 1
+        search:
+          type: string
+          minLength: 1
+    outputSchema:
+      type: array
+      items:
+        type: object
+        required:
+          - name
+          - namespace
+          - clusterId
+          - subscriptionMode
+          - consumeType
+          - onlineInstances
+          - totalLag
+          - subscribedTopics
+          - retryMaxTimes
+        additionalProperties: false
+        properties:
+          name:
+            type: string
+          namespace:
+            type: string
+          clusterId:
+            type: string
+          subscriptionMode:
+            type: string
+          consumeType:
+            type: string
+          onlineInstances:
+            type: integer
+          totalLag:
+            type: integer
+          subscribedTopics:
+            type: array
+            items:
+              type: string
+          retryMaxTimes:
+            type: integer
+    viewHint: table
+    deprecated: false
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalogTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalogTest.java
index c35a11e1..50f10175 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalogTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalogTest.java
@@ -38,7 +38,12 @@ class ToolCatalogTest {
         assertThat(catalog.getMinimumClientVersion()).isEqualTo("1.0.0");
         assertThat(catalog.getDigest()).matches("[0-9a-f]{64}");
         assertThat(catalog.list()).extracting(ToolDefinition::getName)
-                .containsExactly("rmq.cluster.list", "rmq.capabilities");
+                .containsExactly(
+                        "rmq.cluster.list",
+                        "rmq.capabilities",
+                        "rmq.dashboard.summary",
+                        "rmq.topic.list",
+                        "rmq.group.list");
         assertThat(catalog.find("rmq.cluster.list")).isPresent();
         assertThat(catalog.find("rmq.unknown")).isEmpty();
     }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayServiceTest.java
index 13dd6fe7..46a0699d 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayServiceTest.java
@@ -21,8 +21,19 @@ import 
org.apache.rocketmq.studio.cluster.broker.ClusterService;
 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;
+import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
+import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.topic.MetadataService;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
 import org.apache.rocketmq.studio.ops.ai.AiToolVO;
+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.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.core.io.ByteArrayResource;
@@ -43,19 +54,35 @@ class ToolGatewayServiceTest {
 
     private ToolCatalog catalog;
     private ClusterService clusterService;
+    private DashboardService dashboardService;
+    private MetadataService metadataService;
     private CapabilityResolver capabilityResolver;
     private ClusterListToolHandler clusterListHandler;
     private CapabilitiesToolHandler capabilitiesHandler;
+    private DashboardSummaryToolHandler dashboardSummaryHandler;
+    private TopicListToolHandler topicListHandler;
+    private ConsumerGroupListToolHandler consumerGroupListHandler;
     private ToolGatewayService gateway;
 
     @BeforeEach
     void setUp() {
         catalog = canonicalCatalog();
         clusterService = mock(ClusterService.class);
+        dashboardService = mock(DashboardService.class);
+        metadataService = mock(MetadataService.class);
         capabilityResolver = new CapabilityResolver(clusterService);
         clusterListHandler = new ClusterListToolHandler(clusterService);
         capabilitiesHandler = new CapabilitiesToolHandler(clusterService, 
capabilityResolver);
-        gateway = gateway(catalog, clusterListHandler, capabilitiesHandler);
+        dashboardSummaryHandler = new 
DashboardSummaryToolHandler(dashboardService);
+        topicListHandler = new TopicListToolHandler(metadataService);
+        consumerGroupListHandler = new 
ConsumerGroupListToolHandler(metadataService);
+        gateway = gateway(
+                catalog,
+                clusterListHandler,
+                capabilitiesHandler,
+                dashboardSummaryHandler,
+                topicListHandler,
+                consumerGroupListHandler);
     }
 
     @Test
@@ -72,7 +99,12 @@ class ToolGatewayServiceTest {
 
         assertThat(gateway.discover("cluster-v5"))
                 .extracting(AiToolVO::getName)
-                .containsExactly("rmq.cluster.list", "rmq.capabilities");
+                .containsExactly(
+                        "rmq.cluster.list",
+                        "rmq.capabilities",
+                        "rmq.dashboard.summary",
+                        "rmq.topic.list",
+                        "rmq.group.list");
     }
 
     @Test
@@ -113,6 +145,73 @@ class ToolGatewayServiceTest {
                         "ROCKETMQ_5")));
     }
 
+    @Test
+    @SuppressWarnings("unchecked")
+    void executesDashboardSummaryWithADataMinimizingProjection() {
+        
when(dashboardService.getDashboard()).thenReturn(DashboardDataVO.builder()
+                .stats(DashboardStatsVO.builder()
+                        .totalClusters(1)
+                        .healthyClusters(1)
+                        .totalBrokers(2)
+                        .totalProxies(1)
+                        .totalNameServers(1)
+                        .totalTopics(3)
+                        .totalConsumerGroups(4)
+                        .totalMessagesToday(500L)
+                        .messagesPerSecond(6L)
+                        .tpsIn(7L)
+                        .tpsOut(8L)
+                        .build())
+                .clusters(List.of(ClusterOverviewVO.builder()
+                        .id("cluster-v5")
+                        .name("test")
+                        .type(ClusterType.V5_PROXY_CLUSTER)
+                        .status(ClusterStatus.healthy)
+                        .brokers(2)
+                        .proxies(1)
+                        .topics(3)
+                        .groups(4)
+                        .tpsIn(7)
+                        .tpsOut(8)
+                        .version("5.2.0")
+                        .throughput(List.of(1, 2, 3))
+                        .build()))
+                .build());
+
+        Object output = gateway.execute(
+                "rmq.dashboard.summary", Map.of("cluster", "cluster-v5"));
+
+        Map<String, Object> result = (Map<String, Object>) output;
+        assertThat(result).containsOnlyKeys("cluster", "stats");
+        Map<String, Object> cluster = (Map<String, Object>) 
result.get("cluster");
+        assertThat(cluster).containsEntry("id", "cluster-v5");
+        assertThat(cluster).containsEntry("name", "test");
+        assertThat(cluster).containsEntry("type", "V5_PROXY_CLUSTER");
+        assertThat(cluster).containsEntry("status", "healthy");
+        assertThat(cluster).containsEntry("brokers", 2);
+        assertThat(cluster).containsEntry("proxies", 1);
+        assertThat(cluster).containsEntry("topics", 3);
+        assertThat(cluster).containsEntry("groups", 4);
+        assertThat(cluster).containsEntry("tpsIn", 7);
+        assertThat(cluster).containsEntry("tpsOut", 8);
+        assertThat(cluster).containsEntry("version", "5.2.0");
+        assertThat(cluster).containsEntry("throughput", List.of(1, 2, 3));
+        assertThat(cluster).doesNotContainKeys("endpoint");
+        assertThat((Map<String, Object>) 
result.get("stats")).containsAllEntriesOf(Map.of(
+                "totalMessagesToday", 500L,
+                "messagesPerSecond", 6L,
+                "tpsIn", 7L,
+                "tpsOut", 8L));
+    }
+
+    @Test
+    void rejectsDashboardSummaryWithoutRequiredClusterBeforeHandlerRuns() {
+        assertThatThrownBy(() -> gateway.execute("rmq.dashboard.summary", 
Map.of()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("input validation failed");
+        verifyNoInteractions(dashboardService);
+    }
+
     @Test
     void resolvesCapabilitiesForEveryExistingClusterType() {
         when(clusterService.getCluster("v4")).thenReturn(cluster("v4", 
ClusterType.V4_DIRECT));
@@ -152,6 +251,73 @@ class ToolGatewayServiceTest {
                 .hasMessageContaining("Cluster type is unavailable");
     }
 
+    @Test
+    void executesTopicListThroughADataMinimizingProjection() {
+        
when(clusterService.getCluster("cluster-v5")).thenReturn(cluster(ClusterType.V5_PROXY_CLUSTER));
+        TopicVO topic = topic();
+        topic.setRemark("do-not-expose");
+        when(metadataService.listTopics("cluster-v5", "NORMAL", "order"))
+                .thenReturn(List.of(topic));
+
+        Object output = gateway.execute("rmq.topic.list", Map.of(
+                "cluster", "cluster-v5",
+                "type", "NORMAL",
+                "search", "order"));
+
+        assertThat(output).isEqualTo(List.of(Map.of(
+                "name", "order-topic",
+                "namespace", "default",
+                "clusterId", "cluster-v5",
+                "type", "NORMAL",
+                "writeQueues", 8,
+                "readQueues", 8,
+                "perm", "RW",
+                "messageCount", 1200L,
+                "tps", 23.5D,
+                "consumerGroupCount", 3)));
+        assertThat(output.toString()).doesNotContain("do-not-expose");
+    }
+
+    @Test
+    void rejectsTopicListWithoutAClusterBeforeHandlerRuns() {
+        assertThatThrownBy(() -> gateway.execute("rmq.topic.list", 
Map.of("type", "NORMAL")))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("input validation failed");
+        verifyNoInteractions(metadataService);
+    }
+
+    @Test
+    void executesConsumerGroupListThroughADataMinimizingProjection() {
+        
when(clusterService.getCluster("cluster-v5")).thenReturn(cluster(ClusterType.V5_PROXY_CLUSTER));
+        ConsumerGroupVO group = consumerGroup();
+        group.setDelaySeconds(30);
+        when(metadataService.listConsumerGroups("cluster-v5", 
"order")).thenReturn(List.of(group));
+
+        Object output = gateway.execute("rmq.group.list", Map.of(
+                "cluster", "cluster-v5",
+                "search", "order"));
+
+        assertThat(output).isEqualTo(List.of(Map.of(
+                "name", "cg-order",
+                "namespace", "default",
+                "clusterId", "cluster-v5",
+                "subscriptionMode", "Push",
+                "consumeType", "CLUSTERING",
+                "onlineInstances", 2,
+                "totalLag", 42L,
+                "subscribedTopics", List.of("order-topic"),
+                "retryMaxTimes", 16)));
+        assertThat(output.toString()).doesNotContain("delaySeconds");
+    }
+
+    @Test
+    void rejectsConsumerGroupListWithoutAClusterBeforeHandlerRuns() {
+        assertThatThrownBy(() -> gateway.execute("rmq.group.list", Map.of()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("input validation failed");
+        verifyNoInteractions(metadataService);
+    }
+
     @Test
     void rejectsCapabilitiesExecutionWhenClusterTypeIsMissing() {
         
when(clusterService.getCluster("unknown")).thenReturn(cluster("unknown", null));
@@ -201,7 +367,13 @@ class ToolGatewayServiceTest {
         ToolCatalog l2Catalog = ToolCatalog.load(
                 new ByteArrayResource(yaml.getBytes(StandardCharsets.UTF_8)),
                 new ClassPathResource("tool-catalog/rmq-tools.schema.json"));
-        ToolGatewayService l2Gateway = gateway(l2Catalog, clusterListHandler, 
capabilitiesHandler);
+        ToolGatewayService l2Gateway = gateway(
+                l2Catalog,
+                clusterListHandler,
+                capabilitiesHandler,
+                dashboardSummaryHandler,
+                topicListHandler,
+                consumerGroupListHandler);
 
         assertThatThrownBy(() -> l2Gateway.execute("rmq.cluster.list", 
Map.of()))
                 .isInstanceOf(BusinessException.class)
@@ -212,7 +384,13 @@ class ToolGatewayServiceTest {
     @Test
     void failsStartupForDuplicateHandlerNames() {
         assertThatThrownBy(() -> gateway(
-                catalog, clusterListHandler, clusterListHandler, 
capabilitiesHandler))
+                catalog,
+                clusterListHandler,
+                clusterListHandler,
+                capabilitiesHandler,
+                dashboardSummaryHandler,
+                topicListHandler,
+                consumerGroupListHandler))
                 .isInstanceOf(IllegalStateException.class)
                 .hasMessageContaining("duplicate handler");
     }
@@ -241,7 +419,12 @@ class ToolGatewayServiceTest {
                 new ClassPathResource("tool-catalog/rmq-tools.schema.json"));
 
         assertThatThrownBy(() -> gateway(
-                invalidCatalog, clusterListHandler, capabilitiesHandler))
+                invalidCatalog,
+                clusterListHandler,
+                capabilitiesHandler,
+                dashboardSummaryHandler,
+                topicListHandler,
+                consumerGroupListHandler))
                 .isInstanceOf(IllegalStateException.class)
                 .hasMessageContaining("input schema")
                 .hasMessageContaining("rmq.cluster.list");
@@ -263,7 +446,12 @@ class ToolGatewayServiceTest {
                 new ClassPathResource("tool-catalog/rmq-tools.schema.json"));
 
         assertThatThrownBy(() -> gateway(
-                invalidCatalog, clusterListHandler, capabilitiesHandler))
+                invalidCatalog,
+                clusterListHandler,
+                capabilitiesHandler,
+                dashboardSummaryHandler,
+                topicListHandler,
+                consumerGroupListHandler))
                 .isInstanceOf(IllegalStateException.class)
                 .hasMessageContaining("input schema")
                 .hasMessageContaining("rmq.cluster.list");
@@ -283,7 +471,12 @@ class ToolGatewayServiceTest {
             }
         };
         ToolGatewayService invalidGateway = gateway(
-                catalog, invalidClusterListHandler, capabilitiesHandler);
+                catalog,
+                invalidClusterListHandler,
+                capabilitiesHandler,
+                dashboardSummaryHandler,
+                topicListHandler,
+                consumerGroupListHandler);
 
         assertThatThrownBy(() -> invalidGateway.execute("rmq.cluster.list", 
Map.of()))
                 .isInstanceOf(IllegalStateException.class)
@@ -323,4 +516,33 @@ class ToolGatewayServiceTest {
         cluster.setId(id);
         return cluster;
     }
+
+    private static TopicVO topic() {
+        TopicVO topic = new TopicVO();
+        topic.setName("order-topic");
+        topic.setNamespace("default");
+        topic.setClusterId("cluster-v5");
+        topic.setType(TopicType.NORMAL);
+        topic.setWriteQueues(8);
+        topic.setReadQueues(8);
+        topic.setPerm(TopicPerm.RW);
+        topic.setMessageCount(1200L);
+        topic.setTps(23.5D);
+        topic.setConsumerGroupCount(3);
+        return topic;
+    }
+
+    private static ConsumerGroupVO consumerGroup() {
+        ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setName("cg-order");
+        group.setNamespace("default");
+        group.setClusterId("cluster-v5");
+        group.setSubscriptionMode(SubscriptionMode.Push);
+        group.setConsumeType(ConsumeType.CLUSTERING);
+        group.setOnlineInstances(2);
+        group.setTotalLag(42L);
+        group.setSubscribedTopics(List.of("order-topic"));
+        group.setRetryMaxTimes(16);
+        return group;
+    }
 }
diff --git a/web/src/pages/ai/chatDraft.test.ts 
b/web/src/pages/ai/chatDraft.test.ts
index 6f1e09e8..d7d89f42 100644
--- a/web/src/pages/ai/chatDraft.test.ts
+++ b/web/src/pages/ai/chatDraft.test.ts
@@ -20,7 +20,7 @@ import { getChatDraft } from './chatDraft';
 
 describe('AI chat draft navigation state', () => {
   it('normalizes a prompt and preserves a selected model', () => {
-    expect(getChatDraft({ prompt: '  检查集群状态  ', model: 'qwen3.7-max' 
})).toEqual({
+    expect(getChatDraft({ prompt: '  检查集群状态  ', model: '  qwen3.7-max  ' 
})).toEqual({
       prompt: '检查集群状态',
       model: 'qwen3.7-max',
     });
@@ -31,4 +31,10 @@ describe('AI chat draft navigation state', () => {
     expect(getChatDraft({ prompt: '   ' })).toBeNull();
     expect(getChatDraft({ prompt: 42 })).toBeNull();
   });
+
+  it('drops empty model values after normalization', () => {
+    expect(getChatDraft({ prompt: '检查集群状态', model: '   ' })).toEqual({
+      prompt: '检查集群状态',
+    });
+  });
 });
diff --git a/web/src/pages/ai/chatDraft.ts b/web/src/pages/ai/chatDraft.ts
index 115692ad..2ad1a936 100644
--- a/web/src/pages/ai/chatDraft.ts
+++ b/web/src/pages/ai/chatDraft.ts
@@ -24,9 +24,10 @@ export function getChatDraft(state: unknown): ChatDraft | 
null {
   if (typeof state !== 'object' || state === null) return null;
   const candidate = state as Record<string, unknown>;
   if (typeof candidate.prompt !== 'string' || !candidate.prompt.trim()) return 
null;
+  const model = typeof candidate.model === 'string' ? candidate.model.trim() : 
'';
 
   return {
     prompt: candidate.prompt.trim(),
-    ...(typeof candidate.model === 'string' && candidate.model ? { model: 
candidate.model } : {}),
+    ...(model ? { model } : {}),
   };
 }

Reply via email to