zhou zhou
18 小时以前 ffbf67765d2ae447d62333eed85100a15685d781
#AI.内置工具治理
1个文件已添加
8个文件已修改
391 ■■■■ 已修改文件
rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiMcpToolPreviewDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/BuiltinMcpToolRegistry.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/BuiltinMcpToolRegistryImpl.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/tool/BuiltinToolGovernanceSupport.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/tool/RsfWmsBaseTools.java 68 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/tool/RsfWmsStockTools.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/tool/RsfWmsTaskTools.java 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx
@@ -305,8 +305,18 @@
                                                    <Typography variant="body2" color="text.secondary">
                                                        {tool.description || "暂无描述"}
                                                    </Typography>
                                                    {!!tool.toolPurpose && (
                                                        <Typography variant="caption" color="text.secondary" display="block" mt={0.5}>
                                                            用途: {tool.toolPurpose}
                                                        </Typography>
                                                    )}
                                                </Box>
                                                <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
                                                    {!!tool.toolGroup && (
                                                        <Typography variant="caption" color="text.secondary">
                                                            {tool.toolGroup}
                                                        </Typography>
                                                    )}
                                                    <Typography variant="caption" color="text.secondary">
                                                        {schemaInfo.fields.length} 个参数
                                                    </Typography>
@@ -317,8 +327,25 @@
                                            </Stack>
                                        </AccordionSummary>
                                        <AccordionDetails>
                                            <Card variant="outlined" sx={{ borderRadius: 3 }}>
                                                <Card variant="outlined" sx={{ borderRadius: 3 }}>
                                                <CardContent>
                                                    {!!tool.queryBoundary && (
                                                        <Alert severity="info" sx={{ mb: 2 }}>
                                                            查询边界: {tool.queryBoundary}
                                                        </Alert>
                                                    )}
                                                    {!!tool.exampleQuestions?.length && (
                                                        <Alert severity="success" sx={{ mb: 2 }}>
                                                            <Typography variant="body2" fontWeight={700} mb={0.5}>
                                                                示例提问
                                                            </Typography>
                                                            {tool.exampleQuestions.map((question) => (
                                                                <Typography key={question} variant="body2">
                                                                    {`- ${question}`}
                                                                </Typography>
                                                            ))}
                                                        </Alert>
                                                    )}
                                                    {!!schemaInfo.error && (
                                                        <Alert severity="warning" sx={{ mb: 2 }}>
                                                            {schemaInfo.error}
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiMcpToolPreviewDto.java
@@ -3,6 +3,8 @@
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class AiMcpToolPreviewDto {
@@ -14,4 +16,12 @@
    private String inputSchema;
    private Boolean returnDirect;
    private String toolGroup;
    private String toolPurpose;
    private String queryBoundary;
    private List<String> exampleQuestions;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/BuiltinMcpToolRegistry.java
@@ -1,5 +1,6 @@
package com.vincent.rsf.server.ai.service;
import com.vincent.rsf.server.ai.dto.AiMcpToolPreviewDto;
import com.vincent.rsf.server.ai.entity.AiMcpMount;
import org.springframework.ai.tool.ToolCallback;
@@ -10,4 +11,6 @@
    void validateBuiltinCode(String builtinCode);
    List<ToolCallback> createToolCallbacks(AiMcpMount mount, Long userId);
    List<AiMcpToolPreviewDto> listBuiltinToolCatalog(String builtinCode);
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java
@@ -72,7 +72,10 @@
        AiMcpMount mount = requireMount(mountId, tenantId);
        long startedAt = System.currentTimeMillis();
        try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) {
            List<AiMcpToolPreviewDto> tools = buildToolPreviewDtos(runtime.getToolCallbacks());
            List<AiMcpToolPreviewDto> tools = buildToolPreviewDtos(runtime.getToolCallbacks(),
                    AiDefaults.MCP_TRANSPORT_BUILTIN.equals(mount.getTransportType())
                            ? builtinMcpToolRegistry.listBuiltinToolCatalog(mount.getBuiltinCode())
                            : List.of());
            if (!runtime.getErrors().isEmpty()) {
                String message = String.join(";", runtime.getErrors());
                updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, System.currentTimeMillis() - startedAt);
@@ -274,17 +277,29 @@
        }
    }
    private List<AiMcpToolPreviewDto> buildToolPreviewDtos(ToolCallback[] callbacks) {
    private List<AiMcpToolPreviewDto> buildToolPreviewDtos(ToolCallback[] callbacks, List<AiMcpToolPreviewDto> governedCatalog) {
        List<AiMcpToolPreviewDto> tools = new ArrayList<>();
        Map<String, AiMcpToolPreviewDto> catalogMap = new java.util.LinkedHashMap<>();
        for (AiMcpToolPreviewDto item : governedCatalog) {
            if (item == null || !StringUtils.hasText(item.getName())) {
                continue;
            }
            catalogMap.put(item.getName(), item);
        }
        for (ToolCallback callback : callbacks) {
            if (callback == null || callback.getToolDefinition() == null) {
                continue;
            }
            AiMcpToolPreviewDto governedItem = catalogMap.get(callback.getToolDefinition().name());
            tools.add(AiMcpToolPreviewDto.builder()
                    .name(callback.getToolDefinition().name())
                    .description(callback.getToolDefinition().description())
                    .inputSchema(callback.getToolDefinition().inputSchema())
                    .returnDirect(callback.getToolMetadata() == null ? null : callback.getToolMetadata().returnDirect())
                    .toolGroup(governedItem == null ? null : governedItem.getToolGroup())
                    .toolPurpose(governedItem == null ? null : governedItem.getToolPurpose())
                    .queryBoundary(governedItem == null ? null : governedItem.getQueryBoundary())
                    .exampleQuestions(governedItem == null ? List.of() : governedItem.getExampleQuestions())
                    .build());
        }
        return tools;
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/BuiltinMcpToolRegistryImpl.java
@@ -2,6 +2,7 @@
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;
@@ -15,7 +16,9 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
@@ -41,21 +44,52 @@
        validateBuiltinCode(builtinCode);
        if (AiDefaults.MCP_BUILTIN_RSF_WMS.equals(builtinCode)) {
            List<ToolCallback> callbacks = new ArrayList<>();
            callbacks.addAll(Arrays.asList(ToolCallbacks.from(rsfWmsStockTools)));
            callbacks.addAll(Arrays.asList(ToolCallbacks.from(rsfWmsTaskTools)));
            callbacks.addAll(Arrays.asList(ToolCallbacks.from(rsfWmsBaseTools)));
            callbacks.addAll(createValidatedCallbacks(rsfWmsStockTools, AiDefaults.MCP_BUILTIN_RSF_WMS_STOCK));
            callbacks.addAll(createValidatedCallbacks(rsfWmsTaskTools, AiDefaults.MCP_BUILTIN_RSF_WMS_TASK));
            callbacks.addAll(createValidatedCallbacks(rsfWmsBaseTools, AiDefaults.MCP_BUILTIN_RSF_WMS_BASE));
            return callbacks;
        }
        if (AiDefaults.MCP_BUILTIN_RSF_WMS_STOCK.equals(builtinCode)) {
            return Arrays.asList(ToolCallbacks.from(rsfWmsStockTools));
            return createValidatedCallbacks(rsfWmsStockTools, builtinCode);
        }
        if (AiDefaults.MCP_BUILTIN_RSF_WMS_TASK.equals(builtinCode)) {
            return Arrays.asList(ToolCallbacks.from(rsfWmsTaskTools));
            return createValidatedCallbacks(rsfWmsTaskTools, builtinCode);
        }
        if (AiDefaults.MCP_BUILTIN_RSF_WMS_BASE.equals(builtinCode)) {
            return Arrays.asList(ToolCallbacks.from(rsfWmsBaseTools));
            return createValidatedCallbacks(rsfWmsBaseTools, builtinCode);
        }
        throw new CoolException("不支持的内置 MCP 编码: " + builtinCode);
    }
    @Override
    public List<AiMcpToolPreviewDto> listBuiltinToolCatalog(String builtinCode) {
        validateBuiltinCode(builtinCode);
        if (AiDefaults.MCP_BUILTIN_RSF_WMS.equals(builtinCode)) {
            List<AiMcpToolPreviewDto> catalog = new ArrayList<>();
            catalog.addAll(catalogByBuiltinCode(AiDefaults.MCP_BUILTIN_RSF_WMS_STOCK).values());
            catalog.addAll(catalogByBuiltinCode(AiDefaults.MCP_BUILTIN_RSF_WMS_TASK).values());
            catalog.addAll(catalogByBuiltinCode(AiDefaults.MCP_BUILTIN_RSF_WMS_BASE).values());
            return catalog;
        }
        return new ArrayList<>(catalogByBuiltinCode(builtinCode).values());
    }
    private List<ToolCallback> createValidatedCallbacks(Object toolBean, String builtinCode) {
        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() {
@@ -66,4 +100,80 @@
                AiDefaults.MCP_BUILTIN_RSF_WMS_BASE
        );
    }
    private Map<String, AiMcpToolPreviewDto> catalogByBuiltinCode(String builtinCode) {
        if (AiDefaults.MCP_BUILTIN_RSF_WMS_STOCK.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 类型的作业站点")
            ));
            return catalog;
        }
        if (AiDefaults.MCP_BUILTIN_RSF_WMS_TASK.equals(builtinCode)) {
            Map<String, AiMcpToolPreviewDto> catalog = new LinkedHashMap<>();
            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 查看执行明细")
            ));
            return catalog;
        }
        if (AiDefaults.MCP_BUILTIN_RSF_WMS_BASE.equals(builtinCode)) {
            Map<String, AiMcpToolPreviewDto> catalog = new LinkedHashMap<>();
            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();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/tool/BuiltinToolGovernanceSupport.java
New file
@@ -0,0 +1,67 @@
package com.vincent.rsf.server.ai.tool;
import com.vincent.rsf.framework.exception.CoolException;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
public final class BuiltinToolGovernanceSupport {
    private BuiltinToolGovernanceSupport() {
    }
    public static int normalizeLimit(Integer limit, int defaultValue, int maxValue) {
        if (limit == null) {
            return defaultValue;
        }
        if (limit < 1 || limit > maxValue) {
            throw new CoolException("limit 必须在 1 到 " + maxValue + " 之间");
        }
        return limit;
    }
    public static void requireAnyFilter(String message, String... values) {
        if (values == null || values.length == 0) {
            throw new CoolException(message);
        }
        for (String value : values) {
            if (StringUtils.hasText(value)) {
                return;
            }
        }
        throw new CoolException(message);
    }
    public static String sanitizeQueryText(String value, String fieldLabel, int maxLength) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        String normalized = value.trim();
        if (normalized.length() > maxLength) {
            throw new CoolException(fieldLabel + "长度不能超过 " + maxLength);
        }
        return normalized;
    }
    public static List<String> sanitizeStringList(List<String> values, String fieldLabel, int maxSize, int maxItemLength) {
        if (values == null || values.isEmpty()) {
            throw new CoolException(fieldLabel + "不能为空");
        }
        if (values.size() > maxSize) {
            throw new CoolException(fieldLabel + "数量不能超过 " + maxSize);
        }
        List<String> result = new ArrayList<>();
        for (String value : values) {
            String normalized = sanitizeQueryText(value, fieldLabel + "项", maxItemLength);
            if (!StringUtils.hasText(normalized)) {
                continue;
            }
            result.add(normalized);
        }
        if (result.isEmpty()) {
            throw new CoolException(fieldLabel + "不能为空");
        }
        return result;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/tool/RsfWmsBaseTools.java
@@ -27,18 +27,21 @@
    private final BasStationService basStationService;
    private final DictDataService dictDataService;
    @Tool(name = "rsf_query_warehouses", description = "按仓库编码或名称查询仓库基础信息。")
    @Tool(name = "rsf_query_warehouses", description = "只读查询工具。按仓库编码或名称查询仓库基础信息。")
    public List<Map<String, Object>> queryWarehouses(
            @ToolParam(description = "仓库编码,可选") String code,
            @ToolParam(description = "仓库名称,可选") String name,
            @ToolParam(description = "返回条数,默认 10,最大 50") Integer limit) {
        String normalizedCode = BuiltinToolGovernanceSupport.sanitizeQueryText(code, "仓库编码", 64);
        String normalizedName = BuiltinToolGovernanceSupport.sanitizeQueryText(name, "仓库名称", 100);
        BuiltinToolGovernanceSupport.requireAnyFilter("仓库查询至少需要提供仓库编码或名称", normalizedCode, normalizedName);
        LambdaQueryWrapper<Warehouse> queryWrapper = new LambdaQueryWrapper<>();
        int finalLimit = normalizeLimit(limit, 10, 50);
        if (StringUtils.hasText(code)) {
            queryWrapper.like(Warehouse::getCode, code);
        int finalLimit = BuiltinToolGovernanceSupport.normalizeLimit(limit, 10, 50);
        if (StringUtils.hasText(normalizedCode)) {
            queryWrapper.like(Warehouse::getCode, normalizedCode);
        }
        if (StringUtils.hasText(name)) {
            queryWrapper.like(Warehouse::getName, name);
        if (StringUtils.hasText(normalizedName)) {
            queryWrapper.like(Warehouse::getName, normalizedName);
        }
        queryWrapper.orderByAsc(Warehouse::getCode).last("LIMIT " + finalLimit);
        List<Warehouse> warehouses = warehouseService.list(queryWrapper);
@@ -58,22 +61,27 @@
        return result;
    }
    @Tool(name = "rsf_query_bas_stations", description = "按站点编号、站点名称或使用状态查询基础站点。")
    @Tool(name = "rsf_query_bas_stations", description = "只读查询工具。按站点编号、站点名称或使用状态查询基础站点。")
    public List<Map<String, Object>> queryBasStations(
            @ToolParam(description = "站点编号,可选") String stationName,
            @ToolParam(description = "站点名称,可选") String stationId,
            @ToolParam(description = "站点名称,可选") String stationName,
            @ToolParam(description = "站点编号,可选") String stationId,
            @ToolParam(description = "使用状态,可选") String useStatus,
            @ToolParam(description = "返回条数,默认 10,最大 50") Integer limit) {
        String normalizedStationName = BuiltinToolGovernanceSupport.sanitizeQueryText(stationName, "站点名称", 100);
        String normalizedStationId = BuiltinToolGovernanceSupport.sanitizeQueryText(stationId, "站点编号", 64);
        String normalizedUseStatus = BuiltinToolGovernanceSupport.sanitizeQueryText(useStatus, "使用状态", 32);
        BuiltinToolGovernanceSupport.requireAnyFilter("基础站点查询至少需要提供站点名称、站点编号或使用状态",
                normalizedStationName, normalizedStationId, normalizedUseStatus);
        LambdaQueryWrapper<BasStation> queryWrapper = new LambdaQueryWrapper<>();
        int finalLimit = normalizeLimit(limit, 10, 50);
        if (StringUtils.hasText(stationName)) {
            queryWrapper.like(BasStation::getStationName, stationName);
        int finalLimit = BuiltinToolGovernanceSupport.normalizeLimit(limit, 10, 50);
        if (StringUtils.hasText(normalizedStationName)) {
            queryWrapper.like(BasStation::getStationName, normalizedStationName);
        }
        if (StringUtils.hasText(stationId)) {
            queryWrapper.like(BasStation::getStationId, stationId);
        if (StringUtils.hasText(normalizedStationId)) {
            queryWrapper.like(BasStation::getStationId, normalizedStationId);
        }
        if (StringUtils.hasText(useStatus)) {
            queryWrapper.eq(BasStation::getUseStatus, useStatus);
        if (StringUtils.hasText(normalizedUseStatus)) {
            queryWrapper.eq(BasStation::getUseStatus, normalizedUseStatus);
        }
        queryWrapper.orderByAsc(BasStation::getStationName).last("LIMIT " + finalLimit);
        List<BasStation> stations = basStationService.list(queryWrapper);
@@ -98,23 +106,26 @@
        return result;
    }
    @Tool(name = "rsf_query_dict_data", description = "根据字典类型编码查询字典数据,可按值或标签进一步过滤。")
    @Tool(name = "rsf_query_dict_data", description = "只读查询工具。根据字典类型编码查询字典数据,可按值或标签进一步过滤。")
    public List<Map<String, Object>> queryDictData(
            @ToolParam(required = true, description = "字典类型编码") String dictTypeCode,
            @ToolParam(description = "字典值,可选") String value,
            @ToolParam(description = "字典标签,可选") String label,
            @ToolParam(description = "返回条数,默认 20,最大 100") Integer limit) {
        if (!StringUtils.hasText(dictTypeCode)) {
        String normalizedDictTypeCode = BuiltinToolGovernanceSupport.sanitizeQueryText(dictTypeCode, "字典类型编码", 64);
        String normalizedValue = BuiltinToolGovernanceSupport.sanitizeQueryText(value, "字典值", 64);
        String normalizedLabel = BuiltinToolGovernanceSupport.sanitizeQueryText(label, "字典标签", 100);
        if (!StringUtils.hasText(normalizedDictTypeCode)) {
            throw new CoolException("字典类型编码不能为空");
        }
        int finalLimit = normalizeLimit(limit, 20, 100);
        int finalLimit = BuiltinToolGovernanceSupport.normalizeLimit(limit, 20, 100);
        LambdaQueryWrapper<DictData> queryWrapper = new LambdaQueryWrapper<DictData>()
                .eq(DictData::getDictTypeCode, dictTypeCode);
        if (StringUtils.hasText(value)) {
            queryWrapper.like(DictData::getValue, value);
                .eq(DictData::getDictTypeCode, normalizedDictTypeCode);
        if (StringUtils.hasText(normalizedValue)) {
            queryWrapper.like(DictData::getValue, normalizedValue);
        }
        if (StringUtils.hasText(label)) {
            queryWrapper.like(DictData::getLabel, label);
        if (StringUtils.hasText(normalizedLabel)) {
            queryWrapper.like(DictData::getLabel, normalizedLabel);
        }
        queryWrapper.orderByAsc(DictData::getSort).last("LIMIT " + finalLimit);
        List<DictData> dictDataList = dictDataService.list(queryWrapper);
@@ -135,13 +146,4 @@
        return result;
    }
    private int normalizeLimit(Integer limit, int defaultValue, int maxValue) {
        if (limit == null) {
            return defaultValue;
        }
        if (limit < 1 || limit > maxValue) {
            throw new CoolException("limit 必须在 1 到 " + maxValue + " 之间");
        }
        return limit;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/tool/RsfWmsStockTools.java
@@ -27,23 +27,26 @@
    private final LocItemService locItemService;
    private final DeviceSiteService deviceSiteService;
    @Tool(name = "rsf_query_available_inventory", description = "根据物料编码或物料名称查询当前在库且可用于出库的库存明细。")
    @Tool(name = "rsf_query_available_inventory", description = "只读查询工具。根据物料编码或物料名称查询当前在库且可用于出库的库存明细。")
    public List<Map<String, Object>> queryAvailableInventory(
            @ToolParam(description = "物料编码,优先使用") String matnr,
            @ToolParam(description = "物料名称,当没有物料编码时使用") String maktx) {
        if (!StringUtils.hasText(matnr) && !StringUtils.hasText(maktx)) {
            throw new CoolException("物料编码或物料名称至少需要提供一个");
        }
            @ToolParam(description = "物料名称,当没有物料编码时使用") String maktx,
            @ToolParam(description = "返回条数,默认 10,最大 50") Integer limit) {
        String normalizedMatnr = BuiltinToolGovernanceSupport.sanitizeQueryText(matnr, "物料编码", 64);
        String normalizedMaktx = BuiltinToolGovernanceSupport.sanitizeQueryText(maktx, "物料名称", 100);
        BuiltinToolGovernanceSupport.requireAnyFilter("物料编码或物料名称至少需要提供一个", normalizedMatnr, normalizedMaktx);
        int finalLimit = BuiltinToolGovernanceSupport.normalizeLimit(limit, 10, 50);
        LambdaQueryWrapper<LocItem> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.hasText(matnr)) {
            queryWrapper.eq(LocItem::getMatnrCode, matnr);
        if (StringUtils.hasText(normalizedMatnr)) {
            queryWrapper.eq(LocItem::getMatnrCode, normalizedMatnr);
        } else {
            queryWrapper.eq(LocItem::getMaktx, maktx);
            queryWrapper.like(LocItem::getMaktx, normalizedMaktx);
        }
        queryWrapper.apply(
                "EXISTS (SELECT 1 FROM man_loc ml WHERE ml.use_status = {0} AND ml.id = man_loc_item.loc_id)",
                LocStsType.LOC_STS_TYPE_F.type
        );
        queryWrapper.orderByDesc(LocItem::getId).last("LIMIT " + finalLimit);
        List<LocItem> locItems = locItemService.list(queryWrapper);
        List<Map<String, Object>> result = new ArrayList<>();
        for (LocItem locItem : locItems) {
@@ -69,14 +72,17 @@
        return result;
    }
    @Tool(name = "rsf_query_station_list", description = "根据作业类型列表查询可用站点,返回站点编号、名称、目标位置和状态等信息。")
    @Tool(name = "rsf_query_station_list", description = "只读查询工具。根据作业类型列表查询可用站点,返回站点编号、名称、目标位置和状态等信息。")
    public List<Map<String, Object>> queryStationList(
            @ToolParam(required = true, description = "作业类型列表") List<String> types) {
        if (types == null || types.isEmpty()) {
            throw new CoolException("站点类型列表不能为空");
        }
            @ToolParam(required = true, description = "作业类型列表") List<String> types,
            @ToolParam(description = "返回条数,默认 20,最大 50") Integer limit) {
        List<String> normalizedTypes = BuiltinToolGovernanceSupport.sanitizeStringList(types, "站点类型列表", 10, 32);
        int finalLimit = BuiltinToolGovernanceSupport.normalizeLimit(limit, 20, 50);
        List<DeviceSite> sites = deviceSiteService.list(new LambdaQueryWrapper<DeviceSite>()
                .in(DeviceSite::getType, types));
                .in(DeviceSite::getType, normalizedTypes)
                .orderByAsc(DeviceSite::getType)
                .orderByAsc(DeviceSite::getSite)
                .last("LIMIT " + finalLimit));
        List<Map<String, Object>> result = new ArrayList<>();
        for (DeviceSite site : sites) {
            Map<String, Object> item = new LinkedHashMap<>();
rsf-server/src/main/java/com/vincent/rsf/server/ai/tool/RsfWmsTaskTools.java
@@ -21,7 +21,7 @@
    private final TaskService taskService;
    @Tool(name = "rsf_query_task_list", description = "按任务号、状态、任务类型、源站点、目标站点等条件查询任务列表。")
    @Tool(name = "rsf_query_task_list", description = "只读查询工具。按任务号、状态、任务类型、源站点、目标站点等条件查询任务列表。")
    public List<Map<String, Object>> queryTaskList(
            @ToolParam(description = "任务号,可模糊查询") String taskCode,
            @ToolParam(description = "任务状态,可选") Integer taskStatus,
@@ -29,10 +29,17 @@
            @ToolParam(description = "源站点,可选") String orgSite,
            @ToolParam(description = "目标站点,可选") String targSite,
            @ToolParam(description = "返回条数,默认 10,最大 50") Integer limit) {
        String normalizedTaskCode = BuiltinToolGovernanceSupport.sanitizeQueryText(taskCode, "任务号", 64);
        String normalizedOrgSite = BuiltinToolGovernanceSupport.sanitizeQueryText(orgSite, "源站点", 64);
        String normalizedTargSite = BuiltinToolGovernanceSupport.sanitizeQueryText(targSite, "目标站点", 64);
        BuiltinToolGovernanceSupport.requireAnyFilter("任务列表查询至少需要提供一个过滤条件",
                normalizedTaskCode, normalizedOrgSite, normalizedTargSite,
                taskStatus == null ? null : String.valueOf(taskStatus),
                taskType == null ? null : String.valueOf(taskType));
        LambdaQueryWrapper<Task> queryWrapper = new LambdaQueryWrapper<>();
        int finalLimit = normalizeLimit(limit, 10, 50);
        if (StringUtils.hasText(taskCode)) {
            queryWrapper.like(Task::getTaskCode, taskCode);
        int finalLimit = BuiltinToolGovernanceSupport.normalizeLimit(limit, 10, 50);
        if (StringUtils.hasText(normalizedTaskCode)) {
            queryWrapper.like(Task::getTaskCode, normalizedTaskCode);
        }
        if (taskStatus != null) {
            queryWrapper.eq(Task::getTaskStatus, taskStatus);
@@ -40,11 +47,11 @@
        if (taskType != null) {
            queryWrapper.eq(Task::getTaskType, taskType);
        }
        if (StringUtils.hasText(orgSite)) {
            queryWrapper.eq(Task::getOrgSite, orgSite);
        if (StringUtils.hasText(normalizedOrgSite)) {
            queryWrapper.eq(Task::getOrgSite, normalizedOrgSite);
        }
        if (StringUtils.hasText(targSite)) {
            queryWrapper.eq(Task::getTargSite, targSite);
        if (StringUtils.hasText(normalizedTargSite)) {
            queryWrapper.eq(Task::getTargSite, normalizedTargSite);
        }
        queryWrapper.orderByDesc(Task::getCreateTime).last("LIMIT " + finalLimit);
        List<Task> tasks = taskService.list(queryWrapper);
@@ -55,18 +62,19 @@
        return result;
    }
    @Tool(name = "rsf_query_task_detail", description = "根据任务 ID 或任务号查询任务详情。")
    @Tool(name = "rsf_query_task_detail", description = "只读查询工具。根据任务 ID 或任务号查询任务详情。")
    public Map<String, Object> queryTaskDetail(
            @ToolParam(description = "任务 ID") Long taskId,
            @ToolParam(description = "任务号") String taskCode) {
        if (taskId == null && !StringUtils.hasText(taskCode)) {
        String normalizedTaskCode = BuiltinToolGovernanceSupport.sanitizeQueryText(taskCode, "任务号", 64);
        if (taskId == null && !StringUtils.hasText(normalizedTaskCode)) {
            throw new CoolException("任务 ID 和任务号至少需要提供一个");
        }
        Task task;
        if (taskId != null) {
            task = taskService.getById(taskId);
        } else {
            task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getTaskCode, taskCode));
            task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getTaskCode, normalizedTaskCode));
        }
        if (task == null) {
            throw new CoolException("未查询到任务");
@@ -111,13 +119,4 @@
        return item;
    }
    private int normalizeLimit(Integer limit, int defaultValue, int maxValue) {
        if (limit == null) {
            return defaultValue;
        }
        if (limit < 1 || limit > maxValue) {
            throw new CoolException("limit 必须在 1 到 " + maxValue + " 之间");
        }
        return limit;
    }
}