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.service.AiAutoTuneChangeService; import com.zy.ai.service.AiAutoTuneJobService; import com.zy.ai.service.AutoTuneApplyService; import com.zy.ai.service.AutoTuneSnapshotService; 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 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() .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."); } String fingerprint = buildChangeFingerprint(changes); if (Boolean.FALSE.equals(dryRun)) { requireMatchingDryRunToken(dryRunToken, fingerprint); } AutoTuneApplyRequest request = new AutoTuneApplyRequest(); request.setReason(reason); request.setAnalysisIntervalMinutes(analysisIntervalMinutes); request.setTriggerType(triggerType); request.setDryRun(dryRun); request.setChanges(changes); AutoTuneApplyResult result = autoTuneApplyService.apply(request); if (Boolean.TRUE.equals(dryRun) && isSuccessful(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()); item.put("changes", listChangeSummaries(job.getId())); return item; } private List> listChangeSummaries(Long jobId) { if (jobId == null) { return new ArrayList<>(); } List changes = aiAutoTuneChangeService.list(new QueryWrapper() .eq("job_id", jobId) .orderByAsc("id")); if (changes == null || changes.isEmpty()) { return new ArrayList<>(); } List> result = new ArrayList<>(); for (AiAutoTuneChange change : changes) { result.add(toChangeSummary(change)); } return result; } private Map toChangeSummary(AiAutoTuneChange change) { LinkedHashMap item = new LinkedHashMap<>(); 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()); 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 String buildChangeFingerprint(List changes) { List> normalizedChanges = new ArrayList<>(); if (changes != null) { for (AutoTuneChangeCommand change : changes) { normalizedChanges.add(toNormalizedChange(change)); } } 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 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; } } }