| | |
| | | <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> |
| | |
| | | </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} |
| | |
| | | import lombok.Builder; |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | @Builder |
| | | public class AiMcpToolPreviewDto { |
| | |
| | | private String inputSchema; |
| | | |
| | | private Boolean returnDirect; |
| | | |
| | | private String toolGroup; |
| | | |
| | | private String toolPurpose; |
| | | |
| | | private String queryBoundary; |
| | | |
| | | private List<String> exampleQuestions; |
| | | } |
| | |
| | | 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; |
| | | |
| | |
| | | void validateBuiltinCode(String builtinCode); |
| | | |
| | | List<ToolCallback> createToolCallbacks(AiMcpMount mount, Long userId); |
| | | |
| | | List<AiMcpToolPreviewDto> listBuiltinToolCatalog(String builtinCode); |
| | | } |
| | |
| | | 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); |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | |
| | | |
| | | 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 java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Service |
| | | @RequiredArgsConstructor |
| | |
| | | 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() { |
| | |
| | | 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(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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); |
| | |
| | | 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); |
| | |
| | | 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); |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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) { |
| | |
| | | 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<>(); |
| | |
| | | |
| | | 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, |
| | |
| | | @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); |
| | |
| | | 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); |
| | |
| | | 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("未查询到任务"); |
| | |
| | | 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; |
| | | } |
| | | } |