package com.zy.ai.mcp.tool; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.zy.ai.domain.autotune.AutoTuneApplyRequest; import com.zy.ai.domain.autotune.AutoTuneApplyResult; import com.zy.ai.domain.autotune.AutoTuneChangeCommand; import com.zy.ai.domain.autotune.AutoTuneSnapshot; import com.zy.ai.entity.AiAutoTuneChange; import com.zy.ai.entity.AiAutoTuneJob; import com.zy.ai.entity.AiAutoTuneMcpCall; import com.zy.ai.enums.AiPromptScene; import com.zy.ai.service.AiAutoTuneChangeService; import com.zy.ai.service.AiAutoTuneJobService; import com.zy.ai.service.AiAutoTuneMcpCallService; import com.zy.ai.service.AutoTuneApplyService; import com.zy.ai.service.AutoTuneSnapshotService; import com.zy.ai.utils.AutoTuneWriteBehaviorUtils; import lombok.RequiredArgsConstructor; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.LongSupplier; @Component @RequiredArgsConstructor public class AutoTuneMcpTools { private static final int DEFAULT_RECENT_JOB_LIMIT = 5; private static final int MAX_RECENT_JOB_LIMIT = 20; private static final long DRY_RUN_TOKEN_TTL_MILLIS = 10L * 60L * 1000L; private final AutoTuneSnapshotService autoTuneSnapshotService; private final AutoTuneApplyService autoTuneApplyService; private final AiAutoTuneJobService aiAutoTuneJobService; private final AiAutoTuneChangeService aiAutoTuneChangeService; private final AiAutoTuneMcpCallService aiAutoTuneMcpCallService; private final ConcurrentMap dryRunPreviews = new ConcurrentHashMap<>(); private LongSupplier currentTimeMillisSupplier = System::currentTimeMillis; @Tool(name = "dispatch_get_auto_tune_snapshot", description = "获取WCS自动调参所需的调度快照、站点运行态、拓扑容量、当前可写参数和调参规则约束") public AutoTuneSnapshot getAutoTuneSnapshot() { return autoTuneSnapshotService.buildSnapshot(); } @Tool(name = "dispatch_get_recent_auto_tune_jobs", description = "获取近期自动调参任务摘要及其变更结果,默认5条,最大20条") public List> getRecentAutoTuneJobs( @ToolParam(description = "返回任务数量上限,默认5,最大20", required = false) Integer limit) { int safeLimit = boundLimit(limit); List jobs = aiAutoTuneJobService.list(new QueryWrapper() .eq("prompt_scene_code", AiPromptScene.AUTO_TUNE_DISPATCH.getCode()) .orderByDesc("start_time") .orderByDesc("id") .last("limit " + safeLimit)); if (jobs == null || jobs.isEmpty()) { return new ArrayList<>(); } List> result = new ArrayList<>(); for (AiAutoTuneJob job : jobs) { result.add(toJobSummary(job)); } return result; } @Tool(name = "dispatch_apply_auto_tune_changes", description = "提交自动调参变更。实际应用前必须先使用 dryRun=true 验证") public AutoTuneApplyResult applyAutoTuneChanges( @ToolParam(description = "本次调参原因或分析摘要", required = false) String reason, @ToolParam(description = "建议自动调参分析间隔分钟", required = false) Integer analysisIntervalMinutes, @ToolParam(description = "触发类型,例如 scheduler/manual/agent", required = false) String triggerType, @ToolParam(description = "是否仅试算,实际应用前必须先传 true", required = false) Boolean dryRun, @ToolParam(description = "dry-run 成功后返回的预览令牌。dryRun=false 时必须提供,且变更集必须完全一致", required = false) String dryRunToken, @ToolParam(description = "调参变更列表") List changes) { if (dryRun == null) { throw new IllegalArgumentException("dryRun is required. Use dryRun=true first to create a preview token."); } AutoTuneApplyRequest request = new AutoTuneApplyRequest(); request.setReason(reason); request.setAnalysisIntervalMinutes(analysisIntervalMinutes); request.setTriggerType(triggerType); request.setDryRun(dryRun); request.setChanges(changes); String fingerprint = buildChangeFingerprint(changes); if (Boolean.FALSE.equals(dryRun)) { requireMatchingDryRunToken(dryRunToken, fingerprint); } AutoTuneApplyResult result = autoTuneApplyService.apply(request); if (Boolean.TRUE.equals(dryRun) && hasApplicableDryRunChanges(result)) { result.setDryRunToken(createDryRunToken(fingerprint)); } return result; } @Tool(name = "dispatch_revert_last_auto_tune_job", description = "回滚最近一次成功的自动调参任务") public AutoTuneApplyResult revertLastAutoTuneJob( @ToolParam(description = "回滚原因,必须说明来自MCP事实的异常证据", required = false) String reason) { return autoTuneApplyService.rollbackLastSuccessfulJob(reason); } private Map toJobSummary(AiAutoTuneJob job) { LinkedHashMap item = new LinkedHashMap<>(); item.put("id", job.getId()); item.put("triggerType", job.getTriggerType()); item.put("status", job.getStatus()); item.put("startTime", job.getStartTime()); item.put("finishTime", job.getFinishTime()); item.put("summary", job.getSummary()); item.put("successCount", job.getSuccessCount()); item.put("rejectCount", job.getRejectCount()); item.put("errorMessage", job.getErrorMessage()); List mcpCalls = listMcpCalls(job.getId()); List> mcpCallSummaries = toMcpCallSummaries(mcpCalls); List> changeSummaries = listChangeSummaries(job, mcpCalls); AutoTuneWriteBehaviorUtils.addWriteBehavior(item, AutoTuneWriteBehaviorUtils.resolveJobWriteBehavior(job, mcpCallSummaries, changeSummaries)); item.put("mcpCallCount", mcpCalls.size()); item.put("mcpCalls", mcpCallSummaries); item.put("changes", changeSummaries); return item; } private List listMcpCalls(Long agentJobId) { if (agentJobId == null) { return new ArrayList<>(); } List mcpCalls = aiAutoTuneMcpCallService.list(new QueryWrapper() .eq("agent_job_id", agentJobId) .orderByAsc("call_seq") .orderByAsc("id")); return mcpCalls == null ? new ArrayList<>() : mcpCalls; } private List> toMcpCallSummaries(List mcpCalls) { List> result = new ArrayList<>(); if (mcpCalls == null || mcpCalls.isEmpty()) { return result; } for (AiAutoTuneMcpCall mcpCall : mcpCalls) { result.add(toMcpCallSummary(mcpCall)); } return result; } private Map toMcpCallSummary(AiAutoTuneMcpCall mcpCall) { LinkedHashMap item = new LinkedHashMap<>(); item.put("callSeq", mcpCall.getCallSeq()); item.put("toolName", mcpCall.getToolName()); item.put("status", mcpCall.getStatus()); item.put("dryRun", toBoolean(mcpCall.getDryRun())); item.put("applyJobId", mcpCall.getApplyJobId()); item.put("successCount", mcpCall.getSuccessCount()); item.put("rejectCount", mcpCall.getRejectCount()); item.put("errorMessage", mcpCall.getErrorMessage()); AutoTuneWriteBehaviorUtils.addWriteBehavior(item, AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(mcpCall)); return item; } private Boolean toBoolean(Integer value) { if (value == null) { return null; } return value == 1; } private List> listChangeSummaries(AiAutoTuneJob job, List mcpCalls) { Map ownerTriggerTypes = collectChangeOwnerTriggerTypes(job, mcpCalls); List applyJobIds = new ArrayList<>(ownerTriggerTypes.keySet()); if (applyJobIds.isEmpty()) { return new ArrayList<>(); } List changes = aiAutoTuneChangeService.list(new QueryWrapper() .in("job_id", applyJobIds) .orderByAsc("job_id") .orderByAsc("id")); if (changes == null || changes.isEmpty()) { return new ArrayList<>(); } List> result = new ArrayList<>(); for (AiAutoTuneChange change : changes) { result.add(toChangeSummary(change, ownerTriggerTypes.get(change.getJobId()))); } return result; } private Map collectChangeOwnerTriggerTypes(AiAutoTuneJob job, List mcpCalls) { LinkedHashMap result = new LinkedHashMap<>(); if (job != null && job.getId() != null) { result.put(job.getId(), job.getTriggerType()); } if (mcpCalls == null || mcpCalls.isEmpty()) { return result; } for (AiAutoTuneMcpCall mcpCall : mcpCalls) { Long applyJobId = mcpCall.getApplyJobId(); if (applyJobId == null || result.containsKey(applyJobId)) { continue; } result.put(applyJobId, resolveMcpApplyJobTriggerType(mcpCall)); } return result; } private String resolveMcpApplyJobTriggerType(AiAutoTuneMcpCall mcpCall) { if (mcpCall == null || mcpCall.getToolName() == null) { return null; } String toolName = mcpCall.getToolName().toLowerCase(Locale.ROOT); return toolName.contains("revert_last_auto_tune_job") || toolName.contains("rollback") ? "rollback" : null; } private Map toChangeSummary(AiAutoTuneChange change, String ownerTriggerType) { LinkedHashMap item = new LinkedHashMap<>(); item.put("jobId", change.getJobId()); item.put("targetType", change.getTargetType()); item.put("targetId", change.getTargetId()); item.put("targetKey", change.getTargetKey()); item.put("oldValue", change.getOldValue()); item.put("requestedValue", change.getRequestedValue()); item.put("appliedValue", change.getAppliedValue()); item.put("resultStatus", change.getResultStatus()); item.put("rejectReason", change.getRejectReason()); item.put("cooldownExpireTime", change.getCooldownExpireTime()); item.put("createTime", change.getCreateTime()); AutoTuneWriteBehaviorUtils.addWriteBehavior(item, AutoTuneWriteBehaviorUtils.resolveChangeWriteBehavior(change, ownerTriggerType)); return item; } private int boundLimit(Integer limit) { if (limit == null || limit <= 0) { return DEFAULT_RECENT_JOB_LIMIT; } return Math.min(limit, MAX_RECENT_JOB_LIMIT); } private void requireMatchingDryRunToken(String dryRunToken, String fingerprint) { cleanExpiredDryRunPreviews(); if (isBlank(dryRunToken)) { throw new IllegalArgumentException("dryRunToken is required when dryRun=false. Run dryRun=true first."); } DryRunPreview preview = dryRunPreviews.remove(dryRunToken.trim()); if (preview == null) { throw new IllegalArgumentException("dryRunToken is missing, expired, or already used."); } if (preview.isExpired(currentTimeMillis())) { throw new IllegalArgumentException("dryRunToken is expired. Run dryRun=true again."); } if (!preview.getFingerprint().equals(fingerprint)) { throw new IllegalArgumentException("dryRunToken does not match the requested change set."); } } private String createDryRunToken(String fingerprint) { cleanExpiredDryRunPreviews(); String token = UUID.randomUUID().toString(); dryRunPreviews.put(token, new DryRunPreview(fingerprint, currentTimeMillis() + DRY_RUN_TOKEN_TTL_MILLIS)); return token; } private void cleanExpiredDryRunPreviews() { long currentTimeMillis = currentTimeMillis(); for (Map.Entry entry : dryRunPreviews.entrySet()) { if (entry.getValue() == null || entry.getValue().isExpired(currentTimeMillis)) { dryRunPreviews.remove(entry.getKey()); } } } private boolean isSuccessful(AutoTuneApplyResult result) { return result != null && Boolean.TRUE.equals(result.getSuccess()); } private boolean hasApplicableDryRunChanges(AutoTuneApplyResult result) { if (!isSuccessful(result) || result.getChanges() == null || result.getChanges().isEmpty()) { return false; } for (AiAutoTuneChange change : result.getChanges()) { if (change != null && "dry_run".equals(normalizeLower(change.getResultStatus()))) { return true; } } return false; } private String buildChangeFingerprint(List changes) { List> normalizedChanges = new ArrayList<>(); if (changes != null) { for (AutoTuneChangeCommand change : changes) { normalizedChanges.add(toNormalizedChange(change)); } } validateUniqueChangeTargets(normalizedChanges); normalizedChanges.sort(Comparator .comparing((Map item) -> item.get("targetType")) .thenComparing(item -> item.get("targetId")) .thenComparing(item -> item.get("targetKey")) .thenComparing(item -> item.get("newValue"))); return JSON.toJSONString(normalizedChanges); } private void validateUniqueChangeTargets(List> normalizedChanges) { Map> uniqueTargets = new LinkedHashMap<>(); for (Map change : normalizedChanges) { String targetSignature = buildTargetSignature(change); if (uniqueTargets.containsKey(targetSignature)) { throw new IllegalArgumentException("Duplicate auto-tune change target in same request: " + "targetType=" + change.get("targetType") + ", targetId=" + change.get("targetId") + ", targetKey=" + change.get("targetKey")); } uniqueTargets.put(targetSignature, change); } } private String buildTargetSignature(Map change) { return change.get("targetType") + "\n" + change.get("targetId") + "\n" + change.get("targetKey"); } private Map toNormalizedChange(AutoTuneChangeCommand change) { LinkedHashMap item = new LinkedHashMap<>(); String targetType = normalizeLower(change == null ? null : change.getTargetType()); item.put("targetType", targetType); item.put("targetId", "sys_config".equals(targetType) ? "" : normalizeText(change == null ? null : change.getTargetId())); item.put("targetKey", normalizeText(change == null ? null : change.getTargetKey())); item.put("newValue", normalizeText(change == null ? null : change.getNewValue())); return item; } private String normalizeLower(String value) { return normalizeText(value).toLowerCase(Locale.ROOT); } private String normalizeText(String value) { return value == null ? "" : value.trim(); } private boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } private long currentTimeMillis() { return currentTimeMillisSupplier.getAsLong(); } void setCurrentTimeMillisSupplier(LongSupplier currentTimeMillisSupplier) { this.currentTimeMillisSupplier = currentTimeMillisSupplier == null ? System::currentTimeMillis : currentTimeMillisSupplier; } private static class DryRunPreview { private final String fingerprint; private final long expireAtMillis; DryRunPreview(String fingerprint, long expireAtMillis) { this.fingerprint = fingerprint; this.expireAtMillis = expireAtMillis; } String getFingerprint() { return fingerprint; } boolean isExpired(long currentTimeMillis) { return currentTimeMillis > expireAtMillis; } } }