zhou zhou
7 小时以前 80a6d9236ade191a5de0975abe4de5a6e7e63915
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package com.vincent.rsf.server.ai.service.impl;
 
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.ai.config.AiDefaults;
import com.vincent.rsf.server.ai.dto.AiMcpToolPreviewDto;
import com.vincent.rsf.server.ai.entity.AiMcpMount;
import com.vincent.rsf.server.ai.service.BuiltinMcpToolRegistry;
import com.vincent.rsf.server.ai.tool.RsfWmsBaseTools;
import com.vincent.rsf.server.ai.tool.RsfWmsStockTools;
import com.vincent.rsf.server.ai.tool.RsfWmsTaskTools;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
@Service
@RequiredArgsConstructor
public class BuiltinMcpToolRegistryImpl implements BuiltinMcpToolRegistry {
 
    private final RsfWmsStockTools rsfWmsStockTools;
    private final RsfWmsTaskTools rsfWmsTaskTools;
    private final RsfWmsBaseTools rsfWmsBaseTools;
 
    /**
     * 校验内置 MCP 编码是否合法。
     * 当前版本只允许使用显式登记在注册表中的编码,未知编码直接拒绝,
     * 这样可以确保“页面可选项”和“运行时可挂载项”始终一致。
     */
    @Override
    public void validateBuiltinCode(String builtinCode) {
        if (!StringUtils.hasText(builtinCode)) {
            throw new CoolException("内置 MCP 编码不能为空");
        }
        if (!supportedBuiltinCodes().contains(builtinCode)) {
            throw new CoolException("不支持的内置 MCP 编码: " + builtinCode);
        }
    }
 
    /**
     * 根据挂载记录创建内置工具回调。
     * 这里不会做任何动态发现,所有工具都必须经过显式注册和治理目录校验后才能暴露给模型。
     */
    @Override
    public List<ToolCallback> createToolCallbacks(AiMcpMount mount, Long userId) {
        String builtinCode = mount.getBuiltinCode();
        validateBuiltinCode(builtinCode);
        if (AiDefaults.MCP_BUILTIN_RSF_WMS.equals(builtinCode)) {
            List<ToolCallback> callbacks = new ArrayList<>();
            callbacks.addAll(createValidatedCallbacks(rsfWmsStockTools, builtinCode));
            callbacks.addAll(createValidatedCallbacks(rsfWmsTaskTools, builtinCode));
            callbacks.addAll(createValidatedCallbacks(rsfWmsBaseTools, builtinCode));
            return callbacks;
        }
        throw new CoolException("不支持的内置 MCP 编码: " + builtinCode);
    }
 
    /**
     * 返回某个内置编码下可预览的工具目录信息。
     * 该目录比运行时回调多了工具用途、查询边界和示例提问,供管理页展示。
     */
    @Override
    public List<AiMcpToolPreviewDto> listBuiltinToolCatalog(String builtinCode) {
        validateBuiltinCode(builtinCode);
        if (AiDefaults.MCP_BUILTIN_RSF_WMS.equals(builtinCode)) {
            return new ArrayList<>(catalogByBuiltinCode(builtinCode).values());
        }
        return new ArrayList<>(catalogByBuiltinCode(builtinCode).values());
    }
 
    private List<ToolCallback> createValidatedCallbacks(Object toolBean, String builtinCode) {
        /**
         * 把 `@Tool` Bean 转成 Spring AI ToolCallback,并强制校验:
         * 1. 工具名必须符合命名规范
         * 2. 每个工具都必须出现在治理目录里
         */
        List<ToolCallback> callbacks = Arrays.asList(ToolCallbacks.from(toolBean));
        Map<String, AiMcpToolPreviewDto> catalog = catalogByBuiltinCode(builtinCode);
        for (ToolCallback callback : callbacks) {
            if (callback == null || callback.getToolDefinition() == null) {
                continue;
            }
            String toolName = callback.getToolDefinition().name();
            if (!StringUtils.hasText(toolName) || !toolName.startsWith("rsf_query_")) {
                throw new CoolException("内置工具命名不符合规范,必须以 rsf_query_ 开头: " + toolName);
            }
            if (!catalog.containsKey(toolName)) {
                throw new CoolException("内置工具缺少治理目录配置: " + toolName);
            }
        }
        return callbacks;
    }
 
    private List<String> supportedBuiltinCodes() {
        /** 当前版本允许挂载的全部内置 MCP 编码。 */
        return List.of(AiDefaults.MCP_BUILTIN_RSF_WMS);
    }
 
    private Map<String, AiMcpToolPreviewDto> catalogByBuiltinCode(String builtinCode) {
        /**
         * 构造内置工具治理目录。
         * 这里的目录是运行时校验和管理端预览的共同事实来源,不能与工具实现脱节。
         */
        if (AiDefaults.MCP_BUILTIN_RSF_WMS.equals(builtinCode)) {
            Map<String, AiMcpToolPreviewDto> catalog = new LinkedHashMap<>();
            catalog.put("rsf_query_available_inventory", buildCatalogItem(
                    "rsf_query_available_inventory",
                    "库存查询",
                    "查询指定物料当前可用于出库的库存明细。",
                    "必须提供物料编码或物料名称,并且最多返回 50 条库存记录。",
                    List.of("查询物料 MAT001 当前可出库库存", "按物料名称查询托盘库存明细")
            ));
            catalog.put("rsf_query_station_list", buildCatalogItem(
                    "rsf_query_station_list",
                    "库存查询",
                    "查询指定作业类型可用的设备站点。",
                    "必须提供站点类型列表,类型数量最多 10 个,最多返回 50 个站点。",
                    List.of("查询入库和出库作业可用站点", "列出 AGV_PICK 类型的作业站点")
            ));
            catalog.put("rsf_query_task_list", buildCatalogItem(
                    "rsf_query_task_list",
                    "任务查询",
                    "按任务号、状态、类型或站点条件查询任务列表。",
                    "至少提供一个过滤条件,最多返回 50 条任务记录,不支持全表扫描。",
                    List.of("查询最近 10 条状态为执行中的任务", "按任务号关键字查询任务列表")
            ));
            catalog.put("rsf_query_task_detail", buildCatalogItem(
                    "rsf_query_task_detail",
                    "任务查询",
                    "按任务 ID 或任务号查询单个任务详情。",
                    "必须提供任务 ID 或任务号之一,只返回单个任务。",
                    List.of("查询任务 12345 的详情", "根据任务号 TASK24001 查看执行明细")
            ));
            catalog.put("rsf_query_warehouses", buildCatalogItem(
                    "rsf_query_warehouses",
                    "基础资料",
                    "查询仓库基础信息。",
                    "至少提供仓库编码或名称,最多返回 50 条仓库记录。",
                    List.of("查询编码包含 WH 的仓库", "按仓库名称查询仓库地址")
            ));
            catalog.put("rsf_query_bas_stations", buildCatalogItem(
                    "rsf_query_bas_stations",
                    "基础资料",
                    "查询基础站点信息。",
                    "至少提供站点编号、站点名称或使用状态之一,最多返回 50 条站点记录。",
                    List.of("查询使用中的基础站点", "按站点编号查询基础站点")
            ));
            catalog.put("rsf_query_dict_data", buildCatalogItem(
                    "rsf_query_dict_data",
                    "基础资料",
                    "查询指定字典类型下的字典数据。",
                    "必须提供字典类型编码,最多返回 100 条字典记录。",
                    List.of("查询 task_status 字典", "按字典标签过滤 task_type 字典数据")
            ));
            return catalog;
        }
        throw new CoolException("不支持的内置 MCP 编码: " + builtinCode);
    }
 
    private AiMcpToolPreviewDto buildCatalogItem(String name, String toolGroup, String toolPurpose,
                                                 String queryBoundary, List<String> exampleQuestions) {
        /** 统一创建工具目录条目,避免不同工具组出现字段风格不一致。 */
        return AiMcpToolPreviewDto.builder()
                .name(name)
                .toolGroup(toolGroup)
                .toolPurpose(toolPurpose)
                .queryBoundary(queryBoundary)
                .exampleQuestions(exampleQuestions)
                .build();
    }
}