package com.vincent.rsf.server.system.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.vincent.rsf.framework.common.Cools; import com.vincent.rsf.framework.common.R; import com.vincent.rsf.framework.common.SnowflakeIdWorker; import com.vincent.rsf.server.ai.constant.AiMcpConstants; import com.vincent.rsf.server.ai.constant.AiSceneCode; import com.vincent.rsf.server.ai.service.mcp.AiMcpPayloadMapper; import com.vincent.rsf.server.ai.service.mcp.AiMcpRegistryService; import com.vincent.rsf.server.common.annotation.OperationLog; import com.vincent.rsf.server.common.domain.BaseParam; import com.vincent.rsf.server.common.domain.PageParam; import com.vincent.rsf.server.common.utils.ExcelUtil; import com.vincent.rsf.server.system.entity.AiDiagnosticToolConfig; import com.vincent.rsf.server.system.entity.AiMcpMount; import com.vincent.rsf.server.system.service.AiDiagnosticToolConfigService; import com.vincent.rsf.server.system.service.AiMcpMountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @RestController public class AiMcpMountController extends BaseController { @Autowired private AiMcpMountService aiMcpMountService; @Autowired private AiMcpRegistryService aiMcpRegistryService; @Autowired private AiDiagnosticToolConfigService aiDiagnosticToolConfigService; @Autowired private SnowflakeIdWorker snowflakeIdWorker; @Autowired private AiMcpPayloadMapper aiMcpPayloadMapper; @PreAuthorize("hasAuthority('system:aiMcpMount:list')") @PostMapping("/aiMcpMount/page") public R page(@RequestBody Map map) { BaseParam baseParam = buildParam(map, BaseParam.class); PageParam pageParam = new PageParam<>(baseParam, AiMcpMount.class); com.baomidou.mybatisplus.core.conditions.query.QueryWrapper wrapper = pageParam.buildWrapper(true); wrapper.eq("tenant_id", getTenantId()); return R.ok().add(aiMcpMountService.page(pageParam, wrapper)); } @PreAuthorize("hasAuthority('system:aiMcpMount:list')") @GetMapping("/aiMcpMount/{id}") public R get(@PathVariable("id") Long id) { AiMcpMount mount = aiMcpMountService.getTenantMount(getTenantId(), id); if (mount == null) { return R.error("mount not found"); } return R.ok().add(mount); } @PreAuthorize("hasAuthority('system:aiMcpMount:save')") @OperationLog("Create AiMcpMount") @PostMapping("/aiMcpMount/save") public R save(@RequestBody AiMcpMount mount) { if (Cools.isEmpty(mount.getName()) || Cools.isEmpty(mount.getMountCode()) || Cools.isEmpty(mount.getTransportType())) { return R.error("名称、挂载编码和传输类型不能为空"); } Date now = new Date(); mount.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3)); mount.setEnabledFlag(mount.getEnabledFlag() == null ? 1 : mount.getEnabledFlag()); mount.setTimeoutMs(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs()); mount.setStatus(mount.getStatus() == null ? 1 : mount.getStatus()); mount.setTransportType(mount.getTransportType().toUpperCase()); if (AiMcpConstants.TRANSPORT_INTERNAL.equals(mount.getTransportType())) { mount.setUrl("/ai/mcp"); } mount.setTenantId(getTenantId()); mount.setCreateBy(getLoginUserId()); mount.setCreateTime(now); mount.setUpdateBy(getLoginUserId()); mount.setUpdateTime(now); if (!aiMcpMountService.save(mount)) { return R.error("Save Fail"); } return R.ok("Save Success").add(mount); } @PreAuthorize("hasAuthority('system:aiMcpMount:update')") @OperationLog("Update AiMcpMount") @PostMapping("/aiMcpMount/update") public R update(@RequestBody AiMcpMount mount) { AiMcpMount existed = aiMcpMountService.getTenantMount(getTenantId(), mount.getId()); if (existed == null) { return R.error("mount not found"); } if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(existed.getTransportType())) { return R.error("内置挂载由系统托管,不支持直接编辑"); } if (Cools.isEmpty(mount.getName()) || Cools.isEmpty(mount.getMountCode()) || Cools.isEmpty(mount.getTransportType())) { return R.error("名称、挂载编码和传输类型不能为空"); } mount.setTransportType(mount.getTransportType().toUpperCase()); if (AiMcpConstants.TRANSPORT_INTERNAL.equals(mount.getTransportType())) { mount.setUrl("/ai/mcp"); } mount.setTenantId(getTenantId()); mount.setCreateBy(existed.getCreateBy()); mount.setCreateTime(existed.getCreateTime()); mount.setLastTestResult(existed.getLastTestResult()); mount.setLastTestTime(existed.getLastTestTime()); mount.setLastTestMessage(existed.getLastTestMessage()); mount.setLastToolCount(existed.getLastToolCount()); mount.setUpdateBy(getLoginUserId()); mount.setUpdateTime(new Date()); if (!aiMcpMountService.updateById(mount)) { return R.error("Update Fail"); } return R.ok("Update Success").add(mount); } @PreAuthorize("hasAuthority('system:aiMcpMount:remove')") @OperationLog("Delete AiMcpMount") @PostMapping("/aiMcpMount/remove/{ids}") public R remove(@PathVariable Long[] ids) { List idList = Arrays.asList(ids); List mounts = aiMcpMountService.list(new LambdaQueryWrapper() .eq(AiMcpMount::getTenantId, getTenantId()) .in(AiMcpMount::getId, idList)); for (AiMcpMount mount : mounts) { if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) { return R.error("内置挂载由系统托管,不支持删除"); } } if (mounts.size() != idList.size() || !aiMcpMountService.removeByIds(idList)) { return R.error("Delete Fail"); } return R.ok("Delete Success").add(ids); } @PreAuthorize("hasAuthority('system:aiMcpMount:list')") @PostMapping("/aiMcpMount/export") public void export(@RequestBody Map map, HttpServletResponse response) throws Exception { ExcelUtil.build(ExcelUtil.create(aiMcpMountService.list(new LambdaQueryWrapper() .eq(AiMcpMount::getTenantId, getTenantId())), AiMcpMount.class), response); } @PreAuthorize("hasAuthority('system:aiMcpMount:list')") @GetMapping("/ai/mcp/mount/list") public R list() { aiMcpRegistryService.ensureDefaultMount(getTenantId(), getLoginUserId()); return R.ok().add(aiMcpMountService.listTenantMounts(getTenantId())); } @PreAuthorize("hasAnyAuthority('system:aiMcpMount:save','system:aiMcpMount:update')") @PostMapping("/ai/mcp/mount/save") public R customSave(@RequestBody AiMcpMount mount) { if (mount.getId() == null) { return save(mount); } return update(mount); } @PreAuthorize("hasAuthority('system:aiMcpMount:update')") @PostMapping("/ai/mcp/mount/initDefaults") public R initDefaults() { aiMcpRegistryService.ensureDefaultMount(getTenantId(), getLoginUserId()); return R.ok(); } @PreAuthorize("hasAuthority('system:aiMcpMount:list')") @GetMapping("/ai/mcp/mount/toolList") public R toolList(@RequestParam(required = false) Long mountId) { return R.ok().add(aiMcpRegistryService.listTools(getTenantId(), mountId)); } @PreAuthorize("hasAuthority('system:aiMcpMount:update')") @PostMapping("/ai/mcp/mount/test") public R test(@RequestBody Map map) { Long id = Long.valueOf(String.valueOf(map.get("id"))); return R.ok().add(aiMcpRegistryService.testMount(getTenantId(), id)); } @PreAuthorize("hasAuthority('system:aiMcpMount:list')") @PostMapping("/ai/mcp/mount/toolPreview") public R toolPreview(@RequestBody Map map) { return R.ok().add(aiMcpRegistryService.previewTool( getTenantId(), String.valueOf(map.get("mountCode")), String.valueOf(map.get("toolCode")), map.get("sceneCode") == null ? null : String.valueOf(map.get("sceneCode")), map.get("question") == null ? null : String.valueOf(map.get("question")) )); } @PreAuthorize("hasAuthority('system:aiMcpMount:list')") @GetMapping("/ai/mcp/console/overview") public R consoleOverview() { aiMcpRegistryService.ensureDefaultMount(getTenantId(), getLoginUserId()); Map payload = new LinkedHashMap<>(); payload.put("builtInMount", null); payload.put("builtInTools", new java.util.ArrayList<>()); payload.put("externalServices", new java.util.ArrayList<>()); for (AiMcpMount mount : aiMcpMountService.listTenantMounts(getTenantId())) { if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) { payload.put("builtInMount", buildMountView(mount, true)); payload.put("builtInTools", buildBuiltInToolViews()); } else { ((List) payload.get("externalServices")).add(buildMountView(mount, false)); } } return R.ok().add(payload); } @PreAuthorize("hasAnyAuthority('system:aiMcpMount:save','system:aiMcpMount:update')") @PostMapping("/ai/mcp/console/service/save") public R consoleSaveService(@RequestBody AiMcpMount mount) { if (Cools.isEmpty(mount.getName()) || Cools.isEmpty(mount.getUrl())) { return R.error("名称和地址不能为空"); } Date now = new Date(); AiMcpMount existed = mount.getId() == null ? null : aiMcpMountService.getTenantMount(getTenantId(), mount.getId()); if (existed != null && AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(existed.getTransportType())) { return R.error("内置挂载由系统托管,不支持编辑"); } String transportType = Cools.isEmpty(mount.getTransportType()) ? AiMcpConstants.TRANSPORT_AUTO : mount.getTransportType().toUpperCase(); String usageScope = normalizeUsageScope(mount.getUsageScope()); AiMcpMount target = existed == null ? new AiMcpMount() : existed; target.setName(mount.getName()); target.setUrl(mount.getUrl()); target.setTransportType(transportType); target.setAuthType(Cools.isEmpty(mount.getAuthType()) ? AiMcpConstants.AUTH_TYPE_NONE : mount.getAuthType().toUpperCase()); target.setAuthValue(mount.getAuthValue()); target.setUsageScope(usageScope); target.setEnabledFlag(mount.getEnabledFlag() == null ? 1 : mount.getEnabledFlag()); target.setTimeoutMs(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs()); target.setStatus(mount.getStatus() == null ? 1 : mount.getStatus()); target.setMemo(mount.getMemo()); target.setTenantId(getTenantId()); target.setUpdateBy(getLoginUserId()); target.setUpdateTime(now); if (existed == null) { target.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3)); target.setMountCode(generateMountCode(target.getName())); target.setCreateBy(getLoginUserId()); target.setCreateTime(now); if (!aiMcpMountService.save(target)) { return R.error("Save Fail"); } } else if (!aiMcpMountService.updateById(target)) { return R.error("Update Fail"); } return R.ok().add(buildMountView(target, false)); } @PreAuthorize("hasAuthority('system:aiMcpMount:update')") @PostMapping("/ai/mcp/console/service/test") public R consoleTestService(@RequestBody Map map) { Long id = Long.valueOf(String.valueOf(map.get("id"))); Map result = (Map) aiMcpRegistryService.testMount(getTenantId(), id); AiMcpMount mount = aiMcpMountService.getTenantMount(getTenantId(), id); if (mount == null) { return R.error("mount not found"); } result.put("service", buildMountView(mount, false)); return R.ok().add(result); } @PreAuthorize("hasAuthority('system:aiMcpMount:remove')") @PostMapping("/ai/mcp/console/service/remove/{id}") public R consoleRemoveService(@PathVariable("id") Long id) { AiMcpMount mount = aiMcpMountService.getTenantMount(getTenantId(), id); if (mount == null) { return R.error("mount not found"); } if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) { return R.error("内置挂载由系统托管,不支持删除"); } return aiMcpMountService.removeById(id) ? R.ok() : R.error("Delete Fail"); } @PreAuthorize("hasAnyAuthority('system:aiToolConfig:save','system:aiToolConfig:update')") @PostMapping("/ai/mcp/console/builtin-tool/save") public R consoleSaveBuiltInTool(@RequestBody AiDiagnosticToolConfig config) { if (Cools.isEmpty(config.getToolCode())) { return R.error("工具编码不能为空"); } String usageScope = aiMcpPayloadMapper.normalizeUsageScope(config.getUsageScope()); String sceneCode = resolveSceneCodeByUsageScope(usageScope); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(AiDiagnosticToolConfig::getTenantId, getTenantId()) .eq(AiDiagnosticToolConfig::getToolCode, config.getToolCode()); if (Cools.isEmpty(sceneCode)) { wrapper.and(w -> w.isNull(AiDiagnosticToolConfig::getSceneCode).or().eq(AiDiagnosticToolConfig::getSceneCode, "")); } else { wrapper.eq(AiDiagnosticToolConfig::getSceneCode, sceneCode); } AiDiagnosticToolConfig existed = aiDiagnosticToolConfigService.getOne(wrapper.last("limit 1")); Date now = new Date(); AiDiagnosticToolConfig target = existed == null ? new AiDiagnosticToolConfig() : existed; if (existed == null) { target.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3)); target.setCreateBy(getLoginUserId()); target.setCreateTime(now); } target .setToolCode(config.getToolCode()) .setToolName(config.getToolName()) .setSceneCode(sceneCode) .setUsageScope(usageScope) .setEnabledFlag(AiMcpConstants.USAGE_SCOPE_DISABLED.equals(usageScope) ? 0 : (config.getEnabledFlag() == null ? 1 : config.getEnabledFlag())) .setPriority(config.getPriority() == null ? 10 : config.getPriority()) .setToolPrompt(config.getToolPrompt()) .setStatus(config.getStatus() == null ? 1 : config.getStatus()) .setTenantId(getTenantId()) .setUpdateBy(getLoginUserId()) .setUpdateTime(now) .setMemo(config.getMemo()); if (existed == null) { aiDiagnosticToolConfigService.save(target); } else { aiDiagnosticToolConfigService.updateById(target); } return R.ok().add(buildBuiltInToolViews()); } private List> buildBuiltInToolViews() { List> output = new java.util.ArrayList<>(); for (com.vincent.rsf.server.ai.model.AiMcpToolDescriptor descriptor : aiMcpRegistryService.listInternalTools(getTenantId())) { Map item = new LinkedHashMap<>(); item.put("toolCode", descriptor.getToolCode()); item.put("toolName", descriptor.getToolName()); item.put("description", descriptor.getDescription()); item.put("enabledFlag", descriptor.getEnabledFlag()); item.put("priority", descriptor.getPriority()); item.put("toolPrompt", descriptor.getToolPrompt()); item.put("usageScope", aiMcpPayloadMapper.resolveUsageScope(descriptor.getSceneCode(), descriptor.getEnabledFlag(), descriptor.getUsageScope())); item.put("advanced", buildAdvancedFlag(descriptor)); output.add(item); } return output; } private Map buildMountView(AiMcpMount mount, boolean internalManaged) { Map item = new LinkedHashMap<>(); item.put("id", mount.getId()); item.put("name", mount.getName()); item.put("mountCode", mount.getMountCode()); item.put("transportType", mount.getTransportType()); item.put("displayTransportType", AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType()) ? "内置" : mount.getTransportType()); item.put("url", mount.getUrl()); item.put("enabledFlag", mount.getEnabledFlag()); item.put("timeoutMs", mount.getTimeoutMs()); item.put("lastTestResult", mount.getLastTestResult()); item.put("lastTestResult$", mount.getLastTestResult$()); item.put("lastTestTime", mount.getLastTestTime$()); item.put("lastTestMessage", mount.getLastTestMessage()); item.put("lastToolCount", mount.getLastToolCount()); item.put("authType", mount.getAuthType()); item.put("hasAuth", !Cools.isEmpty(mount.getAuthValue())); item.put("usageScope", normalizeUsageScope(mount.getUsageScope())); item.put("internalManaged", internalManaged); return item; } private boolean buildAdvancedFlag(com.vincent.rsf.server.ai.model.AiMcpToolDescriptor descriptor) { return descriptor.getPriority() != null && descriptor.getPriority() != 10 || (descriptor.getToolPrompt() != null && !descriptor.getToolPrompt().trim().isEmpty()); } private String normalizeUsageScope(String usageScope) { return aiMcpPayloadMapper.normalizeUsageScope(usageScope); } private String resolveUsageScope(String sceneCode, Integer enabledFlag) { return aiMcpPayloadMapper.resolveUsageScope(sceneCode, enabledFlag, null); } private String resolveSceneCodeByUsageScope(String usageScope) { return AiMcpConstants.USAGE_SCOPE_DIAGNOSE_ONLY.equals(usageScope) ? AiSceneCode.SYSTEM_DIAGNOSE : ""; } private String generateMountCode(String name) { String normalized = name == null ? "remote_mcp" : name.toLowerCase().replaceAll("[^a-z0-9]+", "_"); normalized = normalized.replaceAll("^_+|_+$", ""); if (normalized.isEmpty()) { normalized = "remote_mcp"; } return normalized + "_" + String.valueOf(System.currentTimeMillis()).substring(7); } }