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.impl.mcp.BuiltinMcpToolCatalogProvider; 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; private final BuiltinMcpToolCatalogProvider builtinMcpToolCatalogProvider; /** * 校验内置 MCP 编码是否合法。 * 当前版本只允许使用显式登记在注册表中的编码,未知编码直接拒绝, * 这样可以确保“页面可选项”和“运行时可挂载项”始终一致。 */ @Override public void validateBuiltinCode(String builtinCode) { if (!StringUtils.hasText(builtinCode)) { throw new CoolException("内置 MCP 编码不能为空"); } if (!builtinMcpToolCatalogProvider.supportedBuiltinCodes().contains(builtinCode)) { throw new CoolException("不支持的内置 MCP 编码: " + builtinCode); } } /** * 根据挂载记录创建内置工具回调。 * 这里不会做任何动态发现,所有工具都必须经过显式注册和治理目录校验后才能暴露给模型。 */ @Override public List createToolCallbacks(AiMcpMount mount, Long userId) { String builtinCode = mount.getBuiltinCode(); validateBuiltinCode(builtinCode); if (AiDefaults.MCP_BUILTIN_RSF_WMS.equals(builtinCode)) { List 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 listBuiltinToolCatalog(String builtinCode) { validateBuiltinCode(builtinCode); return new ArrayList<>(builtinMcpToolCatalogProvider.getCatalog(builtinCode).values()); } private List createValidatedCallbacks(Object toolBean, String builtinCode) { /** * 把 `@Tool` Bean 转成 Spring AI ToolCallback,并强制校验: * 1. 工具名必须符合命名规范 * 2. 每个工具都必须出现在治理目录里 */ List callbacks = Arrays.asList(ToolCallbacks.from(toolBean)); Map catalog = builtinMcpToolCatalogProvider.getCatalog(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; } }