Junjie
2026-04-27 10bdc4b6e9701befd1a83bccd2998dcc96cb2c43
src/main/java/com/zy/ai/mcp/tool/AutoTuneMcpTools.java
@@ -1,5 +1,6 @@
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;
@@ -17,9 +18,14 @@
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;
@Component
@RequiredArgsConstructor
@@ -27,11 +33,13 @@
    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<String, DryRunPreview> dryRunPreviews = new ConcurrentHashMap<>();
    @Tool(name = "dispatch_get_auto_tune_snapshot", description = "获取WCS自动调参所需的调度快照、站点运行态、拓扑容量和当前可写参数")
    public AutoTuneSnapshot getAutoTuneSnapshot() {
@@ -63,14 +71,27 @@
            @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<AutoTuneChangeCommand> 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);
        return autoTuneApplyService.apply(request);
        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 = "回滚最近一次成功的自动调参任务")
@@ -133,4 +154,95 @@
        }
        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()) {
            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, System.currentTimeMillis() + DRY_RUN_TOKEN_TTL_MILLIS));
        return token;
    }
    private void cleanExpiredDryRunPreviews() {
        for (Map.Entry<String, DryRunPreview> entry : dryRunPreviews.entrySet()) {
            if (entry.getValue() == null || entry.getValue().isExpired()) {
                dryRunPreviews.remove(entry.getKey());
            }
        }
    }
    private boolean isSuccessful(AutoTuneApplyResult result) {
        return result != null && Boolean.TRUE.equals(result.getSuccess());
    }
    private String buildChangeFingerprint(List<AutoTuneChangeCommand> changes) {
        List<Map<String, String>> normalizedChanges = new ArrayList<>();
        if (changes != null) {
            for (AutoTuneChangeCommand change : changes) {
                normalizedChanges.add(toNormalizedChange(change));
            }
        }
        normalizedChanges.sort(Comparator
                .comparing((Map<String, String> 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<String, String> toNormalizedChange(AutoTuneChangeCommand change) {
        LinkedHashMap<String, String> 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 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() {
            return System.currentTimeMillis() > expireAtMillis;
        }
    }
}