13个文件已添加
16个文件已修改
1个文件已删除
| | |
| | | import com.zy.ai.service.AutoTuneApplyService; |
| | | import com.zy.ai.service.AutoTuneCoordinatorService; |
| | | import com.zy.ai.service.AutoTuneSnapshotService; |
| | | import com.zy.ai.utils.AutoTuneWriteBehaviorUtils; |
| | | import com.zy.common.web.BaseController; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | |
| | | item.put("completionTokens", job.getCompletionTokens()); |
| | | item.put("totalTokens", job.getTotalTokens()); |
| | | List<AiAutoTuneMcpCall> mcpCalls = listMcpCalls(job.getId()); |
| | | List<Map<String, Object>> mcpCallSummaries = toMcpCallSummaries(mcpCalls); |
| | | List<Map<String, Object>> changeSummaries = listChangeSummaries(job, mcpCalls); |
| | | AutoTuneWriteBehaviorUtils.addWriteBehavior(item, |
| | | AutoTuneWriteBehaviorUtils.resolveJobWriteBehavior(job, mcpCallSummaries, changeSummaries)); |
| | | item.put("mcpCallCount", mcpCalls.size()); |
| | | item.put("mcpCalls", toMcpCallSummaries(mcpCalls)); |
| | | item.put("changes", listChangeSummaries(mcpCalls)); |
| | | item.put("mcpCalls", mcpCallSummaries); |
| | | item.put("changes", changeSummaries); |
| | | return item; |
| | | } |
| | | |
| | |
| | | item.put("responseJson", mcpCall.getResponseJson()); |
| | | item.put("errorMessage", mcpCall.getErrorMessage()); |
| | | item.put("createTime", mcpCall.getCreateTime()); |
| | | AutoTuneWriteBehaviorUtils.addWriteBehavior(item, |
| | | AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(mcpCall)); |
| | | return item; |
| | | } |
| | | |
| | |
| | | return value == 1; |
| | | } |
| | | |
| | | private List<Map<String, Object>> listChangeSummaries(List<AiAutoTuneMcpCall> mcpCalls) { |
| | | private List<Map<String, Object>> listChangeSummaries(AiAutoTuneJob job, List<AiAutoTuneMcpCall> mcpCalls) { |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | List<Long> applyJobIds = collectApplyJobIds(mcpCalls); |
| | | Map<Long, String> ownerTriggerTypes = collectChangeOwnerTriggerTypes(job, mcpCalls); |
| | | List<Long> applyJobIds = new ArrayList<>(ownerTriggerTypes.keySet()); |
| | | if (applyJobIds.isEmpty()) { |
| | | return result; |
| | | } |
| | |
| | | return result; |
| | | } |
| | | for (AiAutoTuneChange change : changes) { |
| | | result.add(toChangeSummary(change)); |
| | | result.add(toChangeSummary(change, ownerTriggerTypes.get(change.getJobId()))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<Long> collectApplyJobIds(List<AiAutoTuneMcpCall> mcpCalls) { |
| | | List<Long> result = new ArrayList<>(); |
| | | private Map<Long, String> collectChangeOwnerTriggerTypes(AiAutoTuneJob job, List<AiAutoTuneMcpCall> mcpCalls) { |
| | | LinkedHashMap<Long, String> 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.contains(applyJobId)) { |
| | | result.add(applyJobId); |
| | | if (applyJobId == null || result.containsKey(applyJobId)) { |
| | | continue; |
| | | } |
| | | result.put(applyJobId, resolveMcpApplyJobTriggerType(mcpCall)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> toChangeSummary(AiAutoTuneChange change) { |
| | | private String resolveMcpApplyJobTriggerType(AiAutoTuneMcpCall mcpCall) { |
| | | if (mcpCall == null || mcpCall.getToolName() == null) { |
| | | return null; |
| | | } |
| | | String toolName = mcpCall.getToolName().toLowerCase(); |
| | | return toolName.contains("revert_last_auto_tune_job") || toolName.contains("rollback") ? "rollback" : null; |
| | | } |
| | | |
| | | private Map<String, Object> toChangeSummary(AiAutoTuneChange change, String ownerTriggerType) { |
| | | LinkedHashMap<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("id", change.getId()); |
| | | item.put("jobId", change.getJobId()); |
| | | item.put("targetType", change.getTargetType()); |
| | | item.put("targetId", change.getTargetId()); |
| | | item.put("targetKey", change.getTargetKey()); |
| | |
| | | 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 Boolean success; |
| | | |
| | | private Boolean analysisOnly; |
| | | |
| | | private Boolean noApply; |
| | | |
| | | private Long jobId; |
| | | |
| | | private String summary; |
| New file |
| | |
| | | package com.zy.ai.domain.autotune; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | public class AutoTuneControlModeSnapshot implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private Boolean enabled; |
| | | |
| | | private Boolean analysisOnly; |
| | | |
| | | private Boolean allowApply; |
| | | |
| | | private String modeCode; |
| | | |
| | | private String modeLabel; |
| | | } |
| New file |
| | |
| | | package com.zy.ai.domain.autotune; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Data |
| | | public class AutoTuneHotPathSegmentItem implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private String segmentKey; |
| | | private List<Integer> stationIds; |
| | | private Integer passTaskCount; |
| | | private Integer loadingCount; |
| | | private Integer taskHoldingCount; |
| | | private Integer runBlockCount; |
| | | private Integer nonAutoingCount; |
| | | private String pressureLevel; |
| | | private Integer pressureScore; |
| | | private Map<String, Object> pressureFactors; |
| | | private List<Integer> relatedTargetStations; |
| | | private List<Integer> sampleWrkNos; |
| | | } |
| New file |
| | |
| | | package com.zy.ai.domain.autotune; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | public class AutoTuneRoutePressureRuleSnapshot implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private Integer segmentWindowSize; |
| | | private Integer mediumPercent; |
| | | private Integer highPercent; |
| | | private Integer passWeightPercent; |
| | | private Integer occupiedWeightPercent; |
| | | private Integer blockedWeightPercent; |
| | | private Integer nonAutoingWeightPercent; |
| | | private Integer runBlockWeightPercent; |
| | | } |
| New file |
| | |
| | | package com.zy.ai.domain.autotune; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | public class AutoTuneRoutePressureSnapshot implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private Integer analyzedTaskCount = 0; |
| | | private Integer tracePathCount = 0; |
| | | private Integer estimatedPathCount = 0; |
| | | private Integer pathErrorCount = 0; |
| | | private String confidence; |
| | | private AutoTuneRoutePressureRuleSnapshot routePressureRuleSnapshot; |
| | | private List<AutoTuneTaskRouteSampleItem> taskRouteSamples = new ArrayList<>(); |
| | | private List<AutoTuneHotPathSegmentItem> hotPathSegments = new ArrayList<>(); |
| | | private List<AutoTuneTargetStationRoutePressureItem> targetStationRoutePressure = new ArrayList<>(); |
| | | } |
| | |
| | | |
| | | private List<AutoTuneRuleSnapshotItem> ruleSnapshot; |
| | | |
| | | private AutoTuneRoutePressureSnapshot routePressureSnapshot; |
| | | |
| | | private AutoTuneControlModeSnapshot controlModeSnapshot; |
| | | |
| | | private Date snapshotTime; |
| | | } |
| | |
| | | |
| | | private Integer taskNo; |
| | | |
| | | private Integer runBlock; |
| | | |
| | | private String ioMode; |
| | | } |
| New file |
| | |
| | | package com.zy.ai.domain.autotune; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Data |
| | | public class AutoTuneTargetStationRoutePressureItem implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private Integer targetStationId; |
| | | private Integer blockedTaskCount; |
| | | private Integer routeTaskCount; |
| | | private List<String> mainHotSegments; |
| | | private String pressureLevel; |
| | | private String recommendedDirection; |
| | | private String heuristicDirection; |
| | | private Integer pressureScore; |
| | | private String confidence; |
| | | private Map<String, Object> pressureFactors; |
| | | private List<String> recommendedTargets; |
| | | private String reason; |
| | | private String evidenceText; |
| | | } |
| New file |
| | |
| | | package com.zy.ai.domain.autotune; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | public class AutoTuneTaskRouteSampleItem implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private Integer wrkNo; |
| | | private Long wrkSts; |
| | | private String batch; |
| | | private Integer batchSeq; |
| | | private Integer sourceStaNo; |
| | | private Integer targetStaNo; |
| | | private String pathSource; |
| | | private List<Integer> pathStationIds; |
| | | private Integer pathLength; |
| | | private String pathError; |
| | | } |
| | |
| | | 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; |
| | |
| | | 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.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) && isSuccessful(result)) { |
| | | if (Boolean.TRUE.equals(dryRun) && hasApplicableDryRunChanges(result)) { |
| | | result.setDryRunToken(createDryRunToken(fingerprint)); |
| | | } |
| | | return result; |
| | |
| | | item.put("rejectCount", job.getRejectCount()); |
| | | item.put("errorMessage", job.getErrorMessage()); |
| | | List<AiAutoTuneMcpCall> mcpCalls = listMcpCalls(job.getId()); |
| | | List<Map<String, Object>> mcpCallSummaries = toMcpCallSummaries(mcpCalls); |
| | | List<Map<String, Object>> changeSummaries = listChangeSummaries(job, mcpCalls); |
| | | AutoTuneWriteBehaviorUtils.addWriteBehavior(item, |
| | | AutoTuneWriteBehaviorUtils.resolveJobWriteBehavior(job, mcpCallSummaries, changeSummaries)); |
| | | item.put("mcpCallCount", mcpCalls.size()); |
| | | item.put("mcpCalls", toMcpCallSummaries(mcpCalls)); |
| | | item.put("changes", listChangeSummaries(mcpCalls)); |
| | | item.put("mcpCalls", mcpCallSummaries); |
| | | item.put("changes", changeSummaries); |
| | | return item; |
| | | } |
| | | |
| | |
| | | item.put("successCount", mcpCall.getSuccessCount()); |
| | | item.put("rejectCount", mcpCall.getRejectCount()); |
| | | item.put("errorMessage", mcpCall.getErrorMessage()); |
| | | AutoTuneWriteBehaviorUtils.addWriteBehavior(item, |
| | | AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(mcpCall)); |
| | | return item; |
| | | } |
| | | |
| | |
| | | return value == 1; |
| | | } |
| | | |
| | | private List<Map<String, Object>> listChangeSummaries(List<AiAutoTuneMcpCall> mcpCalls) { |
| | | List<Long> applyJobIds = collectApplyJobIds(mcpCalls); |
| | | private List<Map<String, Object>> listChangeSummaries(AiAutoTuneJob job, List<AiAutoTuneMcpCall> mcpCalls) { |
| | | Map<Long, String> ownerTriggerTypes = collectChangeOwnerTriggerTypes(job, mcpCalls); |
| | | List<Long> applyJobIds = new ArrayList<>(ownerTriggerTypes.keySet()); |
| | | if (applyJobIds.isEmpty()) { |
| | | return new ArrayList<>(); |
| | | } |
| | |
| | | |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | for (AiAutoTuneChange change : changes) { |
| | | result.add(toChangeSummary(change)); |
| | | result.add(toChangeSummary(change, ownerTriggerTypes.get(change.getJobId()))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<Long> collectApplyJobIds(List<AiAutoTuneMcpCall> mcpCalls) { |
| | | List<Long> result = new ArrayList<>(); |
| | | private Map<Long, String> collectChangeOwnerTriggerTypes(AiAutoTuneJob job, List<AiAutoTuneMcpCall> mcpCalls) { |
| | | LinkedHashMap<Long, String> 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.contains(applyJobId)) { |
| | | result.add(applyJobId); |
| | | if (applyJobId == null || result.containsKey(applyJobId)) { |
| | | continue; |
| | | } |
| | | result.put(applyJobId, resolveMcpApplyJobTriggerType(mcpCall)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> toChangeSummary(AiAutoTuneChange change) { |
| | | 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<String, Object> toChangeSummary(AiAutoTuneChange change, String ownerTriggerType) { |
| | | LinkedHashMap<String, Object> 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("rejectReason", change.getRejectReason()); |
| | | item.put("cooldownExpireTime", change.getCooldownExpireTime()); |
| | | item.put("createTime", change.getCreateTime()); |
| | | AutoTuneWriteBehaviorUtils.addWriteBehavior(item, |
| | | AutoTuneWriteBehaviorUtils.resolveChangeWriteBehavior(change, ownerTriggerType)); |
| | | return item; |
| | | } |
| | | |
| | |
| | | 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<AutoTuneChangeCommand> changes) { |
| | | List<Map<String, String>> normalizedChanges = new ArrayList<>(); |
| | | if (changes != null) { |
| | |
| | | normalizedChanges.add(toNormalizedChange(change)); |
| | | } |
| | | } |
| | | validateUniqueChangeTargets(normalizedChanges); |
| | | normalizedChanges.sort(Comparator |
| | | .comparing((Map<String, String> item) -> item.get("targetType")) |
| | | .thenComparing(item -> item.get("targetId")) |
| | |
| | | return JSON.toJSONString(normalizedChanges); |
| | | } |
| | | |
| | | private void validateUniqueChangeTargets(List<Map<String, String>> normalizedChanges) { |
| | | Map<String, Map<String, String>> uniqueTargets = new LinkedHashMap<>(); |
| | | for (Map<String, String> 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<String, String> change) { |
| | | return change.get("targetType") + "\n" |
| | | + change.get("targetId") + "\n" |
| | | + change.get("targetKey"); |
| | | } |
| | | |
| | | private Map<String, String> toNormalizedChange(AutoTuneChangeCommand change) { |
| | | LinkedHashMap<String, String> item = new LinkedHashMap<>(); |
| | | String targetType = normalizeLower(change == null ? null : change.getTargetType()); |
| | |
| | | |
| | | private Boolean maxRoundsReached; |
| | | |
| | | private Boolean analysisOnly; |
| | | |
| | | private Boolean allowApply; |
| | | |
| | | private String executionMode; |
| | | |
| | | private Boolean actualApplyCalled; |
| | | |
| | | private Boolean rollbackCalled; |
| New file |
| | |
| | | package com.zy.ai.service; |
| | | |
| | | import com.zy.ai.domain.autotune.AutoTuneControlModeSnapshot; |
| | | |
| | | public interface AutoTuneControlModeService { |
| | | |
| | | AutoTuneControlModeSnapshot currentMode(); |
| | | |
| | | default boolean isAnalysisOnly() { |
| | | AutoTuneControlModeSnapshot snapshot = currentMode(); |
| | | return snapshot == null || Boolean.TRUE.equals(snapshot.getAnalysisOnly()); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.ai.service; |
| | | |
| | | import com.zy.ai.domain.autotune.AutoTuneRoutePressureSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneStationRuntimeItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskSnapshot; |
| | | import com.zy.asrs.entity.WrkMast; |
| | | |
| | | import java.util.List; |
| | | |
| | | public interface RoutePressureSnapshotService { |
| | | |
| | | AutoTuneRoutePressureSnapshot buildSnapshot(List<WrkMast> activeTasks, |
| | | AutoTuneTaskSnapshot taskSnapshot, |
| | | List<AutoTuneStationRuntimeItem> stationRuntimeSnapshot); |
| | | } |
| | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.zy.ai.domain.autotune.AutoTuneApplyResult; |
| | | import com.zy.ai.domain.autotune.AutoTuneControlModeSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneTriggerType; |
| | | import com.zy.ai.entity.AiPromptTemplate; |
| | | import com.zy.ai.entity.ChatCompletionRequest; |
| | |
| | | import com.zy.ai.mcp.service.SpringAiMcpToolManager; |
| | | import com.zy.ai.service.AiPromptTemplateService; |
| | | import com.zy.ai.service.AutoTuneAgentService; |
| | | import com.zy.ai.service.AutoTuneControlModeService; |
| | | import com.zy.ai.service.LlmChatService; |
| | | import com.zy.ai.utils.AiPromptUtils; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | |
| | | private final LlmChatService llmChatService; |
| | | private final SpringAiMcpToolManager mcpToolManager; |
| | | private final AiPromptTemplateService aiPromptTemplateService; |
| | | private final AutoTuneControlModeService autoTuneControlModeService; |
| | | |
| | | @Override |
| | | public AutoTuneAgentResult runAutoTune(String triggerType) { |
| | | String normalizedTriggerType = normalizeTriggerType(triggerType); |
| | | AutoTuneControlModeSnapshot controlMode = buildControlModeSnapshot(); |
| | | UsageCounter usageCounter = new UsageCounter(); |
| | | RunState runState = new RunState(); |
| | | boolean maxRoundsReached = false; |
| | |
| | | } |
| | | |
| | | AiPromptTemplate promptTemplate = aiPromptTemplateService.resolvePublished(AiPromptScene.AUTO_TUNE_DISPATCH.getCode()); |
| | | List<ChatCompletionRequest.Message> messages = buildMessages(promptTemplate, normalizedTriggerType); |
| | | List<ChatCompletionRequest.Message> messages = buildMessages(promptTemplate, normalizedTriggerType, controlMode); |
| | | |
| | | for (int round = 0; round < MAX_TOOL_ROUNDS; round++) { |
| | | ChatCompletionResponse response = llmChatService.chatCompletionOrThrow(messages, TEMPERATURE, MAX_TOKENS, tools); |
| | |
| | | List<ChatCompletionRequest.ToolCall> toolCalls = assistantMessage.getTool_calls(); |
| | | if (toolCalls == null || toolCalls.isEmpty()) { |
| | | return buildResult(runState.isSuccessful(), normalizedTriggerType, summaryBuffer, runState, |
| | | usageCounter, false); |
| | | usageCounter, false, controlMode); |
| | | } |
| | | |
| | | for (ChatCompletionRequest.ToolCall toolCall : toolCalls) { |
| | |
| | | } |
| | | } |
| | | maxRoundsReached = true; |
| | | return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached); |
| | | return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached, |
| | | controlMode); |
| | | } catch (Exception exception) { |
| | | log.error("Auto tune agent stopped with error", exception); |
| | | appendSummary(summaryBuffer, "自动调参 Agent 执行异常: " + exception.getMessage()); |
| | | runState.markToolError(); |
| | | return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached); |
| | | return buildResult(false, normalizedTriggerType, summaryBuffer, runState, usageCounter, maxRoundsReached, |
| | | controlMode); |
| | | } |
| | | } |
| | | |
| | | private List<ChatCompletionRequest.Message> buildMessages(AiPromptTemplate promptTemplate, String triggerType) { |
| | | private List<ChatCompletionRequest.Message> buildMessages(AiPromptTemplate promptTemplate, |
| | | String triggerType, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | List<ChatCompletionRequest.Message> messages = new ArrayList<>(); |
| | | |
| | | ChatCompletionRequest.Message systemMessage = new ChatCompletionRequest.Message(); |
| | |
| | | |
| | | ChatCompletionRequest.Message userMessage = new ChatCompletionRequest.Message(); |
| | | userMessage.setRole("user"); |
| | | userMessage.setContent("请执行一次后台 WCS 自动调参。triggerType=" + triggerType |
| | | + "。必须先调用 wcs_local_dispatch_get_auto_tune_snapshot 获取事实;如需提交变更," |
| | | + "必须先 dry-run,再根据 dry-run 结果决定是否实际应用;实际应用时必须带上 dry-run 返回的 dryRunToken。" |
| | | + "禁止在没有收到 wcs_local_dispatch_apply_auto_tune_changes 或 wcs_local_dispatch_revert_last_auto_tune_job 工具返回结果时声称已试算、已应用或已回滚。" |
| | | + "必须检查 taskSnapshot.stationLimitBlockedTasks 和 taskSnapshot.outboundTaskSamples 中的 systemMsg、wrkSts、batchSeq,判断是否存在被上限挡住的早序出库任务。" |
| | | + "所有提交给 wcs_local_dispatch_apply_auto_tune_changes 的 changes 都必须先匹配 snapshot.ruleSnapshot 中对应 targetType/targetKey 的规则。" |
| | | + "每个参数都必须满足 minValue、maxValue 或 dynamicMaxValue、maxStep、cooldownMinutes 和规则 note;找不到规则或无法证明动态上限时禁止提交。" |
| | | + "dry-run 返回 success=false 或 rejectCount>0 时,必须停止实际应用并说明拒绝原因,或重新提交完全合法的 dry-run。" |
| | | + "不要输出自由格式 JSON 供外层解析。"); |
| | | userMessage.setContent(AiPromptUtils.buildAutoTuneRuntimeGuard(triggerType, controlMode)); |
| | | messages.add(userMessage); |
| | | return messages; |
| | | } |
| | |
| | | return message; |
| | | } |
| | | |
| | | private Object callMountedTool(ChatCompletionRequest.ToolCall toolCall, RunState runState, String triggerType) { |
| | | private Object callMountedTool(ChatCompletionRequest.ToolCall toolCall, |
| | | RunState runState, |
| | | String triggerType) { |
| | | String toolName = resolveToolName(toolCall); |
| | | if (!ALLOWED_TOOL_NAMES.contains(toolName)) { |
| | | throw new IllegalArgumentException("Disallowed auto-tune MCP tool: " + toolName); |
| | |
| | | Object output = mcpToolManager.callTool(toolName, arguments); |
| | | runState.markToolSuccess(toolName); |
| | | recordMutationResult(toolName, arguments, output, runState); |
| | | if (TOOL_APPLY_CHANGES.equals(toolName) && isRejectedApplyResult(output)) { |
| | | runState.markApplyRejected(); |
| | | Object wrappedOutput = withRejectedApplyInstruction(output); |
| | | runState.addMcpCall(buildMcpCall(toolName, arguments, wrappedOutput, startTimeMillis, null)); |
| | | return wrappedOutput; |
| | | if (isRejectedApplyResult(output)) { |
| | | runState.markApplyRejected(resolveApplyError(output)); |
| | | if (TOOL_APPLY_CHANGES.equals(toolName)) { |
| | | Object wrappedOutput = withRejectedApplyInstruction(output); |
| | | runState.addMcpCall(buildMcpCall(toolName, arguments, wrappedOutput, startTimeMillis, null)); |
| | | return wrappedOutput; |
| | | } |
| | | } |
| | | runState.addMcpCall(buildMcpCall(toolName, arguments, output, startTimeMillis, null)); |
| | | return output; |
| | |
| | | if (TOOL_APPLY_CHANGES.equals(toolName)) { |
| | | boolean dryRun = Boolean.TRUE.equals(arguments.getBoolean("dryRun")); |
| | | if (!dryRun) { |
| | | runState.markActualApply(); |
| | | runState.addCounts(output); |
| | | if (outputHasSuccessfulChange(output)) { |
| | | runState.markActualApply(); |
| | | } |
| | | } else if (isRejectedApplyResult(output)) { |
| | | runState.addCounts(output); |
| | | } |
| | |
| | | runState.markRollback(); |
| | | runState.addCounts(output); |
| | | } |
| | | } |
| | | |
| | | private boolean outputHasSuccessfulChange(Object output) { |
| | | if (output instanceof AutoTuneApplyResult) { |
| | | AutoTuneApplyResult result = (AutoTuneApplyResult) output; |
| | | if (result.getChanges() != null) { |
| | | return hasSuccessfulChange(result.getChanges()); |
| | | } |
| | | return safeCount(result.getSuccessCount()) > 0; |
| | | } |
| | | if (output instanceof Map<?, ?>) { |
| | | Map<?, ?> result = (Map<?, ?>) output; |
| | | if (!isApplyResultShape(result)) { |
| | | return false; |
| | | } |
| | | if (result.containsKey("changes")) { |
| | | return hasSuccessfulChange(result.get("changes")); |
| | | } |
| | | return safeCount(result.get("successCount")) > 0; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private boolean hasSuccessfulChange(Object changes) { |
| | | if (!(changes instanceof List<?>)) { |
| | | return false; |
| | | } |
| | | for (Object change : (List<?>) changes) { |
| | | String resultStatus = null; |
| | | if (change instanceof com.zy.ai.entity.AiAutoTuneChange) { |
| | | resultStatus = ((com.zy.ai.entity.AiAutoTuneChange) change).getResultStatus(); |
| | | } else if (change instanceof Map<?, ?>) { |
| | | Object status = ((Map<?, ?>) change).get("resultStatus"); |
| | | resultStatus = status == null ? null : String.valueOf(status); |
| | | } |
| | | if (resultStatus != null && "success".equalsIgnoreCase(resultStatus.trim())) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private boolean isRejectedApplyResult(Object output) { |
| | |
| | | StringBuilder summaryBuffer, |
| | | RunState runState, |
| | | UsageCounter usageCounter, |
| | | boolean maxRoundsReached) { |
| | | boolean maxRoundsReached, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | AutoTuneAgentResult result = new AutoTuneAgentResult(); |
| | | result.setSuccess(success); |
| | | result.setTriggerType(triggerType); |
| | | result.setAnalysisOnly(controlMode.getAnalysisOnly()); |
| | | result.setAllowApply(controlMode.getAllowApply()); |
| | | result.setExecutionMode(controlMode.getModeCode()); |
| | | result.setToolCallCount(runState.getToolCallCount()); |
| | | result.setLlmCallCount(usageCounter.getLlmCallCount()); |
| | | result.setPromptTokens(usageCounter.getPromptTokens()); |
| | |
| | | } |
| | | if (runState.hasApplyRejected()) { |
| | | summary = summary + "\n自动调参 Agent 存在被拒绝的 dry-run/apply 结果,未视为成功调参。"; |
| | | if (!isBlank(runState.getFirstRejectReason())) { |
| | | summary = summary + "拒绝原因: " + runState.getFirstRejectReason(); |
| | | } |
| | | } |
| | | if (success && !runState.hasActualMutation()) { |
| | | summary = "自动调参 Agent 未调用实际应用或回滚工具,未修改运行参数。" |
| | |
| | | if (maxRoundsReached) { |
| | | summary = summary + "\n自动调参 Agent 达到最大工具调用轮次,已停止。"; |
| | | } |
| | | summary = buildModeSummary(controlMode) + (summary.isEmpty() ? "" : "\n" + summary); |
| | | result.setSummary(summary); |
| | | return result; |
| | | } |
| | | |
| | | private String buildModeSummary(AutoTuneControlModeSnapshot controlMode) { |
| | | return "执行模式: " + controlMode.getModeCode() |
| | | + ",analysisOnly=" + controlMode.getAnalysisOnly() |
| | | + ",allowApply=" + controlMode.getAllowApply() |
| | | + ",modeLabel=" + controlMode.getModeLabel(); |
| | | } |
| | | |
| | | private AutoTuneControlModeSnapshot buildControlModeSnapshot() { |
| | | return autoTuneControlModeService.currentMode(); |
| | | } |
| | | |
| | | private List<Object> filterAllowedTools(List<Object> tools) { |
| | |
| | | return isBlank(triggerType) ? "agent" : triggerType.trim(); |
| | | } |
| | | |
| | | private boolean isBlank(String value) { |
| | | private static boolean isBlank(String value) { |
| | | return value == null || value.trim().isEmpty(); |
| | | } |
| | | |
| | |
| | | private boolean snapshotCalled; |
| | | private boolean toolError; |
| | | private boolean applyRejected; |
| | | private String firstRejectReason; |
| | | private boolean actualApplyCalled; |
| | | private boolean rollbackCalled; |
| | | private int successCount; |
| | |
| | | toolError = true; |
| | | } |
| | | |
| | | void markApplyRejected() { |
| | | void markApplyRejected(String rejectReason) { |
| | | applyRejected = true; |
| | | if (isBlank(firstRejectReason) && !isBlank(rejectReason)) { |
| | | firstRejectReason = rejectReason; |
| | | } |
| | | } |
| | | |
| | | void markActualApply() { |
| | |
| | | return applyRejected; |
| | | } |
| | | |
| | | String getFirstRejectReason() { |
| | | return firstRejectReason; |
| | | } |
| | | |
| | | boolean isActualApplyCalled() { |
| | | return actualApplyCalled; |
| | | } |
| | |
| | | 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.AutoTuneControlModeSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneJobStatus; |
| | | import com.zy.ai.domain.autotune.AutoTuneRuleDefinition; |
| | | import com.zy.ai.domain.autotune.AutoTuneTargetType; |
| | |
| | | import com.zy.ai.service.AiAutoTuneChangeService; |
| | | import com.zy.ai.service.AiAutoTuneJobService; |
| | | import com.zy.ai.service.AutoTuneApplyService; |
| | | import com.zy.ai.service.AutoTuneControlModeService; |
| | | import com.zy.asrs.entity.BasCrnp; |
| | | import com.zy.asrs.entity.BasDualCrnp; |
| | | import com.zy.asrs.entity.BasStation; |
| | |
| | | private static final String PROMPT_SCENE_CODE = "auto_tune_apply"; |
| | | private static final long APPLY_LOCK_SECONDS = 120L; |
| | | private static final String APPLY_LOCK_BUSY_REASON = "申请调参锁失败,锁不可用,可能已有任务或 Redis 异常"; |
| | | private static final String ANALYSIS_ONLY_REJECT_REASON = "仅分析模式禁止实际应用/回滚,未修改运行参数"; |
| | | private static final List<Long> FINAL_WRK_STS_LIST = Arrays.asList( |
| | | WrkStsType.COMPLETE_INBOUND.sts, |
| | | WrkStsType.SETTLE_INBOUND.sts, |
| | |
| | | @Autowired |
| | | private ConfigService configService; |
| | | @Autowired |
| | | private AutoTuneControlModeService autoTuneControlModeService; |
| | | @Autowired |
| | | private BasStationService basStationService; |
| | | @Autowired |
| | | private BasCrnpService basCrnpService; |
| | |
| | | @Override |
| | | public AutoTuneApplyResult apply(AutoTuneApplyRequest request) { |
| | | AutoTuneApplyRequest safeRequest = request == null ? new AutoTuneApplyRequest() : request; |
| | | AutoTuneControlModeSnapshot controlMode = currentControlModeSnapshot(); |
| | | boolean dryRun = Boolean.TRUE.equals(safeRequest.getDryRun()); |
| | | Date now = new Date(); |
| | | AiAutoTuneJob job = createJob(safeRequest, dryRun, now); |
| | | |
| | | if (dryRun) { |
| | | return applyDryRun(safeRequest, job, now); |
| | | return applyDryRun(safeRequest, job, now, controlMode); |
| | | } |
| | | return applyRealWithLock(safeRequest, job, now); |
| | | if (isAnalysisOnly(controlMode)) { |
| | | return rejectRealApplyForAnalysisOnly(safeRequest, job, now, controlMode); |
| | | } |
| | | return applyRealWithLock(safeRequest, job, now, controlMode); |
| | | } |
| | | |
| | | private AutoTuneApplyResult applyDryRun(AutoTuneApplyRequest request, AiAutoTuneJob job, Date now) { |
| | | private AutoTuneApplyResult applyDryRun(AutoTuneApplyRequest request, |
| | | AiAutoTuneJob job, |
| | | Date now, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | List<ValidatedChange> validatedChanges = validateChanges(request, true, now); |
| | | ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction( |
| | | job, |
| | |
| | | now, |
| | | false |
| | | ); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), true); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), true, controlMode); |
| | | } |
| | | |
| | | private AutoTuneApplyResult applyRealWithLock(AutoTuneApplyRequest request, AiAutoTuneJob job, Date now) { |
| | | private AutoTuneApplyResult applyRealWithLock(AutoTuneApplyRequest request, |
| | | AiAutoTuneJob job, |
| | | Date now, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | if (request.getChanges() == null || request.getChanges().isEmpty()) { |
| | | ApplyPersistenceResult persistenceResult = persistApplyResultInTransaction( |
| | | job, |
| | |
| | | now, |
| | | false |
| | | ); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), false); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), false, controlMode); |
| | | } |
| | | |
| | | String lockKey = RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key; |
| | | String lockToken = UUID.randomUUID().toString(); |
| | | if (!redisUtil.trySetStringIfAbsent(lockKey, lockToken, APPLY_LOCK_SECONDS)) { |
| | | return rejectRealApplyForUnavailableLock(request, job, now, lockKey); |
| | | return rejectRealApplyForUnavailableLock(request, job, now, lockKey, controlMode); |
| | | } |
| | | |
| | | try { |
| | |
| | | now, |
| | | false |
| | | ); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), false); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), false, controlMode); |
| | | } |
| | | |
| | | try { |
| | |
| | | true |
| | | ); |
| | | refreshSystemConfigCacheSafely(persistenceResult); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), false); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), false, controlMode); |
| | | } catch (RuntimeException exception) { |
| | | markWriteFailure(validatedChanges, exception); |
| | | Date failureNow = new Date(); |
| | |
| | | failureNow, |
| | | false |
| | | ); |
| | | return buildResult(failureJob, persistenceResult.getAuditChanges(), false); |
| | | return buildResult(failureJob, persistenceResult.getAuditChanges(), false, controlMode); |
| | | } |
| | | } finally { |
| | | redisUtil.compareAndDelete(lockKey, lockToken); |
| | |
| | | private AutoTuneApplyResult rejectRealApplyForUnavailableLock(AutoTuneApplyRequest request, |
| | | AiAutoTuneJob job, |
| | | Date now, |
| | | String lockKey) { |
| | | String lockKey, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | boolean lockKeyExists = redisUtil.hasKey(lockKey); |
| | | LOGGER.warn("申请AI自动调参 apply 锁失败,lockKey={}, lockKeyExists={}", lockKey, lockKeyExists); |
| | | List<ValidatedChange> validatedChanges = buildLockBusyChanges(request); |
| | |
| | | now, |
| | | false |
| | | ); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), false); |
| | | return buildResult(job, persistenceResult.getAuditChanges(), false, controlMode); |
| | | } |
| | | |
| | | private AutoTuneApplyResult rejectRealApplyForAnalysisOnly(AutoTuneApplyRequest request, |
| | | AiAutoTuneJob job, |
| | | Date now, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | ApplyPersistenceResult persistenceResult = persistAnalysisOnlyApplyRejectionInTransaction(job, request, now); |
| | | AutoTuneApplyResult result = buildResult(job, persistenceResult.getAuditChanges(), false, controlMode); |
| | | result.setAnalysisOnly(true); |
| | | result.setNoApply(true); |
| | | return result; |
| | | } |
| | | |
| | | private List<ValidatedChange> buildLockBusyChanges(AutoTuneApplyRequest request) { |
| | |
| | | |
| | | @Override |
| | | public AutoTuneApplyResult rollbackLastSuccessfulJob(String reason) { |
| | | AutoTuneControlModeSnapshot controlMode = currentControlModeSnapshot(); |
| | | Date now = new Date(); |
| | | AiAutoTuneJob rollbackJob = createRollbackJob(reason, now); |
| | | if (isAnalysisOnly(controlMode)) { |
| | | return rejectRollbackForAnalysisOnly(rollbackJob, now, controlMode); |
| | | } |
| | | |
| | | String lockKey = RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key; |
| | | String lockToken = UUID.randomUUID().toString(); |
| | | if (!redisUtil.trySetStringIfAbsent(lockKey, lockToken, APPLY_LOCK_SECONDS)) { |
| | | return rejectRollbackForUnavailableLock(reason, now, lockKey); |
| | | return rejectRollbackForUnavailableLock(reason, now, lockKey, controlMode); |
| | | } |
| | | |
| | | try { |
| | | List<AiAutoTuneChange> sourceChanges = findLatestSuccessfulChanges(); |
| | | if (sourceChanges.isEmpty()) { |
| | | persistNoRollbackSourceJobInTransaction(rollbackJob, now); |
| | | return buildResult(rollbackJob, new ArrayList<>(), false); |
| | | return buildResult(rollbackJob, new ArrayList<>(), false, controlMode); |
| | | } |
| | | try { |
| | | RollbackPersistenceResult persistenceResult = persistRollbackResultInTransaction( |
| | |
| | | now |
| | | ); |
| | | refreshRollbackConfigCacheSafely(persistenceResult); |
| | | return buildResult(rollbackJob, persistenceResult.getRollbackChanges(), false); |
| | | return buildResult(rollbackJob, persistenceResult.getRollbackChanges(), false, controlMode); |
| | | } catch (RuntimeException exception) { |
| | | Date failureNow = new Date(); |
| | | AiAutoTuneJob failureJob = createRollbackJob(reason, failureNow); |
| | |
| | | exception, |
| | | failureNow |
| | | ); |
| | | return buildResult(failureJob, persistenceResult.getRollbackChanges(), false); |
| | | return buildResult(failureJob, persistenceResult.getRollbackChanges(), false, controlMode); |
| | | } |
| | | } finally { |
| | | redisUtil.compareAndDelete(lockKey, lockToken); |
| | |
| | | |
| | | private AutoTuneApplyResult rejectRollbackForUnavailableLock(String reason, |
| | | Date now, |
| | | String lockKey) { |
| | | String lockKey, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | boolean lockKeyExists = redisUtil.hasKey(lockKey); |
| | | LOGGER.warn("申请AI自动调参 rollback 锁失败,lockKey={}, lockKeyExists={}", lockKey, lockKeyExists); |
| | | AiAutoTuneJob rollbackJob = createRollbackJob(reason, now); |
| | |
| | | rollbackJob, |
| | | now |
| | | ); |
| | | return buildResult(rollbackJob, persistenceResult.getRollbackChanges(), false); |
| | | return buildResult(rollbackJob, persistenceResult.getRollbackChanges(), false, controlMode); |
| | | } |
| | | |
| | | private AutoTuneApplyResult rejectRollbackForAnalysisOnly(AiAutoTuneJob rollbackJob, |
| | | Date now, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | RollbackPersistenceResult persistenceResult = persistAnalysisOnlyRollbackRejectionInTransaction( |
| | | rollbackJob, |
| | | now |
| | | ); |
| | | AutoTuneApplyResult result = buildResult(rollbackJob, persistenceResult.getRollbackChanges(), false, controlMode); |
| | | result.setAnalysisOnly(true); |
| | | result.setNoApply(true); |
| | | return result; |
| | | } |
| | | |
| | | private List<ValidatedChange> validateChanges(AutoTuneApplyRequest request, boolean dryRun, Date now) { |
| | |
| | | }); |
| | | } |
| | | |
| | | private ApplyPersistenceResult persistAnalysisOnlyApplyRejectionInTransaction(AiAutoTuneJob job, |
| | | AutoTuneApplyRequest request, |
| | | Date now) { |
| | | TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); |
| | | return transactionTemplate.execute(status -> { |
| | | saveJob(job); |
| | | List<AiAutoTuneChange> auditChanges = buildAnalysisOnlyApplyChanges(job.getId(), request, now); |
| | | saveAuditChanges(auditChanges); |
| | | finishAnalysisOnlyRejectedJob(job, auditChanges, now); |
| | | updateJob(job); |
| | | return new ApplyPersistenceResult(auditChanges, false); |
| | | }); |
| | | } |
| | | |
| | | private RollbackPersistenceResult persistAnalysisOnlyRollbackRejectionInTransaction(AiAutoTuneJob rollbackJob, |
| | | Date now) { |
| | | TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); |
| | | return transactionTemplate.execute(status -> { |
| | | saveJob(rollbackJob); |
| | | List<AiAutoTuneChange> rollbackChanges = buildAnalysisOnlyRollbackChanges(rollbackJob.getId(), now); |
| | | saveAuditChanges(rollbackChanges); |
| | | finishAnalysisOnlyRejectedJob(rollbackJob, rollbackChanges, now); |
| | | updateJob(rollbackJob); |
| | | return new RollbackPersistenceResult(rollbackChanges, false); |
| | | }); |
| | | } |
| | | |
| | | private RollbackPersistenceResult rollbackChanges(Long rollbackJobId, List<AiAutoTuneChange> sourceChanges, Date now) { |
| | | List<AiAutoTuneChange> rollbackChanges = new ArrayList<>(); |
| | | boolean refreshConfigCache = false; |
| | |
| | | rollbackChange.setRejectReason(APPLY_LOCK_BUSY_REASON); |
| | | rollbackChange.setCreateTime(now); |
| | | return List.of(rollbackChange); |
| | | } |
| | | |
| | | private List<AiAutoTuneChange> buildAnalysisOnlyApplyChanges(Long jobId, |
| | | AutoTuneApplyRequest request, |
| | | Date now) { |
| | | List<AiAutoTuneChange> changes = new ArrayList<>(); |
| | | List<AutoTuneChangeCommand> commands = request == null ? null : request.getChanges(); |
| | | if (commands == null || commands.isEmpty()) { |
| | | changes.add(buildAnalysisOnlyChange(jobId, "analysis_only", "", "apply", null, now)); |
| | | return changes; |
| | | } |
| | | for (AutoTuneChangeCommand command : commands) { |
| | | changes.add(buildAnalysisOnlyChange( |
| | | jobId, |
| | | normalizeText(command == null ? null : command.getTargetType()), |
| | | normalizeText(command == null ? null : command.getTargetId()), |
| | | normalizeText(command == null ? null : command.getTargetKey()), |
| | | command == null ? null : command.getNewValue(), |
| | | now |
| | | )); |
| | | } |
| | | return changes; |
| | | } |
| | | |
| | | private List<AiAutoTuneChange> buildAnalysisOnlyRollbackChanges(Long jobId, Date now) { |
| | | AiAutoTuneChange rollbackChange = buildAnalysisOnlyChange( |
| | | jobId, |
| | | "analysis_only", |
| | | "", |
| | | "rollback", |
| | | null, |
| | | now |
| | | ); |
| | | return List.of(rollbackChange); |
| | | } |
| | | |
| | | private AiAutoTuneChange buildAnalysisOnlyChange(Long jobId, |
| | | String targetType, |
| | | String targetId, |
| | | String targetKey, |
| | | String requestedValue, |
| | | Date now) { |
| | | AiAutoTuneChange change = new AiAutoTuneChange(); |
| | | change.setJobId(jobId); |
| | | change.setTargetType(targetType); |
| | | change.setTargetId(targetId); |
| | | change.setTargetKey(targetKey); |
| | | change.setRequestedValue(requestedValue); |
| | | change.setResultStatus(ChangeStatus.REJECTED.getCode()); |
| | | change.setRejectReason(ANALYSIS_ONLY_REJECT_REASON); |
| | | change.setCreateTime(now); |
| | | return change; |
| | | } |
| | | |
| | | private void refreshRollbackConfigCacheSafely(RollbackPersistenceResult persistenceResult) { |
| | |
| | | } |
| | | } |
| | | |
| | | private void finishAnalysisOnlyRejectedJob(AiAutoTuneJob job, List<AiAutoTuneChange> changes, Date now) { |
| | | int rejectCount = Math.max(1, countRejected(changes)); |
| | | job.setFinishTime(now); |
| | | job.setSuccessCount(0); |
| | | job.setRejectCount(rejectCount); |
| | | job.setIntervalAfter(readIntervalMinutes()); |
| | | job.setStatus(AutoTuneJobStatus.REJECTED.getCode()); |
| | | job.setSummary(ANALYSIS_ONLY_REJECT_REASON); |
| | | job.setErrorMessage(ANALYSIS_ONLY_REJECT_REASON); |
| | | } |
| | | |
| | | private void finishRollbackJob(AiAutoTuneJob job, List<AiAutoTuneChange> changes, Date now) { |
| | | int successCount = countAccepted(changes); |
| | | int rejectCount = countRejected(changes); |
| | |
| | | return !AutoTuneTriggerType.ROLLBACK.getCode().equals(job.getTriggerType()); |
| | | } |
| | | |
| | | private AutoTuneApplyResult buildResult(AiAutoTuneJob job, List<AiAutoTuneChange> changes, boolean dryRun) { |
| | | private AutoTuneApplyResult buildResult(AiAutoTuneJob job, |
| | | List<AiAutoTuneChange> changes, |
| | | boolean dryRun, |
| | | AutoTuneControlModeSnapshot controlMode) { |
| | | AutoTuneApplyResult result = new AutoTuneApplyResult(); |
| | | result.setDryRun(dryRun); |
| | | result.setSuccess(AutoTuneJobStatus.SUCCESS.getCode().equals(job.getStatus()) |
| | | || AutoTuneJobStatus.NO_CHANGE.getCode().equals(job.getStatus())); |
| | | result.setAnalysisOnly(isAnalysisOnly(controlMode)); |
| | | result.setNoApply(false); |
| | | result.setJobId(job.getId()); |
| | | result.setSummary(job.getSummary()); |
| | | result.setSuccessCount(job.getSuccessCount()); |
| | |
| | | } |
| | | } |
| | | |
| | | private AutoTuneControlModeSnapshot currentControlModeSnapshot() { |
| | | return autoTuneControlModeService.currentMode(); |
| | | } |
| | | |
| | | private boolean isAnalysisOnly(AutoTuneControlModeSnapshot controlMode) { |
| | | return controlMode == null || Boolean.TRUE.equals(controlMode.getAnalysisOnly()); |
| | | } |
| | | |
| | | private String toText(Integer value) { |
| | | return value == null ? null : String.valueOf(value); |
| | | } |
| New file |
| | |
| | | package com.zy.ai.service.impl; |
| | | |
| | | import com.zy.ai.domain.autotune.AutoTuneControlModeSnapshot; |
| | | import com.zy.ai.service.AutoTuneControlModeService; |
| | | import com.zy.system.service.ConfigService; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service("autoTuneControlModeService") |
| | | @RequiredArgsConstructor |
| | | public class AutoTuneControlModeServiceImpl implements AutoTuneControlModeService { |
| | | |
| | | private static final String CONFIG_AUTO_TUNE_ENABLED = "aiAutoTuneEnabled"; |
| | | private static final String CONFIG_ANALYSIS_ONLY = "aiAutoTuneAnalysisOnly"; |
| | | private static final String MODE_ANALYSIS_ONLY = "analysis_only"; |
| | | private static final String MODE_APPLY_ENABLED = "apply_enabled"; |
| | | |
| | | private final ConfigService configService; |
| | | |
| | | @Override |
| | | public AutoTuneControlModeSnapshot currentMode() { |
| | | boolean enabled = readConfigBoolean(CONFIG_AUTO_TUNE_ENABLED, false); |
| | | boolean analysisOnly = readConfigBoolean(CONFIG_ANALYSIS_ONLY, true); |
| | | |
| | | AutoTuneControlModeSnapshot snapshot = new AutoTuneControlModeSnapshot(); |
| | | snapshot.setEnabled(enabled); |
| | | snapshot.setAnalysisOnly(analysisOnly); |
| | | snapshot.setAllowApply(!analysisOnly); |
| | | snapshot.setModeCode(analysisOnly ? MODE_ANALYSIS_ONLY : MODE_APPLY_ENABLED); |
| | | snapshot.setModeLabel(analysisOnly ? "仅分析模式" : "允许正式调参"); |
| | | return snapshot; |
| | | } |
| | | |
| | | private boolean readConfigBoolean(String code, boolean defaultValue) { |
| | | String value = configService.getConfigValue(code, defaultValue ? "Y" : "N"); |
| | | if (value == null || value.trim().isEmpty()) { |
| | | return defaultValue; |
| | | } |
| | | String normalized = value.trim(); |
| | | return "Y".equalsIgnoreCase(normalized) |
| | | || "true".equalsIgnoreCase(normalized) |
| | | || "1".equals(normalized); |
| | | } |
| | | } |
| | |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.zy.ai.domain.autotune.AutoTuneControlModeSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneJobStatus; |
| | | import com.zy.ai.domain.autotune.AutoTuneTriggerType; |
| | | import com.zy.ai.entity.AiAutoTuneMcpCall; |
| | |
| | | import com.zy.ai.service.AiAutoTuneJobService; |
| | | import com.zy.ai.service.AiAutoTuneMcpCallService; |
| | | import com.zy.ai.service.AutoTuneAgentService; |
| | | import com.zy.ai.service.AutoTuneControlModeService; |
| | | import com.zy.ai.service.AutoTuneCoordinatorService; |
| | | import com.zy.asrs.entity.WrkMast; |
| | | import com.zy.asrs.service.WrkMastService; |
| | |
| | | @RequiredArgsConstructor |
| | | public class AutoTuneCoordinatorServiceImpl implements AutoTuneCoordinatorService { |
| | | |
| | | private static final String CONFIG_ENABLED = "aiAutoTuneEnabled"; |
| | | private static final String CONFIG_INTERVAL_MINUTES = "aiAutoTuneIntervalMinutes"; |
| | | private static final String DEFAULT_ENABLED = "N"; |
| | | private static final int DEFAULT_INTERVAL_MINUTES = 10; |
| | | private static final int MIN_INTERVAL_MINUTES = 5; |
| | | private static final int MAX_INTERVAL_MINUTES = 60; |
| | |
| | | ); |
| | | |
| | | private final ConfigService configService; |
| | | private final AutoTuneControlModeService autoTuneControlModeService; |
| | | private final WrkMastService wrkMastService; |
| | | private final AiAutoTuneJobService aiAutoTuneJobService; |
| | | private final AiAutoTuneMcpCallService aiAutoTuneMcpCallService; |
| | |
| | | } |
| | | |
| | | private boolean isEnabled() { |
| | | String enabled = configService.getConfigValue(CONFIG_ENABLED, DEFAULT_ENABLED); |
| | | if (enabled == null) { |
| | | return false; |
| | | } |
| | | String normalized = enabled.trim(); |
| | | return "Y".equalsIgnoreCase(normalized) |
| | | || "true".equalsIgnoreCase(normalized) |
| | | || "1".equals(normalized); |
| | | AutoTuneControlModeSnapshot controlMode = autoTuneControlModeService.currentMode(); |
| | | return controlMode != null && Boolean.TRUE.equals(controlMode.getEnabled()); |
| | | } |
| | | |
| | | private int resolveIntervalMinutes() { |
| | |
| | | } |
| | | |
| | | private AutoTuneAgentService.AutoTuneAgentResult failedAgentResult(String triggerType, Exception exception) { |
| | | AutoTuneControlModeSnapshot controlMode = autoTuneControlModeService.currentMode(); |
| | | AutoTuneAgentService.AutoTuneAgentResult result = new AutoTuneAgentService.AutoTuneAgentResult(); |
| | | result.setSuccess(false); |
| | | result.setTriggerType(triggerType); |
| | |
| | | result.setCompletionTokens(0L); |
| | | result.setTotalTokens(0L); |
| | | result.setMaxRoundsReached(false); |
| | | result.setAnalysisOnly(controlMode.getAnalysisOnly()); |
| | | result.setAllowApply(controlMode.getAllowApply()); |
| | | result.setExecutionMode(controlMode.getModeCode()); |
| | | result.setActualApplyCalled(false); |
| | | result.setRollbackCalled(false); |
| | | result.setSuccessCount(0); |
| | |
| | | private Map<String, Object> buildRequestSummary(AutoTuneAgentService.AutoTuneAgentResult agentResult) { |
| | | Map<String, Object> request = new LinkedHashMap<>(); |
| | | request.put("trigger", agentResult.getTriggerType()); |
| | | request.put("analysisOnly", agentResult.getAnalysisOnly()); |
| | | request.put("allowApply", agentResult.getAllowApply()); |
| | | request.put("executionMode", agentResult.getExecutionMode()); |
| | | return request; |
| | | } |
| | | |
| | |
| | | Map<String, Object> response = new LinkedHashMap<>(); |
| | | response.put("success", agentResult.getSuccess()); |
| | | response.put("summary", agentResult.getSummary()); |
| | | response.put("analysisOnly", agentResult.getAnalysisOnly()); |
| | | response.put("allowApply", agentResult.getAllowApply()); |
| | | response.put("executionMode", agentResult.getExecutionMode()); |
| | | response.put("toolCallCount", agentResult.getToolCallCount()); |
| | | response.put("llmCallCount", agentResult.getLlmCallCount()); |
| | | response.put("promptTokens", agentResult.getPromptTokens()); |
| | |
| | | package com.zy.ai.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.zy.ai.domain.autotune.AutoTuneControlModeSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskDetailItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneParameterSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneRoutePressureSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneRuleDefinition; |
| | | import com.zy.ai.domain.autotune.AutoTuneRuleSnapshotItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneStationRuntimeItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskSnapshot; |
| | | import com.zy.ai.service.AutoTuneControlModeService; |
| | | import com.zy.ai.service.AutoTuneSnapshotService; |
| | | import com.zy.ai.service.FlowTopologySnapshotService; |
| | | import com.zy.ai.service.RoutePressureSnapshotService; |
| | | import com.zy.asrs.domain.vo.StationCycleCapacityVo; |
| | | import com.zy.asrs.domain.vo.StationCycleLoopVo; |
| | | import com.zy.asrs.entity.BasCrnp; |
| | |
| | | import com.zy.core.model.protocol.StationProtocol; |
| | | import com.zy.core.thread.StationThread; |
| | | import com.zy.system.service.ConfigService; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | |
| | | @Service("autoTuneSnapshotService") |
| | | public class AutoTuneSnapshotServiceImpl implements AutoTuneSnapshotService { |
| | | |
| | | private static final Logger LOGGER = LoggerFactory.getLogger(AutoTuneSnapshotServiceImpl.class); |
| | | |
| | | private static final int DEFAULT_CRN_OUT_BATCH_RUNNING_LIMIT = 5; |
| | | private static final int DEFAULT_CONVEYOR_STATION_TASK_LIMIT = 30; |
| | | private static final int DEFAULT_AI_AUTO_TUNE_INTERVAL_MINUTES = 10; |
| | | private static final int OUTBOUND_TASK_SAMPLE_LIMIT = 50; |
| | | private static final int STATION_LIMIT_BLOCKED_TASK_LIMIT = 50; |
| | | private static final int SYSTEM_MESSAGE_LIMIT = 160; |
| | | |
| | | @Autowired |
| | | private WrkMastService wrkMastService; |
| | | |
| | |
| | | |
| | | @Autowired |
| | | private FlowTopologySnapshotService flowTopologySnapshotService; |
| | | |
| | | @Autowired |
| | | private RoutePressureSnapshotService routePressureSnapshotService; |
| | | |
| | | @Autowired |
| | | private AutoTuneControlModeService autoTuneControlModeService; |
| | | |
| | | @Autowired |
| | | private ConfigService configService; |
| | |
| | | |
| | | @Override |
| | | public AutoTuneSnapshot buildSnapshot() { |
| | | List<WrkMast> activeTasks = loadActiveTasks(); |
| | | AutoTuneTaskSnapshot taskSnapshot = buildTaskSnapshot(activeTasks); |
| | | List<AutoTuneStationRuntimeItem> stationRuntimeSnapshot = buildStationRuntimeSnapshot(); |
| | | AutoTuneRoutePressureSnapshot routePressureSnapshot = buildRoutePressureSnapshot( |
| | | activeTasks, |
| | | taskSnapshot, |
| | | stationRuntimeSnapshot |
| | | ); |
| | | |
| | | AutoTuneSnapshot snapshot = new AutoTuneSnapshot(); |
| | | snapshot.setTaskSnapshot(buildTaskSnapshot()); |
| | | snapshot.setTaskSnapshot(taskSnapshot); |
| | | snapshot.setStationRuntimeSnapshot(stationRuntimeSnapshot); |
| | | snapshot.setRoutePressureSnapshot(routePressureSnapshot); |
| | | snapshot.setCycleLoadSnapshot(buildCycleLoadSnapshot()); |
| | | snapshot.setFlowTopologySnapshot(flowTopologySnapshotService.buildSnapshot(stationRuntimeSnapshot)); |
| | | snapshot.setCurrentParameterSnapshot(buildCurrentParameterSnapshot()); |
| | | snapshot.setRuleSnapshot(buildRuleSnapshot()); |
| | | snapshot.setControlModeSnapshot(buildControlModeSnapshot()); |
| | | snapshot.setSnapshotTime(new Date()); |
| | | return snapshot; |
| | | } |
| | | |
| | | private AutoTuneControlModeSnapshot buildControlModeSnapshot() { |
| | | return autoTuneControlModeService.currentMode(); |
| | | } |
| | | |
| | | List<AutoTuneRuleSnapshotItem> buildRuleSnapshot() { |
| | |
| | | } |
| | | |
| | | private AutoTuneTaskSnapshot buildTaskSnapshot() { |
| | | List<WrkMast> activeTasks = loadActiveTasks(); |
| | | return buildTaskSnapshot(loadActiveTasks()); |
| | | } |
| | | |
| | | private AutoTuneTaskSnapshot buildTaskSnapshot(List<WrkMast> activeTasks) { |
| | | AutoTuneTaskSnapshot snapshot = new AutoTuneTaskSnapshot(); |
| | | snapshot.setActiveTaskCount(activeTasks.size()); |
| | | snapshot.setStationLimitBlockedTasks(buildStationLimitBlockedTasks(activeTasks)); |
| | |
| | | snapshot.setByDualCrn(countByDualCrn(activeTasks)); |
| | | snapshot.setByIoType(countByIoType(activeTasks)); |
| | | return snapshot; |
| | | } |
| | | |
| | | private AutoTuneRoutePressureSnapshot buildRoutePressureSnapshot(List<WrkMast> activeTasks, |
| | | AutoTuneTaskSnapshot taskSnapshot, |
| | | List<AutoTuneStationRuntimeItem> stationRuntimeSnapshot) { |
| | | if (routePressureSnapshotService == null) { |
| | | return new AutoTuneRoutePressureSnapshot(); |
| | | } |
| | | try { |
| | | AutoTuneRoutePressureSnapshot snapshot = routePressureSnapshotService.buildSnapshot( |
| | | activeTasks, |
| | | taskSnapshot, |
| | | stationRuntimeSnapshot |
| | | ); |
| | | return snapshot == null ? new AutoTuneRoutePressureSnapshot() : snapshot; |
| | | } catch (Exception exception) { |
| | | LOGGER.warn( |
| | | "Build auto tune route pressure snapshot failed, fallback to empty snapshot. activeTaskCount={}, stationRuntimeCount={}", |
| | | safeList(activeTasks).size(), |
| | | safeList(stationRuntimeSnapshot).size(), |
| | | exception |
| | | ); |
| | | return new AutoTuneRoutePressureSnapshot(); |
| | | } |
| | | } |
| | | |
| | | private List<AutoTuneTaskDetailItem> buildStationLimitBlockedTasks(List<WrkMast> activeTasks) { |
| | |
| | | item.setAutoing(protocol.isAutoing() ? 1 : 0); |
| | | item.setLoading(protocol.isLoading() ? 1 : 0); |
| | | item.setTaskNo(protocol.getTaskNo() == null ? 0 : protocol.getTaskNo()); |
| | | item.setRunBlock(protocol.isRunBlock() ? 1 : 0); |
| | | item.setIoMode(protocol.getIoMode() == null ? null : String.valueOf(protocol.getIoMode())); |
| | | return item; |
| | | } |
| New file |
| | |
| | | package com.zy.ai.service.impl; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.zy.ai.domain.autotune.AutoTuneHotPathSegmentItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneRoutePressureSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneRoutePressureRuleSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneStationRuntimeItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTargetStationRoutePressureItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskDetailItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskRouteSampleItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskSnapshot; |
| | | import com.zy.ai.service.RoutePressureSnapshotService; |
| | | import com.zy.asrs.domain.vo.StationTaskTraceVo; |
| | | import com.zy.asrs.entity.WrkMast; |
| | | import com.zy.common.model.NavigateNode; |
| | | import com.zy.common.utils.NavigateUtils; |
| | | import com.zy.core.enums.WrkIoType; |
| | | import com.zy.core.enums.WrkStsType; |
| | | import com.zy.core.trace.StationTaskTraceRegistry; |
| | | import com.zy.core.utils.station.StationOutboundDecisionSupport; |
| | | import com.zy.system.service.ConfigService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Comparator; |
| | | import java.util.HashSet; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | |
| | | @Service("routePressureSnapshotService") |
| | | public class RoutePressureSnapshotServiceImpl implements RoutePressureSnapshotService { |
| | | |
| | | private static final int MAX_ANALYZED_TASK_COUNT = 50; |
| | | private static final int DEFAULT_SEGMENT_WINDOW_SIZE = 4; |
| | | private static final int DEFAULT_MEDIUM_PERCENT = 50; |
| | | private static final int DEFAULT_HIGH_PERCENT = 75; |
| | | private static final int DEFAULT_PASS_WEIGHT_PERCENT = 35; |
| | | private static final int DEFAULT_OCCUPIED_WEIGHT_PERCENT = 25; |
| | | private static final int DEFAULT_BLOCKED_WEIGHT_PERCENT = 20; |
| | | private static final int DEFAULT_NON_AUTOING_WEIGHT_PERCENT = 10; |
| | | private static final int DEFAULT_RUN_BLOCK_WEIGHT_PERCENT = 10; |
| | | private static final int MAX_HOT_PATH_SEGMENT_COUNT = 10; |
| | | private static final int MAX_TARGET_MAIN_SEGMENT_COUNT = 3; |
| | | private static final String CONFIG_SEGMENT_WINDOW_SIZE = "aiAutoTuneRoutePressureSegmentWindowSize"; |
| | | private static final String CONFIG_MEDIUM_PERCENT = "aiAutoTuneRoutePressureMediumPercent"; |
| | | private static final String CONFIG_HIGH_PERCENT = "aiAutoTuneRoutePressureHighPercent"; |
| | | private static final String CONFIG_PASS_WEIGHT_PERCENT = "aiAutoTuneRoutePressurePassWeightPercent"; |
| | | private static final String CONFIG_OCCUPIED_WEIGHT_PERCENT = "aiAutoTuneRoutePressureOccupiedWeightPercent"; |
| | | private static final String CONFIG_BLOCKED_WEIGHT_PERCENT = "aiAutoTuneRoutePressureBlockedWeightPercent"; |
| | | private static final String CONFIG_NON_AUTOING_WEIGHT_PERCENT = "aiAutoTuneRoutePressureNonAutoingWeightPercent"; |
| | | private static final String CONFIG_RUN_BLOCK_WEIGHT_PERCENT = "aiAutoTuneRoutePressureRunBlockWeightPercent"; |
| | | private static final String PATH_SOURCE_TRACE_PENDING = "trace_pending"; |
| | | private static final String PATH_SOURCE_TRACE_ISSUED = "trace_issued"; |
| | | private static final String PATH_SOURCE_ESTIMATED = "estimated"; |
| | | private static final String PATH_SOURCE_MISSING = "missing"; |
| | | private static final String PRESSURE_HIGH = "high"; |
| | | private static final String PRESSURE_MEDIUM = "medium"; |
| | | private static final String PRESSURE_LOW = "low"; |
| | | private static final String DIRECTION_INCREASE_CANDIDATE = "increase_candidate"; |
| | | private static final String DIRECTION_OBSERVE = "observe"; |
| | | private static final String DIRECTION_DECREASE_CANDIDATE = "decrease_candidate"; |
| | | private static final String CONFIDENCE_HIGH = "high"; |
| | | private static final String CONFIDENCE_MEDIUM = "medium"; |
| | | private static final String CONFIDENCE_LOW = "low"; |
| | | |
| | | @Autowired |
| | | private StationTaskTraceRegistry stationTaskTraceRegistry; |
| | | |
| | | @Autowired |
| | | private NavigateUtils navigateUtils; |
| | | |
| | | @Autowired |
| | | private StationOutboundDecisionSupport stationOutboundDecisionSupport; |
| | | |
| | | @Autowired |
| | | private ConfigService configService; |
| | | |
| | | @Override |
| | | public AutoTuneRoutePressureSnapshot buildSnapshot(List<WrkMast> activeTasks, |
| | | AutoTuneTaskSnapshot taskSnapshot, |
| | | List<AutoTuneStationRuntimeItem> stationRuntimeSnapshot) { |
| | | AutoTuneRoutePressureSnapshot snapshot = new AutoTuneRoutePressureSnapshot(); |
| | | AutoTuneRoutePressureRuleSnapshot ruleSnapshot = buildRoutePressureRuleSnapshot(); |
| | | List<WrkMast> selectedTasks = selectTasksForAnalysis(activeTasks, taskSnapshot); |
| | | Map<Integer, StationTaskTraceVo> traceMap = buildTraceMap(); |
| | | Map<String, EstimatedPathResult> estimatedPathCache = new LinkedHashMap<>(); |
| | | List<AutoTuneTaskRouteSampleItem> routeSamples = new ArrayList<>(); |
| | | |
| | | for (WrkMast task : selectedTasks) { |
| | | AutoTuneTaskRouteSampleItem routeSample = buildRouteSample(task, traceMap, estimatedPathCache); |
| | | routeSamples.add(routeSample); |
| | | } |
| | | |
| | | snapshot.setAnalyzedTaskCount(selectedTasks.size()); |
| | | snapshot.setTracePathCount(countByPathSource(routeSamples, PATH_SOURCE_TRACE_PENDING) |
| | | + countByPathSource(routeSamples, PATH_SOURCE_TRACE_ISSUED)); |
| | | snapshot.setEstimatedPathCount(countByPathSource(routeSamples, PATH_SOURCE_ESTIMATED)); |
| | | snapshot.setPathErrorCount(countByPathSource(routeSamples, PATH_SOURCE_MISSING)); |
| | | snapshot.setConfidence(snapshotConfidence(snapshot.getAnalyzedTaskCount(), snapshot.getPathErrorCount())); |
| | | snapshot.setRoutePressureRuleSnapshot(ruleSnapshot); |
| | | snapshot.setTaskRouteSamples(routeSamples); |
| | | Map<Integer, AutoTuneStationRuntimeItem> runtimeMap = buildRuntimeMap(stationRuntimeSnapshot); |
| | | List<AutoTuneHotPathSegmentItem> allHotPathSegments = buildHotPathSegments( |
| | | routeSamples, |
| | | runtimeMap, |
| | | ruleSnapshot |
| | | ); |
| | | snapshot.setHotPathSegments(topHotPathSegments(allHotPathSegments)); |
| | | snapshot.setTargetStationRoutePressure(buildTargetStationRoutePressure( |
| | | routeSamples, |
| | | allHotPathSegments, |
| | | taskSnapshot, |
| | | ruleSnapshot |
| | | )); |
| | | return snapshot; |
| | | } |
| | | |
| | | private AutoTuneRoutePressureRuleSnapshot buildRoutePressureRuleSnapshot() { |
| | | AutoTuneRoutePressureRuleSnapshot snapshot = new AutoTuneRoutePressureRuleSnapshot(); |
| | | snapshot.setSegmentWindowSize(readIntConfig( |
| | | CONFIG_SEGMENT_WINDOW_SIZE, |
| | | DEFAULT_SEGMENT_WINDOW_SIZE, |
| | | 2, |
| | | 20 |
| | | )); |
| | | snapshot.setMediumPercent(readIntConfig(CONFIG_MEDIUM_PERCENT, DEFAULT_MEDIUM_PERCENT, 1, 100)); |
| | | snapshot.setHighPercent(readIntConfig(CONFIG_HIGH_PERCENT, DEFAULT_HIGH_PERCENT, 1, 100)); |
| | | if (snapshot.getMediumPercent() > snapshot.getHighPercent()) { |
| | | snapshot.setMediumPercent(DEFAULT_MEDIUM_PERCENT); |
| | | snapshot.setHighPercent(DEFAULT_HIGH_PERCENT); |
| | | } |
| | | snapshot.setPassWeightPercent(readIntConfig(CONFIG_PASS_WEIGHT_PERCENT, DEFAULT_PASS_WEIGHT_PERCENT, 0, 100)); |
| | | snapshot.setOccupiedWeightPercent(readIntConfig( |
| | | CONFIG_OCCUPIED_WEIGHT_PERCENT, |
| | | DEFAULT_OCCUPIED_WEIGHT_PERCENT, |
| | | 0, |
| | | 100 |
| | | )); |
| | | snapshot.setBlockedWeightPercent(readIntConfig( |
| | | CONFIG_BLOCKED_WEIGHT_PERCENT, |
| | | DEFAULT_BLOCKED_WEIGHT_PERCENT, |
| | | 0, |
| | | 100 |
| | | )); |
| | | snapshot.setNonAutoingWeightPercent(readIntConfig( |
| | | CONFIG_NON_AUTOING_WEIGHT_PERCENT, |
| | | DEFAULT_NON_AUTOING_WEIGHT_PERCENT, |
| | | 0, |
| | | 100 |
| | | )); |
| | | snapshot.setRunBlockWeightPercent(readIntConfig( |
| | | CONFIG_RUN_BLOCK_WEIGHT_PERCENT, |
| | | DEFAULT_RUN_BLOCK_WEIGHT_PERCENT, |
| | | 0, |
| | | 100 |
| | | )); |
| | | return snapshot; |
| | | } |
| | | |
| | | private int readIntConfig(String code, int defaultValue, int minValue, int maxValue) { |
| | | if (configService == null) { |
| | | return defaultValue; |
| | | } |
| | | String value = configService.getConfigValue(code, String.valueOf(defaultValue)); |
| | | try { |
| | | if (value == null || value.trim().isEmpty()) { |
| | | return defaultValue; |
| | | } |
| | | int parsedValue = Integer.parseInt(value.trim()); |
| | | if (parsedValue < minValue || parsedValue > maxValue) { |
| | | return defaultValue; |
| | | } |
| | | return parsedValue; |
| | | } catch (Exception e) { |
| | | return defaultValue; |
| | | } |
| | | } |
| | | |
| | | private List<WrkMast> selectTasksForAnalysis(List<WrkMast> activeTasks, |
| | | AutoTuneTaskSnapshot taskSnapshot) { |
| | | List<WrkMast> sortedOutboundTasks = sortedOutboundTasks(activeTasks); |
| | | Set<Integer> blockedWrkNos = collectBlockedWrkNos(taskSnapshot); |
| | | LinkedHashMap<Integer, WrkMast> selected = new LinkedHashMap<>(); |
| | | |
| | | for (WrkMast task : sortedOutboundTasks) { |
| | | Integer wrkNo = task.getWrkNo(); |
| | | if (wrkNo != null && blockedWrkNos.contains(wrkNo)) { |
| | | selected.put(wrkNo, task); |
| | | } |
| | | if (selected.size() >= MAX_ANALYZED_TASK_COUNT) { |
| | | return new ArrayList<>(selected.values()); |
| | | } |
| | | } |
| | | |
| | | for (WrkMast task : sortedOutboundTasks) { |
| | | Integer wrkNo = task.getWrkNo(); |
| | | if (wrkNo != null) { |
| | | selected.putIfAbsent(wrkNo, task); |
| | | } |
| | | if (selected.size() >= MAX_ANALYZED_TASK_COUNT) { |
| | | break; |
| | | } |
| | | } |
| | | return new ArrayList<>(selected.values()); |
| | | } |
| | | |
| | | private Set<Integer> collectBlockedWrkNos(AutoTuneTaskSnapshot taskSnapshot) { |
| | | Set<Integer> blockedWrkNos = new HashSet<>(); |
| | | if (taskSnapshot == null || taskSnapshot.getStationLimitBlockedTasks() == null) { |
| | | return blockedWrkNos; |
| | | } |
| | | for (AutoTuneTaskDetailItem blockedTask : taskSnapshot.getStationLimitBlockedTasks()) { |
| | | if (blockedTask != null && blockedTask.getWrkNo() != null) { |
| | | blockedWrkNos.add(blockedTask.getWrkNo()); |
| | | } |
| | | } |
| | | return blockedWrkNos; |
| | | } |
| | | |
| | | private List<WrkMast> sortedOutboundTasks(List<WrkMast> activeTasks) { |
| | | List<WrkMast> sortedTasks = new ArrayList<>(); |
| | | if (activeTasks == null) { |
| | | return sortedTasks; |
| | | } |
| | | for (WrkMast task : activeTasks) { |
| | | if (task != null && Objects.equals(task.getIoType(), WrkIoType.OUT.id)) { |
| | | sortedTasks.add(task); |
| | | } |
| | | } |
| | | sortedTasks.sort(Comparator |
| | | .comparing(WrkMast::getBatch, Comparator.nullsLast(String::compareTo)) |
| | | .thenComparing(WrkMast::getBatchSeq, Comparator.nullsLast(Integer::compareTo)) |
| | | .thenComparing(WrkMast::getWrkNo, Comparator.nullsLast(Integer::compareTo))); |
| | | return sortedTasks; |
| | | } |
| | | |
| | | private Map<Integer, StationTaskTraceVo> buildTraceMap() { |
| | | Map<Integer, StationTaskTraceVo> traceMap = new LinkedHashMap<>(); |
| | | if (stationTaskTraceRegistry == null) { |
| | | return traceMap; |
| | | } |
| | | List<StationTaskTraceVo> traceSnapshots; |
| | | try { |
| | | traceSnapshots = stationTaskTraceRegistry.listPlanningActiveTraceSnapshots(); |
| | | } catch (Exception e) { |
| | | return traceMap; |
| | | } |
| | | if (traceSnapshots == null) { |
| | | return traceMap; |
| | | } |
| | | for (StationTaskTraceVo traceSnapshot : traceSnapshots) { |
| | | if (traceSnapshot != null && traceSnapshot.getTaskNo() != null) { |
| | | traceMap.putIfAbsent(traceSnapshot.getTaskNo(), traceSnapshot); |
| | | } |
| | | } |
| | | return traceMap; |
| | | } |
| | | |
| | | private AutoTuneTaskRouteSampleItem buildRouteSample(WrkMast task, |
| | | Map<Integer, StationTaskTraceVo> traceMap, |
| | | Map<String, EstimatedPathResult> estimatedPathCache) { |
| | | AutoTuneTaskRouteSampleItem item = baseRouteSample(task); |
| | | if (!isRunningStationTask(task)) { |
| | | return buildEstimatedRouteSample(item, task, estimatedPathCache); |
| | | } |
| | | |
| | | StationTaskTraceVo trace = task == null || task.getWrkNo() == null ? null : traceMap.get(task.getWrkNo()); |
| | | if (trace == null) { |
| | | return completeRouteSample(item, PATH_SOURCE_MISSING, Collections.emptyList(), |
| | | "missing station trace for running station task"); |
| | | } |
| | | |
| | | List<Integer> remainingPath = traceRemainingPath(trace); |
| | | if (!remainingPath.isEmpty()) { |
| | | return completeRouteSample(item, PATH_SOURCE_TRACE_PENDING, remainingPath, null); |
| | | } |
| | | |
| | | List<Integer> issuedPath = firstNonEmptyDistinct( |
| | | trace == null ? null : trace.getLatestIssuedSegmentPath(), |
| | | trace == null ? null : trace.getIssuedStationIds() |
| | | ); |
| | | if (!issuedPath.isEmpty()) { |
| | | return completeRouteSample(item, PATH_SOURCE_TRACE_ISSUED, issuedPath, null); |
| | | } |
| | | |
| | | return completeRouteSample(item, PATH_SOURCE_MISSING, Collections.emptyList(), |
| | | "missing usable station trace path for running station task"); |
| | | } |
| | | |
| | | private List<Integer> traceRemainingPath(StationTaskTraceVo trace) { |
| | | if (trace == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<Integer> pendingPath = distinctPositive(trace.getPendingStationIds()); |
| | | Integer currentStationId = trace.getCurrentStationId(); |
| | | if (currentStationId == null || currentStationId <= 0) { |
| | | return pendingPath; |
| | | } |
| | | |
| | | List<Integer> remainingPath = new ArrayList<>(); |
| | | remainingPath.add(currentStationId); |
| | | Integer previousStationId = currentStationId; |
| | | for (Integer stationId : pendingPath) { |
| | | if (Objects.equals(stationId, previousStationId)) { |
| | | continue; |
| | | } |
| | | remainingPath.add(stationId); |
| | | previousStationId = stationId; |
| | | } |
| | | return remainingPath; |
| | | } |
| | | |
| | | private boolean isRunningStationTask(WrkMast task) { |
| | | return task != null && Objects.equals(task.getWrkSts(), WrkStsType.STATION_RUN.sts); |
| | | } |
| | | |
| | | private AutoTuneTaskRouteSampleItem baseRouteSample(WrkMast task) { |
| | | AutoTuneTaskRouteSampleItem item = new AutoTuneTaskRouteSampleItem(); |
| | | if (task == null) { |
| | | return item; |
| | | } |
| | | item.setWrkNo(task.getWrkNo()); |
| | | item.setWrkSts(task.getWrkSts()); |
| | | item.setBatch(task.getBatch()); |
| | | item.setBatchSeq(task.getBatchSeq()); |
| | | item.setSourceStaNo(task.getSourceStaNo()); |
| | | item.setTargetStaNo(task.getStaNo()); |
| | | return item; |
| | | } |
| | | |
| | | private AutoTuneTaskRouteSampleItem buildEstimatedRouteSample(AutoTuneTaskRouteSampleItem item, |
| | | WrkMast task, |
| | | Map<String, EstimatedPathResult> cache) { |
| | | if (task == null || task.getSourceStaNo() == null || task.getStaNo() == null) { |
| | | return completeRouteSample(item, PATH_SOURCE_MISSING, Collections.emptyList(), |
| | | "missing sourceStaNo or staNo"); |
| | | } |
| | | |
| | | Double pathLenFactor = resolvePathLenFactor(task); |
| | | String cacheKey = buildEstimatedPathCacheKey(task, pathLenFactor); |
| | | EstimatedPathResult pathResult = cache.get(cacheKey); |
| | | if (pathResult == null) { |
| | | pathResult = estimatePath(task, pathLenFactor); |
| | | cache.put(cacheKey, pathResult); |
| | | } |
| | | if (pathResult.pathStationIds.isEmpty()) { |
| | | return completeRouteSample(item, PATH_SOURCE_MISSING, Collections.emptyList(), pathResult.pathError); |
| | | } |
| | | return completeRouteSample(item, PATH_SOURCE_ESTIMATED, pathResult.pathStationIds, null); |
| | | } |
| | | |
| | | private Double resolvePathLenFactor(WrkMast task) { |
| | | if (stationOutboundDecisionSupport == null) { |
| | | return 0.0d; |
| | | } |
| | | try { |
| | | Double pathLenFactor = stationOutboundDecisionSupport.resolveOutboundPathLenFactor(task); |
| | | return pathLenFactor == null ? 0.0d : pathLenFactor; |
| | | } catch (Exception e) { |
| | | return 0.0d; |
| | | } |
| | | } |
| | | |
| | | private String buildEstimatedPathCacheKey(WrkMast task, Double pathLenFactor) { |
| | | return task.getWrkNo() + ":" + task.getSourceStaNo() + "->" + task.getStaNo() + ":" + pathLenFactor; |
| | | } |
| | | |
| | | private EstimatedPathResult estimatePath(WrkMast task, Double pathLenFactor) { |
| | | List<NavigateNode> navigateNodes; |
| | | try { |
| | | navigateNodes = navigateUtils.calcOptimalPathByStationId( |
| | | task.getSourceStaNo(), |
| | | task.getStaNo(), |
| | | task.getWrkNo(), |
| | | pathLenFactor |
| | | ); |
| | | } catch (Exception e) { |
| | | return EstimatedPathResult.error("path estimate exception: " + e.getClass().getSimpleName()); |
| | | } |
| | | List<Integer> pathStationIds = stationIdsFromNavigateNodes(navigateNodes); |
| | | if (pathStationIds.isEmpty()) { |
| | | return EstimatedPathResult.error("path estimate failed"); |
| | | } |
| | | return EstimatedPathResult.success(pathStationIds); |
| | | } |
| | | |
| | | private List<Integer> stationIdsFromNavigateNodes(List<NavigateNode> navigateNodes) { |
| | | List<Integer> stationIds = new ArrayList<>(); |
| | | if (navigateNodes == null) { |
| | | return stationIds; |
| | | } |
| | | for (NavigateNode navigateNode : navigateNodes) { |
| | | Integer stationId = stationIdFromNavigateNode(navigateNode); |
| | | if (stationId != null && stationId > 0) { |
| | | stationIds.add(stationId); |
| | | } |
| | | } |
| | | return stationIds; |
| | | } |
| | | |
| | | private Integer stationIdFromNavigateNode(NavigateNode navigateNode) { |
| | | if (navigateNode == null) { |
| | | return null; |
| | | } |
| | | if (navigateNode.getStationId() != null) { |
| | | return navigateNode.getStationId(); |
| | | } |
| | | String nodeValue = navigateNode.getNodeValue(); |
| | | if (nodeValue == null || nodeValue.trim().isEmpty()) { |
| | | return null; |
| | | } |
| | | try { |
| | | JSONObject value = JSON.parseObject(nodeValue); |
| | | return value == null ? null : value.getInteger("stationId"); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | @SafeVarargs |
| | | private final List<Integer> firstNonEmptyDistinct(List<Integer>... pathCandidates) { |
| | | if (pathCandidates == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | for (List<Integer> pathCandidate : pathCandidates) { |
| | | List<Integer> pathStationIds = distinctPositive(pathCandidate); |
| | | if (!pathStationIds.isEmpty()) { |
| | | return pathStationIds; |
| | | } |
| | | } |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | private List<Integer> distinctPositive(List<Integer> source) { |
| | | List<Integer> result = new ArrayList<>(); |
| | | if (source == null) { |
| | | return result; |
| | | } |
| | | Integer previousStationId = null; |
| | | for (Integer stationId : source) { |
| | | if (stationId == null || stationId <= 0 || Objects.equals(stationId, previousStationId)) { |
| | | continue; |
| | | } |
| | | result.add(stationId); |
| | | previousStationId = stationId; |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private AutoTuneTaskRouteSampleItem completeRouteSample(AutoTuneTaskRouteSampleItem item, |
| | | String pathSource, |
| | | List<Integer> pathStationIds, |
| | | String pathError) { |
| | | List<Integer> safePathStationIds = pathStationIds == null ? Collections.emptyList() : new ArrayList<>(pathStationIds); |
| | | item.setPathSource(pathSource); |
| | | item.setPathStationIds(safePathStationIds); |
| | | item.setPathLength(safePathStationIds.size()); |
| | | item.setPathError(pathError); |
| | | return item; |
| | | } |
| | | |
| | | private int countByPathSource(List<AutoTuneTaskRouteSampleItem> routeSamples, String pathSource) { |
| | | int count = 0; |
| | | for (AutoTuneTaskRouteSampleItem routeSample : routeSamples) { |
| | | if (routeSample != null && pathSource.equals(routeSample.getPathSource())) { |
| | | count++; |
| | | } |
| | | } |
| | | return count; |
| | | } |
| | | |
| | | private Map<Integer, AutoTuneStationRuntimeItem> buildRuntimeMap(List<AutoTuneStationRuntimeItem> stationRuntimeSnapshot) { |
| | | Map<Integer, AutoTuneStationRuntimeItem> runtimeMap = new LinkedHashMap<>(); |
| | | if (stationRuntimeSnapshot == null) { |
| | | return runtimeMap; |
| | | } |
| | | for (AutoTuneStationRuntimeItem runtimeItem : stationRuntimeSnapshot) { |
| | | if (runtimeItem != null && runtimeItem.getStationId() != null) { |
| | | runtimeMap.putIfAbsent(runtimeItem.getStationId(), runtimeItem); |
| | | } |
| | | } |
| | | return runtimeMap; |
| | | } |
| | | |
| | | private List<AutoTuneHotPathSegmentItem> buildHotPathSegments(List<AutoTuneTaskRouteSampleItem> routeSamples, |
| | | Map<Integer, AutoTuneStationRuntimeItem> runtimeMap, |
| | | AutoTuneRoutePressureRuleSnapshot ruleSnapshot) { |
| | | LinkedHashMap<String, SegmentAggregate> aggregateMap = new LinkedHashMap<>(); |
| | | if (routeSamples == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | int validRouteSampleCount = 0; |
| | | for (AutoTuneTaskRouteSampleItem routeSample : routeSamples) { |
| | | if (!hasValidPath(routeSample)) { |
| | | continue; |
| | | } |
| | | validRouteSampleCount++; |
| | | Set<String> sampleSegmentKeys = new HashSet<>(); |
| | | List<List<Integer>> routeSegments = splitRouteSegments(routeSample.getPathStationIds(), ruleSnapshot); |
| | | for (List<Integer> routeSegment : routeSegments) { |
| | | String segmentKey = buildSegmentKey(routeSegment); |
| | | if (segmentKey == null || !sampleSegmentKeys.add(segmentKey)) { |
| | | continue; |
| | | } |
| | | SegmentAggregate aggregate = aggregateMap.computeIfAbsent( |
| | | segmentKey, |
| | | key -> new SegmentAggregate(key, routeSegment) |
| | | ); |
| | | aggregate.addRouteSample(routeSample); |
| | | } |
| | | } |
| | | List<AutoTuneHotPathSegmentItem> hotPathSegments = new ArrayList<>(); |
| | | for (SegmentAggregate aggregate : aggregateMap.values()) { |
| | | hotPathSegments.add(aggregate.toItem(runtimeMap, ruleSnapshot, validRouteSampleCount)); |
| | | } |
| | | hotPathSegments.sort(this::compareHotPathSegment); |
| | | return hotPathSegments; |
| | | } |
| | | |
| | | private boolean hasValidPath(AutoTuneTaskRouteSampleItem routeSample) { |
| | | return routeSample != null |
| | | && routeSample.getTargetStaNo() != null |
| | | && routeSample.getPathStationIds() != null |
| | | && routeSample.getPathStationIds().size() >= 2 |
| | | && !PATH_SOURCE_MISSING.equals(routeSample.getPathSource()); |
| | | } |
| | | |
| | | private List<List<Integer>> splitRouteSegments(List<Integer> pathStationIds, |
| | | AutoTuneRoutePressureRuleSnapshot ruleSnapshot) { |
| | | if (pathStationIds == null || pathStationIds.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<Integer> safePathStationIds = new ArrayList<>(pathStationIds); |
| | | int segmentWindowSize = ruleWindowSize(ruleSnapshot); |
| | | if (safePathStationIds.size() <= segmentWindowSize) { |
| | | return Collections.singletonList(safePathStationIds); |
| | | } |
| | | List<List<Integer>> routeSegments = new ArrayList<>(); |
| | | for (int startIndex = 0; startIndex <= safePathStationIds.size() - segmentWindowSize; startIndex++) { |
| | | int endIndex = startIndex + segmentWindowSize; |
| | | routeSegments.add(new ArrayList<>(safePathStationIds.subList(startIndex, endIndex))); |
| | | } |
| | | return routeSegments; |
| | | } |
| | | |
| | | private int ruleWindowSize(AutoTuneRoutePressureRuleSnapshot ruleSnapshot) { |
| | | Integer segmentWindowSize = ruleSnapshot == null ? null : ruleSnapshot.getSegmentWindowSize(); |
| | | return segmentWindowSize == null || segmentWindowSize < 2 ? DEFAULT_SEGMENT_WINDOW_SIZE : segmentWindowSize; |
| | | } |
| | | |
| | | private String buildSegmentKey(List<Integer> stationIds) { |
| | | if (stationIds == null || stationIds.isEmpty()) { |
| | | return null; |
| | | } |
| | | StringBuilder segmentKey = new StringBuilder(); |
| | | for (Integer stationId : stationIds) { |
| | | if (stationId == null) { |
| | | return null; |
| | | } |
| | | if (segmentKey.length() > 0) { |
| | | segmentKey.append("-"); |
| | | } |
| | | segmentKey.append(stationId); |
| | | } |
| | | return segmentKey.toString(); |
| | | } |
| | | |
| | | private int compareHotPathSegment(AutoTuneHotPathSegmentItem left, AutoTuneHotPathSegmentItem right) { |
| | | int pressureCompare = Integer.compare( |
| | | pressureRank(right.getPressureLevel()), |
| | | pressureRank(left.getPressureLevel()) |
| | | ); |
| | | if (pressureCompare != 0) { |
| | | return pressureCompare; |
| | | } |
| | | int scoreCompare = Integer.compare(nullSafe(right.getPressureScore()), nullSafe(left.getPressureScore())); |
| | | if (scoreCompare != 0) { |
| | | return scoreCompare; |
| | | } |
| | | int passCompare = Integer.compare(nullSafe(right.getPassTaskCount()), nullSafe(left.getPassTaskCount())); |
| | | if (passCompare != 0) { |
| | | return passCompare; |
| | | } |
| | | int runBlockCompare = Integer.compare(nullSafe(right.getRunBlockCount()), nullSafe(left.getRunBlockCount())); |
| | | if (runBlockCompare != 0) { |
| | | return runBlockCompare; |
| | | } |
| | | int taskHoldingCompare = Integer.compare( |
| | | nullSafe(right.getTaskHoldingCount()), |
| | | nullSafe(left.getTaskHoldingCount()) |
| | | ); |
| | | if (taskHoldingCompare != 0) { |
| | | return taskHoldingCompare; |
| | | } |
| | | int loadingCompare = Integer.compare(nullSafe(right.getLoadingCount()), nullSafe(left.getLoadingCount())); |
| | | if (loadingCompare != 0) { |
| | | return loadingCompare; |
| | | } |
| | | return nullSafeString(left.getSegmentKey()).compareTo(nullSafeString(right.getSegmentKey())); |
| | | } |
| | | |
| | | private int pressureRank(String pressureLevel) { |
| | | if (PRESSURE_HIGH.equals(pressureLevel)) { |
| | | return 3; |
| | | } |
| | | if (PRESSURE_MEDIUM.equals(pressureLevel)) { |
| | | return 2; |
| | | } |
| | | return 1; |
| | | } |
| | | |
| | | private int nullSafe(Integer value) { |
| | | return value == null ? 0 : value; |
| | | } |
| | | |
| | | private String nullSafeString(String value) { |
| | | return value == null ? "" : value; |
| | | } |
| | | |
| | | private List<AutoTuneHotPathSegmentItem> topHotPathSegments(List<AutoTuneHotPathSegmentItem> allHotPathSegments) { |
| | | if (allHotPathSegments == null || allHotPathSegments.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | int toIndex = Math.min(MAX_HOT_PATH_SEGMENT_COUNT, allHotPathSegments.size()); |
| | | return new ArrayList<>(allHotPathSegments.subList(0, toIndex)); |
| | | } |
| | | |
| | | private List<AutoTuneTargetStationRoutePressureItem> buildTargetStationRoutePressure( |
| | | List<AutoTuneTaskRouteSampleItem> routeSamples, |
| | | List<AutoTuneHotPathSegmentItem> allHotPathSegments, |
| | | AutoTuneTaskSnapshot taskSnapshot, |
| | | AutoTuneRoutePressureRuleSnapshot ruleSnapshot) { |
| | | LinkedHashMap<Integer, TargetPressureAggregate> targetAggregateMap = new LinkedHashMap<>(); |
| | | Set<Integer> blockedWrkNos = collectBlockedWrkNos(taskSnapshot); |
| | | |
| | | if (routeSamples != null) { |
| | | for (AutoTuneTaskRouteSampleItem routeSample : routeSamples) { |
| | | if (!hasValidPath(routeSample)) { |
| | | continue; |
| | | } |
| | | Integer targetStaNo = routeSample.getTargetStaNo(); |
| | | TargetPressureAggregate aggregate = targetAggregateMap.computeIfAbsent( |
| | | targetStaNo, |
| | | TargetPressureAggregate::new |
| | | ); |
| | | aggregate.routeTaskCount++; |
| | | if (routeSample.getWrkNo() != null && blockedWrkNos.contains(routeSample.getWrkNo())) { |
| | | aggregate.blockedTaskCount++; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (allHotPathSegments != null) { |
| | | for (AutoTuneHotPathSegmentItem hotPathSegment : allHotPathSegments) { |
| | | if (hotPathSegment == null || hotPathSegment.getRelatedTargetStations() == null) { |
| | | continue; |
| | | } |
| | | for (Integer targetStationId : hotPathSegment.getRelatedTargetStations()) { |
| | | TargetPressureAggregate aggregate = targetAggregateMap.get(targetStationId); |
| | | if (aggregate != null) { |
| | | aggregate.relatedHotSegments.add(hotPathSegment); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | List<AutoTuneTargetStationRoutePressureItem> targetPressureItems = new ArrayList<>(); |
| | | for (TargetPressureAggregate aggregate : targetAggregateMap.values()) { |
| | | targetPressureItems.add(aggregate.toItem(ruleSnapshot)); |
| | | } |
| | | targetPressureItems.sort(Comparator.comparing( |
| | | AutoTuneTargetStationRoutePressureItem::getTargetStationId, |
| | | Comparator.nullsLast(Integer::compareTo) |
| | | )); |
| | | return targetPressureItems; |
| | | } |
| | | |
| | | private int calculateSegmentPressureScore(int passTaskCount, |
| | | int taskHoldingCount, |
| | | int loadingCount, |
| | | int runBlockCount, |
| | | int nonAutoingCount, |
| | | int segmentStationCount, |
| | | int validRouteSampleCount, |
| | | AutoTuneRoutePressureRuleSnapshot ruleSnapshot) { |
| | | double passRatio = ratio(passTaskCount, validRouteSampleCount); |
| | | double occupiedRatio = ratio(taskHoldingCount + loadingCount, segmentStationCount); |
| | | double nonAutoingRatio = ratio(nonAutoingCount, segmentStationCount); |
| | | double runBlockRatio = ratio(runBlockCount, segmentStationCount); |
| | | double score = passRatio * nullSafe(ruleSnapshot.getPassWeightPercent()) |
| | | + occupiedRatio * nullSafe(ruleSnapshot.getOccupiedWeightPercent()) |
| | | + nonAutoingRatio * nullSafe(ruleSnapshot.getNonAutoingWeightPercent()) |
| | | + runBlockRatio * nullSafe(ruleSnapshot.getRunBlockWeightPercent()); |
| | | return clampPercent((int) Math.round(score)); |
| | | } |
| | | |
| | | private String calculatePressureLevel(int pressureScore, AutoTuneRoutePressureRuleSnapshot ruleSnapshot) { |
| | | if (pressureScore >= nullSafe(ruleSnapshot.getHighPercent())) { |
| | | return PRESSURE_HIGH; |
| | | } |
| | | if (pressureScore >= nullSafe(ruleSnapshot.getMediumPercent())) { |
| | | return PRESSURE_MEDIUM; |
| | | } |
| | | return PRESSURE_LOW; |
| | | } |
| | | |
| | | private Map<String, Object> segmentPressureFactors(int passTaskCount, |
| | | int taskHoldingCount, |
| | | int loadingCount, |
| | | int runBlockCount, |
| | | int nonAutoingCount, |
| | | int segmentStationCount, |
| | | int validRouteSampleCount) { |
| | | Map<String, Object> factors = new LinkedHashMap<>(); |
| | | factors.put("passRatio", percentValue(passTaskCount, validRouteSampleCount)); |
| | | factors.put("occupiedRatio", percentValue(taskHoldingCount + loadingCount, segmentStationCount)); |
| | | factors.put("nonAutoingRatio", percentValue(nonAutoingCount, segmentStationCount)); |
| | | factors.put("runBlockRatio", percentValue(runBlockCount, segmentStationCount)); |
| | | return factors; |
| | | } |
| | | |
| | | private double ratio(int numerator, int denominator) { |
| | | if (numerator <= 0 || denominator <= 0) { |
| | | return 0.0d; |
| | | } |
| | | return Math.min(1.0d, (double) numerator / (double) denominator); |
| | | } |
| | | |
| | | private Integer percentValue(int numerator, int denominator) { |
| | | return clampPercent((int) Math.round(ratio(numerator, denominator) * 100.0d)); |
| | | } |
| | | |
| | | private int clampPercent(int value) { |
| | | if (value < 0) { |
| | | return 0; |
| | | } |
| | | return Math.min(value, 100); |
| | | } |
| | | |
| | | private String snapshotConfidence(int analyzedTaskCount, int pathErrorCount) { |
| | | if (analyzedTaskCount >= 10 && pathErrorCount == 0) { |
| | | return CONFIDENCE_HIGH; |
| | | } |
| | | if (analyzedTaskCount >= 3 && pathErrorCount < analyzedTaskCount) { |
| | | return CONFIDENCE_MEDIUM; |
| | | } |
| | | return CONFIDENCE_LOW; |
| | | } |
| | | |
| | | private String targetConfidence(int routeTaskCount) { |
| | | if (routeTaskCount >= 10) { |
| | | return CONFIDENCE_HIGH; |
| | | } |
| | | if (routeTaskCount >= 3) { |
| | | return CONFIDENCE_MEDIUM; |
| | | } |
| | | return CONFIDENCE_LOW; |
| | | } |
| | | |
| | | private static class EstimatedPathResult { |
| | | private final List<Integer> pathStationIds; |
| | | private final String pathError; |
| | | |
| | | private EstimatedPathResult(List<Integer> pathStationIds, String pathError) { |
| | | this.pathStationIds = pathStationIds; |
| | | this.pathError = pathError; |
| | | } |
| | | |
| | | private static EstimatedPathResult success(List<Integer> pathStationIds) { |
| | | return new EstimatedPathResult(new ArrayList<>(pathStationIds), null); |
| | | } |
| | | |
| | | private static EstimatedPathResult error(String pathError) { |
| | | return new EstimatedPathResult(Collections.emptyList(), pathError); |
| | | } |
| | | } |
| | | |
| | | private class SegmentAggregate { |
| | | private final String segmentKey; |
| | | private final List<Integer> stationIds; |
| | | private final Set<Integer> relatedTargetStations = new LinkedHashSet<>(); |
| | | private final Set<Integer> sampleWrkNos = new LinkedHashSet<>(); |
| | | private int passTaskCount; |
| | | |
| | | private SegmentAggregate(String segmentKey, List<Integer> stationIds) { |
| | | this.segmentKey = segmentKey; |
| | | this.stationIds = new ArrayList<>(stationIds); |
| | | } |
| | | |
| | | private void addRouteSample(AutoTuneTaskRouteSampleItem routeSample) { |
| | | passTaskCount++; |
| | | if (routeSample.getTargetStaNo() != null) { |
| | | relatedTargetStations.add(routeSample.getTargetStaNo()); |
| | | } |
| | | if (routeSample.getWrkNo() != null) { |
| | | sampleWrkNos.add(routeSample.getWrkNo()); |
| | | } |
| | | } |
| | | |
| | | private AutoTuneHotPathSegmentItem toItem(Map<Integer, AutoTuneStationRuntimeItem> runtimeMap, |
| | | AutoTuneRoutePressureRuleSnapshot ruleSnapshot, |
| | | int validRouteSampleCount) { |
| | | int loadingCount = 0; |
| | | int taskHoldingCount = 0; |
| | | int runBlockCount = 0; |
| | | int nonAutoingCount = 0; |
| | | for (Integer stationId : stationIds) { |
| | | AutoTuneStationRuntimeItem runtimeItem = runtimeMap.get(stationId); |
| | | if (runtimeItem == null) { |
| | | continue; |
| | | } |
| | | if (Objects.equals(runtimeItem.getLoading(), 1)) { |
| | | loadingCount++; |
| | | } |
| | | if (runtimeItem.getTaskNo() != null && runtimeItem.getTaskNo() > 0) { |
| | | taskHoldingCount++; |
| | | } |
| | | if (Objects.equals(runtimeItem.getRunBlock(), 1)) { |
| | | runBlockCount++; |
| | | } |
| | | if (!Objects.equals(runtimeItem.getAutoing(), 1)) { |
| | | nonAutoingCount++; |
| | | } |
| | | } |
| | | |
| | | int pressureScore = calculateSegmentPressureScore( |
| | | passTaskCount, |
| | | taskHoldingCount, |
| | | loadingCount, |
| | | runBlockCount, |
| | | nonAutoingCount, |
| | | stationIds.size(), |
| | | validRouteSampleCount, |
| | | ruleSnapshot |
| | | ); |
| | | AutoTuneHotPathSegmentItem item = new AutoTuneHotPathSegmentItem(); |
| | | item.setSegmentKey(segmentKey); |
| | | item.setStationIds(new ArrayList<>(stationIds)); |
| | | item.setPassTaskCount(passTaskCount); |
| | | item.setLoadingCount(loadingCount); |
| | | item.setTaskHoldingCount(taskHoldingCount); |
| | | item.setRunBlockCount(runBlockCount); |
| | | item.setNonAutoingCount(nonAutoingCount); |
| | | item.setPressureScore(pressureScore); |
| | | item.setPressureLevel(calculatePressureLevel(pressureScore, ruleSnapshot)); |
| | | item.setPressureFactors(segmentPressureFactors( |
| | | passTaskCount, |
| | | taskHoldingCount, |
| | | loadingCount, |
| | | runBlockCount, |
| | | nonAutoingCount, |
| | | stationIds.size(), |
| | | validRouteSampleCount |
| | | )); |
| | | item.setRelatedTargetStations(new ArrayList<>(relatedTargetStations)); |
| | | item.setSampleWrkNos(new ArrayList<>(sampleWrkNos)); |
| | | return item; |
| | | } |
| | | } |
| | | |
| | | private class TargetPressureAggregate { |
| | | private final Integer targetStationId; |
| | | private final List<AutoTuneHotPathSegmentItem> relatedHotSegments = new ArrayList<>(); |
| | | private int blockedTaskCount; |
| | | private int routeTaskCount; |
| | | |
| | | private TargetPressureAggregate(Integer targetStationId) { |
| | | this.targetStationId = targetStationId; |
| | | } |
| | | |
| | | private AutoTuneTargetStationRoutePressureItem toItem(AutoTuneRoutePressureRuleSnapshot ruleSnapshot) { |
| | | int pressureScore = targetPressureScore(ruleSnapshot); |
| | | String pressureLevel = calculatePressureLevel(pressureScore, ruleSnapshot); |
| | | String heuristicDirection = heuristicDirection(pressureLevel); |
| | | String evidenceText = evidenceText(pressureLevel, pressureScore, heuristicDirection); |
| | | AutoTuneTargetStationRoutePressureItem item = new AutoTuneTargetStationRoutePressureItem(); |
| | | item.setTargetStationId(targetStationId); |
| | | item.setBlockedTaskCount(blockedTaskCount); |
| | | item.setRouteTaskCount(routeTaskCount); |
| | | item.setMainHotSegments(mainHotSegmentKeys()); |
| | | item.setPressureLevel(pressureLevel); |
| | | item.setPressureScore(pressureScore); |
| | | item.setConfidence(targetConfidence(routeTaskCount)); |
| | | item.setPressureFactors(targetPressureFactors(ruleSnapshot)); |
| | | item.setHeuristicDirection(heuristicDirection); |
| | | item.setRecommendedDirection(heuristicDirection); |
| | | item.setRecommendedTargets(Collections.singletonList("station/" + targetStationId + "/outTaskLimit")); |
| | | item.setReason(evidenceText); |
| | | item.setEvidenceText(evidenceText); |
| | | return item; |
| | | } |
| | | |
| | | private int targetPressureScore(AutoTuneRoutePressureRuleSnapshot ruleSnapshot) { |
| | | int pressureScore = highestSegmentPressureScore(); |
| | | int blockedScore = (int) Math.round( |
| | | ratio(blockedTaskCount, routeTaskCount) * nullSafe(ruleSnapshot.getBlockedWeightPercent()) |
| | | ); |
| | | return clampPercent(pressureScore + blockedScore); |
| | | } |
| | | |
| | | private int highestSegmentPressureScore() { |
| | | int pressureScore = 0; |
| | | for (AutoTuneHotPathSegmentItem hotPathSegment : relatedHotSegments) { |
| | | pressureScore = Math.max(pressureScore, nullSafe(hotPathSegment.getPressureScore())); |
| | | } |
| | | return pressureScore; |
| | | } |
| | | |
| | | private Map<String, Object> targetPressureFactors(AutoTuneRoutePressureRuleSnapshot ruleSnapshot) { |
| | | Map<String, Object> factors = new LinkedHashMap<>(); |
| | | factors.put("blockedRatio", percentValue(blockedTaskCount, routeTaskCount)); |
| | | factors.put("highestSegmentScore", highestSegmentPressureScore()); |
| | | factors.put("blockedScore", (int) Math.round( |
| | | ratio(blockedTaskCount, routeTaskCount) * nullSafe(ruleSnapshot.getBlockedWeightPercent()) |
| | | )); |
| | | return factors; |
| | | } |
| | | |
| | | private List<String> mainHotSegmentKeys() { |
| | | List<String> segmentKeys = new ArrayList<>(); |
| | | for (AutoTuneHotPathSegmentItem hotPathSegment : relatedHotSegments) { |
| | | if (hotPathSegment.getSegmentKey() != null) { |
| | | segmentKeys.add(hotPathSegment.getSegmentKey()); |
| | | } |
| | | if (segmentKeys.size() >= MAX_TARGET_MAIN_SEGMENT_COUNT) { |
| | | break; |
| | | } |
| | | } |
| | | return segmentKeys; |
| | | } |
| | | |
| | | private String heuristicDirection(String pressureLevel) { |
| | | if (PRESSURE_HIGH.equals(pressureLevel)) { |
| | | return DIRECTION_DECREASE_CANDIDATE; |
| | | } |
| | | if (blockedTaskCount > 0) { |
| | | return DIRECTION_INCREASE_CANDIDATE; |
| | | } |
| | | return DIRECTION_OBSERVE; |
| | | } |
| | | |
| | | private String evidenceText(String pressureLevel, int pressureScore, String heuristicDirection) { |
| | | return "目标站" + targetStationId |
| | | + "路径事实:pressure=" + pressureLevel |
| | | + ",score=" + pressureScore |
| | | + ",heuristicDirection=" + heuristicDirection |
| | | + ",blockedTaskCount=" + blockedTaskCount |
| | | + ",routeTaskCount=" + routeTaskCount |
| | | + ",mainHotSegments=" + mainHotSegmentKeys() |
| | | + "。"; |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.zy.ai.utils; |
| | | |
| | | import com.zy.ai.domain.autotune.AutoTuneControlModeSnapshot; |
| | | import com.zy.ai.enums.AiPromptBlockType; |
| | | import com.zy.ai.enums.AiPromptScene; |
| | | import org.springframework.stereotype.Component; |
| | |
| | | @Component |
| | | public class AiPromptUtils { |
| | | |
| | | private static final String AUTO_TUNE_TOOL_GET_SNAPSHOT = "wcs_local_dispatch_get_auto_tune_snapshot"; |
| | | private static final String AUTO_TUNE_TOOL_GET_RECENT_JOBS = "wcs_local_dispatch_get_recent_auto_tune_jobs"; |
| | | private static final String AUTO_TUNE_TOOL_APPLY_CHANGES = "wcs_local_dispatch_apply_auto_tune_changes"; |
| | | private static final String AUTO_TUNE_TOOL_REVERT_LAST_JOB = "wcs_local_dispatch_revert_last_auto_tune_job"; |
| | | |
| | | private static final String AUTO_TUNE_RULE_SNAPSHOT_INSTRUCTIONS = |
| | | "Step 4 读取调参规则\n" + |
| | | "- 必须读取 snapshot.ruleSnapshot 中的 minValue、maxValue、maxStep、cooldownMinutes、dynamicMaxValue 和 dynamicMaxSource。\n" + |
| | | "- 提交给 wcs_local_dispatch_apply_auto_tune_changes 的每个 change 都必须匹配对应 targetType/targetKey 的规则。\n" + |
| | | "- 所有提交给 " + AUTO_TUNE_TOOL_APPLY_CHANGES + " 的 change 都必须匹配对应 targetType/targetKey 的规则。\n" + |
| | | "- 每个目标参数的新值必须满足对应 minValue、maxValue 或 dynamicMaxValue、maxStep、cooldownMinutes 和规则 note;找不到规则或无法证明动态上限时禁止提交。"; |
| | | |
| | | public static String buildAutoTuneRuntimeGuard(String triggerType, AutoTuneControlModeSnapshot controlMode) { |
| | | return "请执行一次后台 WCS 自动调参。triggerType=" + safeRuntimeValue(triggerType) + "。\n\n" + |
| | | "==================== 运行时强约束 ====================\n" + |
| | | "- 当前执行模式: modeCode=" + modeValue(controlMode == null ? null : controlMode.getModeCode()) |
| | | + ",modeLabel=" + modeValue(controlMode == null ? null : controlMode.getModeLabel()) |
| | | + ",enabled=" + modeValue(controlMode == null ? null : controlMode.getEnabled()) |
| | | + ",analysisOnly=" + modeValue(controlMode == null ? null : controlMode.getAnalysisOnly()) |
| | | + ",allowApply=" + modeValue(controlMode == null ? null : controlMode.getAllowApply()) + "。\n" + |
| | | "- 必须先调用 " + AUTO_TUNE_TOOL_GET_SNAPSHOT + " 获取后端快照,并读取 snapshot.controlModeSnapshot 的 enabled、analysisOnly、allowApply、modeCode、modeLabel。\n" + |
| | | "- 当 snapshot.controlModeSnapshot.analysisOnly=true 或 allowApply=false 时,只允许分析、调用 " + AUTO_TUNE_TOOL_GET_RECENT_JOBS |
| | | + " 和调用 " + AUTO_TUNE_TOOL_APPLY_CHANGES + " dryRun=true 试算;禁止 dryRun=false 正式应用;禁止调用 " |
| | | + AUTO_TUNE_TOOL_REVERT_LAST_JOB + "。\n" + |
| | | "- 如需提交 changes,必须先调用 " + AUTO_TUNE_TOOL_APPLY_CHANGES |
| | | + " dryRun=true;dry-run 返回 success=false、rejectCount>0、changes 为空或 changes[].resultStatus 全部为 no_change 时,不得调用 dryRun=false。\n" + |
| | | "- 只有 analysisOnly=false 且 allowApply=true、dry-run 通过、存在有效变更且返回 dryRunToken 时,才允许通过 " |
| | | + AUTO_TUNE_TOOL_APPLY_CHANGES + " dryRun=false 正式应用;正式应用必须携带该 dryRunToken。\n" + |
| | | "- 没有收到 " + AUTO_TUNE_TOOL_APPLY_CHANGES + " 或 " + AUTO_TUNE_TOOL_REVERT_LAST_JOB |
| | | + " 的工具返回结果时,不得声称已试算、已应用或已回滚;不要输出自由格式 JSON 供外层解析。\n" + |
| | | "- 必须检查 taskSnapshot.stationLimitBlockedTasks 和 taskSnapshot.outboundTaskSamples 中的 systemMsg、wrkSts、batchSeq,判断是否存在被上限挡住的早序出库任务。\n\n" + |
| | | AUTO_TUNE_RULE_SNAPSHOT_INSTRUCTIONS; |
| | | } |
| | | |
| | | public String getDefaultPrompt(String sceneCode) { |
| | | AiPromptScene scene = AiPromptScene.ofCode(sceneCode); |
| | |
| | | "- wcs_local_dispatch_get_auto_tune_snapshot:获取当前调度、设备、站点、容量与可写参数快照\n" + |
| | | "- wcs_local_dispatch_get_recent_auto_tune_jobs:获取近期自动调参任务和变更结果\n" + |
| | | "- wcs_local_dispatch_apply_auto_tune_changes:提交调参变更,必须先 dry-run 再实际应用\n" + |
| | | "- wcs_local_dispatch_revert_last_auto_tune_job:仅在明确需要回滚最近一次调参时使用\n\n" + |
| | | "- wcs_local_dispatch_revert_last_auto_tune_job:仅在运行模式允许且明确需要回滚最近一次调参时使用\n\n" + |
| | | "禁止调用上述列表之外的工具完成调参。禁止输出自由格式 JSON 让外层解析后调参;所有参数读取、试算、应用和回滚都必须通过 MCP 工具完成。\n\n" + |
| | | "实际应用前必须先调用 wcs_local_dispatch_apply_auto_tune_changes 执行 dry-run。只有 dry-run 返回允许应用且没有高风险拒绝原因时,才可以再次调用 wcs_local_dispatch_apply_auto_tune_changes 执行实际应用。"); |
| | | "实际应用前必须先调用 wcs_local_dispatch_apply_auto_tune_changes 执行 dry-run。只有 snapshot.controlModeSnapshot 显示 analysisOnly=false 且 allowApply=true,dry-run 返回允许应用且存在有效变更时,才可以再次调用 wcs_local_dispatch_apply_auto_tune_changes 执行实际应用。"); |
| | | blocks.put(AiPromptBlockType.OUTPUT_CONTRACT, |
| | | "==================== 输出要求 ====================\n\n" + |
| | | "输出必须使用简体中文,并保持审计友好:\n" + |
| | | "1. 快照摘要:说明本轮依据的关键事实\n" + |
| | | "2. 调整计划:列出目标参数、原值、建议值和原因\n" + |
| | | "3. dry-run 结果:说明允许、拒绝或需要人工处理的原因\n" + |
| | | "4. 实际应用结果:只汇总 MCP 工具返回的应用状态\n" + |
| | | "4. 实际应用结果:只汇总 MCP 工具返回的应用状态;仅分析模式必须说明“仅分析/未正式应用参数”\n" + |
| | | "5. 风险与观察点:说明下一轮应重点观察的指标\n\n" + |
| | | "如果没有足够事实支撑调参,输出“不调整”并说明缺少哪些 MCP 快照事实。"); |
| | | blocks.put(AiPromptBlockType.SCENE_PLAYBOOK, |
| | | "==================== 自动调参规则 ====================\n\n" + |
| | | "注意:后台 Agent 每轮用户消息还会附加当前执行模式、dryRunToken、no_change 和工具返回边界的运行时强约束;与本 playbook 存在差异时以运行时强约束为准。\n\n" + |
| | | "Step 1 获取事实\n" + |
| | | "- 先调用 wcs_local_dispatch_get_auto_tune_snapshot 获取后端快照/MCP facts。\n" + |
| | | "- 如需判断近期调参影响,再调用 wcs_local_dispatch_get_recent_auto_tune_jobs。\n" + |
| | | "- 先调用 " + AUTO_TUNE_TOOL_GET_SNAPSHOT + " 获取后端快照/MCP facts。\n" + |
| | | "- 必须读取 snapshot.controlModeSnapshot,确认 enabled、analysisOnly、allowApply、modeCode、modeLabel。\n" + |
| | | "- 如需判断近期调参影响,再调用 " + AUTO_TUNE_TOOL_GET_RECENT_JOBS + "。\n" + |
| | | "- 方向与容量事实必须来自后端快照或 MCP facts,禁止从前端地图推断。\n\n" + |
| | | "Step 1.1 遵守运行模式\n" + |
| | | "- 当 controlModeSnapshot.analysisOnly=true 或 allowApply=false 时,只允许分析、获取快照、查询最近任务、调用 dryRun=true 试算;禁止 dryRun=false 实际应用;禁止 rollback;输出必须说明“仅分析/未正式应用参数”。\n" + |
| | | "- 只有当 controlModeSnapshot.analysisOnly=false 且 allowApply=true,并且 dry-run 通过且存在有效变更时,才允许正式应用。\n\n" + |
| | | "Step 2 分析站点运行态\n" + |
| | | "- 运行时站点分析只能使用 autoing、loading、taskNo。\n" + |
| | | "- 禁止使用 taskWriteIdx 或 taskBufferItems 作为调参依据。\n\n" + |
| | | "Step 2.1 分析出库任务阻塞\n" + |
| | | "- 必须检查 taskSnapshot.stationLimitBlockedTasks 和 taskSnapshot.outboundTaskSamples。\n" + |
| | | "- 如果同一站点/同一批次中,较小 batchSeq 的 NEW_OUTBOUND 任务因 systemMsg 显示“出库任务上限”被挡住,而较大 batchSeq 已进入 STATION_RUN,说明当前上限可能造成出库排队或顺序异常,应优先评估 outTaskLimit、maxOutTask、crnOutBatchRunningLimit 等相关规则允许的上调空间。\n\n" + |
| | | "Step 2.2 分析路径局部压力\n" + |
| | | "- 必须读取 routePressureSnapshot.routePressureRuleSnapshot,理解当前仓库使用的百分比阈值和评分权重。\n" + |
| | | "- 必须读取 routePressureSnapshot.targetStationRoutePressure、hotPathSegments、pressureScore、pressureFactors、confidence 和 evidenceText。\n" + |
| | | "- heuristicDirection/recommendedDirection 只是后端启发式提示,不是最终调参结论;最终是否调参必须由你结合任务阻塞、路径评分、规则步长、动态上限和冷却独立判断。\n" + |
| | | "- 禁止仅凭全局环线空闲、总节点空闲或邻接节点空闲上调参数;上调必须有目标站局部路径压力事实支撑。\n" + |
| | | "- 当 heuristicDirection=increase_candidate 时,仍必须检查 ruleSnapshot、outBufferCapacity、maxStep 和 cooldown,且不得突破动态上限。\n" + |
| | | "- 当 heuristicDirection=decrease_candidate 时,允许把参数下调到低于当前已执行/已占用数量;该动作只限制后续新增任务,不取消已下发任务。\n" + |
| | | "- 当 pressureLevel=high、confidence=low 或 pathErrorCount>0 时,禁止激进上调;路径事实不足时必须说明缺少哪些目标站路径事实。\n\n" + |
| | | "Step 3 限制可写参数\n" + |
| | | "提交给 MCP/apply-service 的 target_key 必须使用以下键名,不得提交原始数据库列名作为 CRN/双工位堆垛机参数键:\n" + |
| | | "- crnOutBatchRunningLimit:对应 sys_config.crnOutBatchRunningLimit\n" + |
| | |
| | | "注意:asr_bas_station.out_buffer_capacity 是人工维护的出库缓存容量,只用于证明 outTaskLimit 可上调上限,Agent 不允许修改该字段;增大 outTaskLimit 时建议值不得超过对应站点 outBufferCapacity。\n\n" + |
| | | AUTO_TUNE_RULE_SNAPSHOT_INSTRUCTIONS + "\n\n" + |
| | | "Step 5 提交变更\n" + |
| | | "- 先通过 wcs_local_dispatch_apply_auto_tune_changes 执行 dry-run。\n" + |
| | | "- dry-run 通过后才允许通过同一工具实际应用。\n" + |
| | | "- 先通过 " + AUTO_TUNE_TOOL_APPLY_CHANGES + " 执行 dry-run。\n" + |
| | | "- dry-run 返回 success=false、rejectCount>0、changes 为空或全部 resultStatus=no_change 时,必须停止实际应用。\n" + |
| | | "- 只有 controlModeSnapshot.analysisOnly=false 且 allowApply=true,dry-run 通过、存在有效变更并返回 dryRunToken 时,才允许通过同一工具携带 dryRunToken 实际应用。\n" + |
| | | "- 如果工具返回拒绝、冷却中、存在活动任务风险或参数不在白名单内,必须停止实际应用。\n\n" + |
| | | "Step 6 回滚边界\n" + |
| | | "- 只有当最近一次自动调参被 MCP facts 明确证明造成异常,才允许调用 wcs_local_dispatch_revert_last_auto_tune_job。\n" + |
| | | "- analysisOnly=true 或 allowApply=false 时禁止 rollback。\n" + |
| | | "- 只有当允许正式应用,且最近一次自动调参被 MCP facts 明确证明造成异常,才允许调用 " + AUTO_TUNE_TOOL_REVERT_LAST_JOB + "。\n" + |
| | | "- 不得臆测回滚原因。"); |
| | | return blocks; |
| | | } |
| | |
| | | private String localTool(String name) { |
| | | return "wcs_local_" + name; |
| | | } |
| | | |
| | | private static String safeRuntimeValue(String value) { |
| | | if (value == null || value.trim().isEmpty()) { |
| | | return "unknown"; |
| | | } |
| | | return value.trim(); |
| | | } |
| | | |
| | | private static String modeValue(Object value) { |
| | | return value == null ? "unknown" : String.valueOf(value); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.ai.utils; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.zy.ai.entity.AiAutoTuneChange; |
| | | import com.zy.ai.entity.AiAutoTuneJob; |
| | | import com.zy.ai.entity.AiAutoTuneMcpCall; |
| | | |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | |
| | | public final class AutoTuneWriteBehaviorUtils { |
| | | |
| | | public static final String WRITE_ANALYSIS_ONLY = "analysis_only"; |
| | | public static final String WRITE_DRY_RUN = "dry_run"; |
| | | public static final String WRITE_APPLY = "apply"; |
| | | public static final String WRITE_ROLLBACK = "rollback"; |
| | | public static final String WRITE_NO_CHANGE = "no_change"; |
| | | public static final String WRITE_READ_ONLY = "read_only"; |
| | | public static final String WRITE_FAILED = "failed"; |
| | | public static final String WRITE_REJECTED = "rejected"; |
| | | public static final String WRITE_UNKNOWN = "unknown"; |
| | | |
| | | private AutoTuneWriteBehaviorUtils() { |
| | | } |
| | | |
| | | public static void addWriteBehavior(Map<String, Object> item, String writeBehavior) { |
| | | String safeWriteBehavior = isBlank(writeBehavior) ? WRITE_UNKNOWN : writeBehavior; |
| | | item.put("writeBehavior", safeWriteBehavior); |
| | | item.put("writeBehaviorLabel", writeBehaviorLabel(safeWriteBehavior)); |
| | | } |
| | | |
| | | public static String resolveJobWriteBehavior(AiAutoTuneJob job, |
| | | List<Map<String, Object>> mcpCalls, |
| | | List<Map<String, Object>> changes) { |
| | | if (containsWriteBehavior(mcpCalls, WRITE_ANALYSIS_ONLY) |
| | | || containsWriteBehavior(changes, WRITE_ANALYSIS_ONLY) |
| | | || jobSummaryIndicatesAnalysisOnly(job)) { |
| | | return WRITE_ANALYSIS_ONLY; |
| | | } |
| | | if (jobIndicatesSuccessfulRollback(job) |
| | | || containsWriteBehavior(changes, WRITE_ROLLBACK) |
| | | || hasSuccessfulRollbackCall(mcpCalls, changes)) { |
| | | return WRITE_ROLLBACK; |
| | | } |
| | | if (containsWriteBehavior(mcpCalls, WRITE_APPLY) || containsWriteBehavior(changes, WRITE_APPLY)) { |
| | | return WRITE_APPLY; |
| | | } |
| | | if (jobIndicatesStatus(job, WRITE_FAILED) |
| | | || containsWriteBehavior(mcpCalls, WRITE_FAILED) |
| | | || containsWriteBehavior(changes, WRITE_FAILED)) { |
| | | return WRITE_FAILED; |
| | | } |
| | | if (jobIndicatesStatus(job, WRITE_REJECTED) |
| | | || containsWriteBehavior(mcpCalls, WRITE_REJECTED) |
| | | || containsWriteBehavior(changes, WRITE_REJECTED)) { |
| | | return WRITE_REJECTED; |
| | | } |
| | | if (onlyReadOnlyCalls(mcpCalls, changes)) { |
| | | return WRITE_READ_ONLY; |
| | | } |
| | | if (jobIndicatesNoChange(job, mcpCalls, changes)) { |
| | | return WRITE_NO_CHANGE; |
| | | } |
| | | if (containsWriteBehavior(mcpCalls, WRITE_DRY_RUN) || containsWriteBehavior(changes, WRITE_DRY_RUN)) { |
| | | return WRITE_DRY_RUN; |
| | | } |
| | | return WRITE_UNKNOWN; |
| | | } |
| | | |
| | | public static String resolveMcpWriteBehavior(AiAutoTuneMcpCall mcpCall) { |
| | | if (mcpCall == null) { |
| | | return WRITE_UNKNOWN; |
| | | } |
| | | String toolName = normalizeLower(mcpCall.getToolName()); |
| | | if (isRollbackTool(toolName)) { |
| | | return resolveRollbackMcpWriteBehavior(mcpCall); |
| | | } |
| | | String failedBehavior = failedOrRejectedBehavior(mcpCall.getStatus()); |
| | | if (isApplyTool(toolName)) { |
| | | Boolean dryRun = toBoolean(mcpCall.getDryRun()); |
| | | if (Boolean.TRUE.equals(dryRun)) { |
| | | JSONObject response = parseJsonObject(mcpCall.getResponseJson()); |
| | | if (responseIndicatesAnalysisOnly(response) || textIndicatesAnalysisOnly(mcpCall.getErrorMessage())) { |
| | | return WRITE_ANALYSIS_ONLY; |
| | | } |
| | | return failedBehavior == null ? WRITE_DRY_RUN : failedBehavior; |
| | | } |
| | | if (!Boolean.FALSE.equals(dryRun)) { |
| | | return failedBehavior == null ? WRITE_UNKNOWN : failedBehavior; |
| | | } |
| | | return resolveRealApplyMcpWriteBehavior(mcpCall); |
| | | } |
| | | if (isReadOnlyTool(toolName)) { |
| | | return failedBehavior == null ? WRITE_READ_ONLY : failedBehavior; |
| | | } |
| | | return WRITE_UNKNOWN; |
| | | } |
| | | |
| | | public static String resolveChangeWriteBehavior(AiAutoTuneChange change) { |
| | | return resolveChangeWriteBehavior(change, null); |
| | | } |
| | | |
| | | public static String resolveChangeWriteBehavior(AiAutoTuneChange change, String ownerTriggerType) { |
| | | if (change == null) { |
| | | return WRITE_UNKNOWN; |
| | | } |
| | | String resultStatus = normalizeLower(change.getResultStatus()); |
| | | if (WRITE_DRY_RUN.equals(resultStatus)) { |
| | | return WRITE_DRY_RUN; |
| | | } |
| | | if ("success".equals(resultStatus)) { |
| | | return isRollbackOwnerTriggerType(ownerTriggerType) ? WRITE_ROLLBACK : WRITE_APPLY; |
| | | } |
| | | if (WRITE_NO_CHANGE.equals(resultStatus)) { |
| | | return WRITE_NO_CHANGE; |
| | | } |
| | | if ("rejected".equals(resultStatus) && textIndicatesAnalysisOnly(change.getRejectReason())) { |
| | | return WRITE_ANALYSIS_ONLY; |
| | | } |
| | | if (WRITE_REJECTED.equals(resultStatus)) { |
| | | return WRITE_REJECTED; |
| | | } |
| | | if (WRITE_FAILED.equals(resultStatus)) { |
| | | return WRITE_FAILED; |
| | | } |
| | | return WRITE_UNKNOWN; |
| | | } |
| | | |
| | | public static String writeBehaviorLabel(String writeBehavior) { |
| | | if (WRITE_ANALYSIS_ONLY.equals(writeBehavior)) { |
| | | return "仅分析"; |
| | | } |
| | | if (WRITE_DRY_RUN.equals(writeBehavior)) { |
| | | return "试算"; |
| | | } |
| | | if (WRITE_APPLY.equals(writeBehavior)) { |
| | | return "正式写入"; |
| | | } |
| | | if (WRITE_ROLLBACK.equals(writeBehavior)) { |
| | | return "回滚"; |
| | | } |
| | | if (WRITE_NO_CHANGE.equals(writeBehavior)) { |
| | | return "无变更"; |
| | | } |
| | | if (WRITE_READ_ONLY.equals(writeBehavior)) { |
| | | return "只读"; |
| | | } |
| | | if (WRITE_FAILED.equals(writeBehavior)) { |
| | | return "失败"; |
| | | } |
| | | if (WRITE_REJECTED.equals(writeBehavior)) { |
| | | return "已拒绝"; |
| | | } |
| | | return "未知"; |
| | | } |
| | | |
| | | private static boolean hasSuccessfulRollbackCall(List<Map<String, Object>> mcpCalls, |
| | | List<Map<String, Object>> changes) { |
| | | if (mcpCalls == null || mcpCalls.isEmpty()) { |
| | | return false; |
| | | } |
| | | for (Map<String, Object> mcpCall : mcpCalls) { |
| | | if (!WRITE_ROLLBACK.equals(textValue(mcpCall.get("writeBehavior")))) { |
| | | continue; |
| | | } |
| | | if (isSuccessStatus(mcpCall.get("status")) && rollbackHasAppliedChange(mcpCall, changes)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private static boolean rollbackHasAppliedChange(Map<String, Object> mcpCall, |
| | | List<Map<String, Object>> changes) { |
| | | JSONObject response = parseJsonObject(textValue(mcpCall.get("responseJson"))); |
| | | if (isSuccessStatus(mcpCall.get("status")) && safeCount(mcpCall.get("successCount")) > 0) { |
| | | return true; |
| | | } |
| | | return responseIndicatesSuccessfulChange(response) || changesContainSuccessfulWrite(changes); |
| | | } |
| | | |
| | | private static boolean containsWriteBehavior(List<Map<String, Object>> items, String writeBehavior) { |
| | | if (items == null || items.isEmpty()) { |
| | | return false; |
| | | } |
| | | for (Map<String, Object> item : items) { |
| | | if (item != null && writeBehavior.equals(textValue(item.get("writeBehavior")))) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private static boolean onlyReadOnlyCalls(List<Map<String, Object>> mcpCalls, |
| | | List<Map<String, Object>> changes) { |
| | | if (mcpCalls == null || mcpCalls.isEmpty() || (changes != null && !changes.isEmpty())) { |
| | | return false; |
| | | } |
| | | for (Map<String, Object> mcpCall : mcpCalls) { |
| | | if (!WRITE_READ_ONLY.equals(textValue(mcpCall.get("writeBehavior")))) { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private static boolean jobIndicatesNoChange(AiAutoTuneJob job, |
| | | List<Map<String, Object>> mcpCalls, |
| | | List<Map<String, Object>> changes) { |
| | | if (job != null && WRITE_NO_CHANGE.equals(normalizeLower(job.getStatus()))) { |
| | | return true; |
| | | } |
| | | if (allChangesAreNoChange(changes)) { |
| | | return true; |
| | | } |
| | | return containsWriteBehavior(mcpCalls, WRITE_NO_CHANGE); |
| | | } |
| | | |
| | | private static boolean jobIndicatesStatus(AiAutoTuneJob job, String status) { |
| | | return job != null && status.equals(normalizeLower(job.getStatus())); |
| | | } |
| | | |
| | | private static boolean jobIndicatesSuccessfulRollback(AiAutoTuneJob job) { |
| | | if (job == null || !isRollbackOwnerTriggerType(job.getTriggerType())) { |
| | | return false; |
| | | } |
| | | return isSuccessStatus(job.getStatus()) && safeCount(job.getSuccessCount()) > 0; |
| | | } |
| | | |
| | | private static boolean allChangesAreNoChange(List<Map<String, Object>> changes) { |
| | | if (changes == null || changes.isEmpty()) { |
| | | return false; |
| | | } |
| | | for (Map<String, Object> change : changes) { |
| | | if (!WRITE_NO_CHANGE.equals(textValue(change.get("writeBehavior")))) { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private static boolean jobSummaryIndicatesAnalysisOnly(AiAutoTuneJob job) { |
| | | if (job == null) { |
| | | return false; |
| | | } |
| | | return textIndicatesAnalysisOnly(job.getSummary()) || textIndicatesAnalysisOnly(job.getErrorMessage()); |
| | | } |
| | | |
| | | private static String resolveRealApplyMcpWriteBehavior(AiAutoTuneMcpCall mcpCall) { |
| | | JSONObject response = parseJsonObject(mcpCall.getResponseJson()); |
| | | if (responseIndicatesAnalysisOnly(response) || textIndicatesAnalysisOnly(mcpCall.getErrorMessage())) { |
| | | return WRITE_ANALYSIS_ONLY; |
| | | } |
| | | if (hasSuccessfulApplyChange(mcpCall, response)) { |
| | | return WRITE_APPLY; |
| | | } |
| | | String failedBehavior = failedOrRejectedBehavior(mcpCall.getStatus()); |
| | | if (failedBehavior != null) { |
| | | return failedBehavior; |
| | | } |
| | | if (responseIndicatesNoChange(response)) { |
| | | return WRITE_NO_CHANGE; |
| | | } |
| | | if (!isSuccessStatus(mcpCall.getStatus())) { |
| | | return WRITE_UNKNOWN; |
| | | } |
| | | return WRITE_UNKNOWN; |
| | | } |
| | | |
| | | private static String resolveRollbackMcpWriteBehavior(AiAutoTuneMcpCall mcpCall) { |
| | | JSONObject response = parseJsonObject(mcpCall.getResponseJson()); |
| | | if (responseIndicatesAnalysisOnly(response) || textIndicatesAnalysisOnly(mcpCall.getErrorMessage())) { |
| | | return WRITE_ANALYSIS_ONLY; |
| | | } |
| | | if (hasSuccessfulRollbackChange(mcpCall, response)) { |
| | | return WRITE_ROLLBACK; |
| | | } |
| | | String failedBehavior = failedOrRejectedBehavior(mcpCall.getStatus()); |
| | | if (failedBehavior != null) { |
| | | return failedBehavior; |
| | | } |
| | | if (responseIndicatesNoChange(response)) { |
| | | return WRITE_NO_CHANGE; |
| | | } |
| | | return WRITE_UNKNOWN; |
| | | } |
| | | |
| | | private static boolean hasSuccessfulApplyChange(AiAutoTuneMcpCall mcpCall, JSONObject response) { |
| | | if (responseIndicatesSuccessfulChange(response)) { |
| | | return true; |
| | | } |
| | | if (responseHasChangeDetails(response)) { |
| | | return false; |
| | | } |
| | | return isSuccessStatus(mcpCall.getStatus()) && safeCount(mcpCall.getSuccessCount()) > 0; |
| | | } |
| | | |
| | | private static boolean hasSuccessfulRollbackChange(AiAutoTuneMcpCall mcpCall, JSONObject response) { |
| | | if (responseIndicatesSuccessfulChange(response)) { |
| | | return true; |
| | | } |
| | | if (responseHasChangeDetails(response)) { |
| | | return false; |
| | | } |
| | | return isSuccessStatus(mcpCall.getStatus()) && safeCount(mcpCall.getSuccessCount()) > 0; |
| | | } |
| | | |
| | | private static String failedOrRejectedBehavior(Object status) { |
| | | String normalizedStatus = normalizeLower(textValue(status)); |
| | | if (WRITE_FAILED.equals(normalizedStatus)) { |
| | | return WRITE_FAILED; |
| | | } |
| | | if (WRITE_REJECTED.equals(normalizedStatus)) { |
| | | return WRITE_REJECTED; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private static boolean isReadOnlyTool(String toolName) { |
| | | return toolName.contains("get_auto_tune_snapshot") || toolName.contains("get_recent_auto_tune_jobs"); |
| | | } |
| | | |
| | | private static boolean isApplyTool(String toolName) { |
| | | return toolName.contains("apply_auto_tune_changes"); |
| | | } |
| | | |
| | | private static boolean isRollbackTool(String toolName) { |
| | | return toolName.contains("revert_last_auto_tune_job") || toolName.contains("rollback"); |
| | | } |
| | | |
| | | private static boolean responseIndicatesAnalysisOnly(JSONObject response) { |
| | | if (response == null) { |
| | | return false; |
| | | } |
| | | if (isTrue(response.get("analysisOnly")) || textIndicatesAnalysisOnly(textValue(response.get("summary")))) { |
| | | return true; |
| | | } |
| | | return changesContainAnalysisOnly(response.get("changes")); |
| | | } |
| | | |
| | | private static boolean responseIndicatesNoChange(JSONObject response) { |
| | | if (response == null) { |
| | | return false; |
| | | } |
| | | Object changes = response.get("changes"); |
| | | if (hasChangeDetails(changes)) { |
| | | return allResponseChangesHaveStatus(changes, WRITE_NO_CHANGE); |
| | | } |
| | | if (isTrue(response.get("noApply"))) { |
| | | return true; |
| | | } |
| | | return isTrue(response.get("success")) |
| | | && safeCount(response.get("successCount")) == 0 |
| | | && safeCount(response.get("rejectCount")) == 0; |
| | | } |
| | | |
| | | private static boolean responseIndicatesSuccessfulChange(JSONObject response) { |
| | | if (response == null) { |
| | | return false; |
| | | } |
| | | Object changes = response.get("changes"); |
| | | if (hasChangeDetails(changes)) { |
| | | return responseChangesContainStatus(changes, "success"); |
| | | } |
| | | return safeCount(response.get("successCount")) > 0; |
| | | } |
| | | |
| | | private static boolean responseHasChangeDetails(JSONObject response) { |
| | | return response != null && hasChangeDetails(response.get("changes")); |
| | | } |
| | | |
| | | private static boolean hasChangeDetails(Object changes) { |
| | | return changes instanceof List<?> && !((List<?>) changes).isEmpty(); |
| | | } |
| | | |
| | | private static boolean changesContainSuccessfulWrite(List<Map<String, Object>> changes) { |
| | | if (changes == null || changes.isEmpty()) { |
| | | return false; |
| | | } |
| | | for (Map<String, Object> change : changes) { |
| | | if (change == null) { |
| | | continue; |
| | | } |
| | | String writeBehavior = textValue(change.get("writeBehavior")); |
| | | if (WRITE_APPLY.equals(writeBehavior) || WRITE_ROLLBACK.equals(writeBehavior)) { |
| | | return true; |
| | | } |
| | | if ("success".equals(normalizeLower(textValue(change.get("resultStatus"))))) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private static boolean changesContainAnalysisOnly(Object changes) { |
| | | if (!(changes instanceof List<?>)) { |
| | | return false; |
| | | } |
| | | for (Object change : (List<?>) changes) { |
| | | if (!(change instanceof Map<?, ?>)) { |
| | | continue; |
| | | } |
| | | Map<?, ?> changeMap = (Map<?, ?>) change; |
| | | if (textIndicatesAnalysisOnly(textValue(changeMap.get("rejectReason")))) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private static boolean allResponseChangesHaveStatus(Object changes, String status) { |
| | | if (!(changes instanceof List<?>)) { |
| | | return false; |
| | | } |
| | | List<?> changeList = (List<?>) changes; |
| | | if (changeList.isEmpty()) { |
| | | return false; |
| | | } |
| | | for (Object change : changeList) { |
| | | if (!(change instanceof Map<?, ?>)) { |
| | | return false; |
| | | } |
| | | Map<?, ?> changeMap = (Map<?, ?>) change; |
| | | if (!status.equals(normalizeLower(textValue(changeMap.get("resultStatus"))))) { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private static boolean responseChangesContainStatus(Object changes, String status) { |
| | | if (!(changes instanceof List<?>)) { |
| | | return false; |
| | | } |
| | | for (Object change : (List<?>) changes) { |
| | | if (!(change instanceof Map<?, ?>)) { |
| | | continue; |
| | | } |
| | | Map<?, ?> changeMap = (Map<?, ?>) change; |
| | | if (status.equals(normalizeLower(textValue(changeMap.get("resultStatus"))))) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private static JSONObject parseJsonObject(String json) { |
| | | if (isBlank(json)) { |
| | | return null; |
| | | } |
| | | try { |
| | | return JSON.parseObject(json); |
| | | } catch (RuntimeException ignore) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private static boolean textIndicatesAnalysisOnly(String text) { |
| | | if (isBlank(text)) { |
| | | return false; |
| | | } |
| | | String normalizedText = text.toLowerCase(Locale.ROOT); |
| | | return normalizedText.contains("仅分析") |
| | | || textContainsPositiveFlag(normalizedText, "analysisonly") |
| | | || textContainsPositiveFlag(normalizedText, "analysis_only") |
| | | || textContainsPositiveFlag(normalizedText, "noapply") |
| | | || textContainsPositiveFlag(normalizedText, "no_apply") |
| | | || normalizedText.contains("禁止实际应用"); |
| | | } |
| | | |
| | | private static boolean textContainsPositiveFlag(String text, String flag) { |
| | | int matchIndex = text.indexOf(flag); |
| | | while (matchIndex >= 0) { |
| | | int valueStart = skipFlagSeparators(text, matchIndex + flag.length()); |
| | | if (startsWith(text, valueStart, "false") || startsWith(text, valueStart, "0")) { |
| | | matchIndex = text.indexOf(flag, matchIndex + flag.length()); |
| | | continue; |
| | | } |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private static int skipFlagSeparators(String text, int startIndex) { |
| | | int index = startIndex; |
| | | while (index < text.length()) { |
| | | char character = text.charAt(index); |
| | | if (!Character.isWhitespace(character) |
| | | && character != '=' |
| | | && character != ':' |
| | | && character != '"' |
| | | && character != '\'') { |
| | | break; |
| | | } |
| | | index++; |
| | | } |
| | | return index; |
| | | } |
| | | |
| | | private static boolean startsWith(String text, int startIndex, String prefix) { |
| | | return startIndex <= text.length() && text.startsWith(prefix, startIndex); |
| | | } |
| | | |
| | | private static boolean isRollbackOwnerTriggerType(String ownerTriggerType) { |
| | | return WRITE_ROLLBACK.equals(normalizeLower(ownerTriggerType)); |
| | | } |
| | | |
| | | private static Boolean toBoolean(Integer value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return value == 1; |
| | | } |
| | | |
| | | private static boolean isSuccessStatus(Object status) { |
| | | return "success".equals(normalizeLower(textValue(status))); |
| | | } |
| | | |
| | | private static boolean isTrue(Object value) { |
| | | if (value instanceof Boolean) { |
| | | return Boolean.TRUE.equals(value); |
| | | } |
| | | return "true".equals(normalizeLower(textValue(value))) || "1".equals(textValue(value)); |
| | | } |
| | | |
| | | private static int safeCount(Object value) { |
| | | if (value instanceof Number) { |
| | | return ((Number) value).intValue(); |
| | | } |
| | | if (value == null) { |
| | | return 0; |
| | | } |
| | | try { |
| | | return Integer.parseInt(String.valueOf(value)); |
| | | } catch (NumberFormatException ignore) { |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | private static String normalizeLower(String value) { |
| | | return isBlank(value) ? "" : value.trim().toLowerCase(Locale.ROOT); |
| | | } |
| | | |
| | | private static String textValue(Object value) { |
| | | return value == null ? "" : String.valueOf(value); |
| | | } |
| | | |
| | | private static boolean isBlank(String value) { |
| | | return value == null || value.trim().isEmpty(); |
| | | } |
| | | } |
| | |
| | | -- AI自动调参合并增量脚本 |
| | | -- 合并范围:2026-04-27 至 2026-04-28 自动调参相关 SQL 变更。 |
| | | -- 执行说明:在目标库中执行本文件一次即可;脚本按幂等方式编写,已存在对象会跳过或更新。 |
| | | -- 不包含已废弃的 asr_station_flow_capacity 建表脚本;如历史库存在该表,会迁移 OUT 容量到 asr_bas_station.out_buffer_capacity 后删除旧表。 |
| | | -- 合并范围:2026-04-27 至 2026-04-28 自动调参与 OpenAI Responses 路由相关 SQL 变更。 |
| | | -- 执行说明:执行前请自行清理已创建的自动调参表和相关初始化数据;本文件保留自动调参最终结构,并补齐 AI 路由运行必需字段。 |
| | | -- 不包含已废弃的 asr_station_flow_capacity 建表脚本。 |
| | | -- 来源脚本: |
| | | -- - 20260427_create_ai_auto_tune_tables.sql |
| | | -- - 20260427_add_ai_auto_tune_sys_configs.sql |
| | |
| | | -- - 20260427_add_ai_auto_tune_console_menu.sql |
| | | -- - 20260427_update_auto_tune_prompt_out_buffer_capacity.sql |
| | | -- - 20260427_update_auto_tune_prompt_rule_snapshot.sql |
| | | -- - 20260428_extend_sys_llm_route_protocol.sql |
| | | |
| | | -- 1. AI自动调参审计表 |
| | | CREATE TABLE IF NOT EXISTS `sys_ai_auto_tune_job` ( |
| | |
| | | KEY `idx_sys_ai_auto_tune_mcp_create_time` (`create_time`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI自动调参MCP调用明细表'; |
| | | |
| | | ALTER TABLE `sys_ai_auto_tune_job` |
| | | MODIFY COLUMN `summary` MEDIUMTEXT COMMENT '执行摘要', |
| | | MODIFY COLUMN `error_message` MEDIUMTEXT COMMENT '错误信息'; |
| | | |
| | | ALTER TABLE `sys_ai_auto_tune_change` |
| | | MODIFY COLUMN `reject_reason` MEDIUMTEXT COMMENT '拒绝原因'; |
| | | |
| | | -- 2. AI自动调参系统配置 |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI自动调参开关', 'aiAutoTuneEnabled', 'N', 1, 1, 'system' |
| | | SELECT 'AI自动调参开关', 'aiAutoTuneEnabled', 'N', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS ( |
| | | SELECT 1 |
| | |
| | | ); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI自动调参间隔(分钟)', 'aiAutoTuneIntervalMinutes', '10', 1, 1, 'system' |
| | | SELECT 'AI自动调参间隔(分钟)', 'aiAutoTuneIntervalMinutes', '10', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS ( |
| | | SELECT 1 |
| | |
| | | ); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI自动调参Prompt日志保留上限', 'aiAutoTunePromptLogLimit', '500', 1, 1, 'system' |
| | | SELECT 'AI自动调参Prompt日志保留上限', 'aiAutoTunePromptLogLimit', '500', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS ( |
| | | SELECT 1 |
| | |
| | | ); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT '输送站出库任务全局上限', 'conveyorStationTaskLimit', '30', 1, 1, 'system' |
| | | SELECT 'AI自动调参仅分析模式', 'aiAutoTuneAnalysisOnly', 'Y', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS ( |
| | | SELECT 1 |
| | | FROM sys_config |
| | | WHERE code = 'aiAutoTuneAnalysisOnly' |
| | | ); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT '输送站出库任务全局上限', 'conveyorStationTaskLimit', '30', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS ( |
| | | SELECT 1 |
| | | FROM sys_config |
| | | WHERE code = 'conveyorStationTaskLimit' |
| | | ); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI调参路径压力切段窗口', 'aiAutoTuneRoutePressureSegmentWindowSize', '4', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiAutoTuneRoutePressureSegmentWindowSize'); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI调参路径压力中等阈值(%)', 'aiAutoTuneRoutePressureMediumPercent', '50', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiAutoTuneRoutePressureMediumPercent'); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI调参路径压力高阈值(%)', 'aiAutoTuneRoutePressureHighPercent', '75', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiAutoTuneRoutePressureHighPercent'); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI调参路径任务集中度权重(%)', 'aiAutoTuneRoutePressurePassWeightPercent', '35', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiAutoTuneRoutePressurePassWeightPercent'); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI调参路径占用权重(%)', 'aiAutoTuneRoutePressureOccupiedWeightPercent', '25', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiAutoTuneRoutePressureOccupiedWeightPercent'); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI调参目标站阻塞权重(%)', 'aiAutoTuneRoutePressureBlockedWeightPercent', '20', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiAutoTuneRoutePressureBlockedWeightPercent'); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI调参路径非自动权重(%)', 'aiAutoTuneRoutePressureNonAutoingWeightPercent', '10', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiAutoTuneRoutePressureNonAutoingWeightPercent'); |
| | | |
| | | INSERT INTO sys_config(name, code, value, type, status, select_type) |
| | | SELECT 'AI调参路径runBlock权重(%)', 'aiAutoTuneRoutePressureRunBlockWeightPercent', '10', 1, 1, 'Agent' |
| | | FROM dual |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiAutoTuneRoutePressureRunBlockWeightPercent'); |
| | | |
| | | UPDATE sys_config |
| | | SET select_type = 'Agent' |
| | | WHERE code IN ( |
| | | 'aiAutoTuneAnalysisOnly', |
| | | 'aiAutoTuneEnabled', |
| | | 'aiAutoTuneIntervalMinutes', |
| | | 'aiAutoTunePromptLogLimit', |
| | | 'aiAutoTuneRoutePressureBlockedWeightPercent', |
| | | 'aiAutoTuneRoutePressureHighPercent', |
| | | 'aiAutoTuneRoutePressureMediumPercent', |
| | | 'aiAutoTuneRoutePressureNonAutoingWeightPercent', |
| | | 'aiAutoTuneRoutePressureOccupiedWeightPercent', |
| | | 'aiAutoTuneRoutePressurePassWeightPercent', |
| | | 'aiAutoTuneRoutePressureRunBlockWeightPercent', |
| | | 'aiAutoTuneRoutePressureSegmentWindowSize', |
| | | 'conveyorStationTaskLimit', |
| | | 'crnOutBatchRunningLimit' |
| | | ); |
| | | |
| | | -- 3. asr_bas_station 增加出库缓存容量配置,替代 asr_station_flow_capacity |
| | |
| | | PREPARE stmt_out_buffer_capacity FROM @add_out_buffer_capacity_sql; |
| | | EXECUTE stmt_out_buffer_capacity; |
| | | DEALLOCATE PREPARE stmt_out_buffer_capacity; |
| | | |
| | | SET @station_flow_capacity_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.TABLES |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'asr_station_flow_capacity' |
| | | ); |
| | | |
| | | SET @migrate_station_flow_capacity_sql := IF( |
| | | @station_flow_capacity_exists > 0, |
| | | 'UPDATE asr_bas_station station |
| | | JOIN asr_station_flow_capacity capacity |
| | | ON capacity.station_id = station.station_id |
| | | AND capacity.direction_code = ''OUT'' |
| | | AND capacity.buffer_capacity IS NOT NULL |
| | | SET station.out_buffer_capacity = capacity.buffer_capacity |
| | | WHERE station.out_buffer_capacity IS NULL', |
| | | 'SELECT ''table asr_station_flow_capacity not exists'' ' |
| | | ); |
| | | PREPARE stmt_migrate_station_flow_capacity FROM @migrate_station_flow_capacity_sql; |
| | | EXECUTE stmt_migrate_station_flow_capacity; |
| | | DEALLOCATE PREPARE stmt_migrate_station_flow_capacity; |
| | | |
| | | SET @drop_station_flow_capacity_sql := IF( |
| | | @station_flow_capacity_exists > 0, |
| | | 'DROP TABLE asr_station_flow_capacity', |
| | | 'SELECT ''table asr_station_flow_capacity already removed'' ' |
| | | ); |
| | | PREPARE stmt_drop_station_flow_capacity FROM @drop_station_flow_capacity_sql; |
| | | EXECUTE stmt_drop_station_flow_capacity; |
| | | DEALLOCATE PREPARE stmt_drop_station_flow_capacity; |
| | | |
| | | -- 4. AI自动调参控制台菜单 |
| | | -- 执行后请在“角色授权”里给对应角色勾选 AI管理 -> AI自动调参控制台。 |
| | |
| | | status = 1 |
| | | WHERE code = 'ai/auto_tune.html#view' AND level = 3; |
| | | |
| | | -- 5. 历史已发布 Prompt 补丁:outBufferCapacity 约束说明 |
| | | UPDATE sys_ai_prompt_block block |
| | | JOIN sys_ai_prompt_template template ON template.id = block.template_id |
| | | SET block.content = CONCAT( |
| | | block.content, |
| | | '\n注意:asr_bas_station.out_buffer_capacity 是人工维护的出库缓存容量,只用于证明 outTaskLimit 可上调上限,Agent 不允许修改该字段;增大 outTaskLimit 时建议值不得超过对应站点 outBufferCapacity。' |
| | | ) |
| | | WHERE template.scene_code = 'wcs_auto_tune_dispatch' |
| | | AND block.block_type = 'scene_playbook' |
| | | AND block.content NOT LIKE '%out_buffer_capacity 是人工维护的出库缓存容量%'; |
| | | -- 5. OpenAI Responses 兼容路由字段补齐 |
| | | -- 当前运行库可能已经存在旧版 sys_llm_route 表,必须补齐实体和 Mapper 运行所需字段。 |
| | | SET @llm_route_provider_type_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_llm_route' |
| | | AND COLUMN_NAME = 'provider_type' |
| | | ); |
| | | |
| | | -- 6. 历史已发布 Prompt 补丁:ruleSnapshot 规则说明 |
| | | -- 追加文本需与 AiPromptUtils 的默认规则说明保持一致。 |
| | | UPDATE sys_ai_prompt_block block |
| | | JOIN sys_ai_prompt_template template ON template.id = block.template_id |
| | | SET block.content = CONCAT( |
| | | block.content, |
| | | '\nStep 4 读取调参规则\n- 必须读取 snapshot.ruleSnapshot 中的 minValue、maxValue、maxStep、cooldownMinutes、dynamicMaxValue 和 dynamicMaxSource。\n- 提交给 wcs_local_dispatch_apply_auto_tune_changes 的每个 change 都必须匹配对应 targetType/targetKey 的规则。\n- 每个目标参数的新值必须满足对应 minValue、maxValue 或 dynamicMaxValue、maxStep、cooldownMinutes 和规则 note;找不到规则或无法证明动态上限时禁止提交。' |
| | | ) |
| | | WHERE template.scene_code = 'wcs_auto_tune_dispatch' |
| | | AND block.block_type = 'scene_playbook' |
| | | AND block.content NOT LIKE '%snapshot.ruleSnapshot%'; |
| | | SET @add_llm_route_provider_type_sql := IF( |
| | | @llm_route_provider_type_exists = 0, |
| | | 'ALTER TABLE sys_llm_route ADD COLUMN provider_type VARCHAR(64) NOT NULL DEFAULT ''OPENAI_COMPATIBLE'' COMMENT ''提供商类型: OPENAI_COMPATIBLE/OPENAI/AZURE等'' AFTER name', |
| | | 'SELECT ''column provider_type already exists'' ' |
| | | ); |
| | | PREPARE stmt_llm_route_alter FROM @add_llm_route_provider_type_sql; |
| | | EXECUTE stmt_llm_route_alter; |
| | | DEALLOCATE PREPARE stmt_llm_route_alter; |
| | | |
| | | -- 7. 历史已发布 Prompt 补丁:出库任务阻塞明细分析说明 |
| | | UPDATE sys_ai_prompt_block block |
| | | JOIN sys_ai_prompt_template template ON template.id = block.template_id |
| | | SET block.content = CONCAT( |
| | | block.content, |
| | | '\nStep 2.1 分析出库任务阻塞\n- 必须检查 taskSnapshot.stationLimitBlockedTasks 和 taskSnapshot.outboundTaskSamples。\n- 如果同一站点/同一批次中,较小 batchSeq 的 NEW_OUTBOUND 任务因 systemMsg 显示“出库任务上限”被挡住,而较大 batchSeq 已进入 STATION_RUN,说明当前上限可能造成出库排队或顺序异常,应优先评估 outTaskLimit、maxOutTask、crnOutBatchRunningLimit 等相关规则允许的上调空间。' |
| | | ) |
| | | WHERE template.scene_code = 'wcs_auto_tune_dispatch' |
| | | AND block.block_type = 'scene_playbook' |
| | | AND block.content NOT LIKE '%taskSnapshot.stationLimitBlockedTasks%'; |
| | | SET @llm_route_protocol_type_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_llm_route' |
| | | AND COLUMN_NAME = 'protocol_type' |
| | | ); |
| | | |
| | | -- 8. 执行后检查 |
| | | SET @add_llm_route_protocol_type_sql := IF( |
| | | @llm_route_protocol_type_exists = 0, |
| | | 'ALTER TABLE sys_llm_route ADD COLUMN protocol_type VARCHAR(64) NOT NULL DEFAULT ''OPENAI_CHAT_COMPLETIONS'' COMMENT ''接口协议: OPENAI_CHAT_COMPLETIONS/OPENAI_RESPONSES'' AFTER provider_type', |
| | | 'SELECT ''column protocol_type already exists'' ' |
| | | ); |
| | | PREPARE stmt_llm_route_alter FROM @add_llm_route_protocol_type_sql; |
| | | EXECUTE stmt_llm_route_alter; |
| | | DEALLOCATE PREPARE stmt_llm_route_alter; |
| | | |
| | | SET @llm_route_endpoint_path_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_llm_route' |
| | | AND COLUMN_NAME = 'endpoint_path' |
| | | ); |
| | | |
| | | SET @add_llm_route_endpoint_path_sql := IF( |
| | | @llm_route_endpoint_path_exists = 0, |
| | | 'ALTER TABLE sys_llm_route ADD COLUMN endpoint_path VARCHAR(255) DEFAULT NULL COMMENT ''接口端点路径,如 /v1/chat/completions 或 /v1/responses'' AFTER base_url', |
| | | 'SELECT ''column endpoint_path already exists'' ' |
| | | ); |
| | | PREPARE stmt_llm_route_alter FROM @add_llm_route_endpoint_path_sql; |
| | | EXECUTE stmt_llm_route_alter; |
| | | DEALLOCATE PREPARE stmt_llm_route_alter; |
| | | |
| | | SET @llm_route_auth_type_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_llm_route' |
| | | AND COLUMN_NAME = 'auth_type' |
| | | ); |
| | | |
| | | SET @add_llm_route_auth_type_sql := IF( |
| | | @llm_route_auth_type_exists = 0, |
| | | 'ALTER TABLE sys_llm_route ADD COLUMN auth_type VARCHAR(32) NOT NULL DEFAULT ''BEARER'' COMMENT ''认证方式: BEARER/API_KEY_HEADER/NONE'' AFTER api_key', |
| | | 'SELECT ''column auth_type already exists'' ' |
| | | ); |
| | | PREPARE stmt_llm_route_alter FROM @add_llm_route_auth_type_sql; |
| | | EXECUTE stmt_llm_route_alter; |
| | | DEALLOCATE PREPARE stmt_llm_route_alter; |
| | | |
| | | SET @llm_route_auth_header_name_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_llm_route' |
| | | AND COLUMN_NAME = 'auth_header_name' |
| | | ); |
| | | |
| | | SET @add_llm_route_auth_header_name_sql := IF( |
| | | @llm_route_auth_header_name_exists = 0, |
| | | 'ALTER TABLE sys_llm_route ADD COLUMN auth_header_name VARCHAR(128) DEFAULT NULL COMMENT ''API_KEY_HEADER 模式下的请求头名称'' AFTER auth_type', |
| | | 'SELECT ''column auth_header_name already exists'' ' |
| | | ); |
| | | PREPARE stmt_llm_route_alter FROM @add_llm_route_auth_header_name_sql; |
| | | EXECUTE stmt_llm_route_alter; |
| | | DEALLOCATE PREPARE stmt_llm_route_alter; |
| | | |
| | | SET @llm_route_capabilities_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_llm_route' |
| | | AND COLUMN_NAME = 'capabilities' |
| | | ); |
| | | |
| | | SET @add_llm_route_capabilities_sql := IF( |
| | | @llm_route_capabilities_exists = 0, |
| | | 'ALTER TABLE sys_llm_route ADD COLUMN capabilities VARCHAR(255) DEFAULT NULL COMMENT ''模型能力标记,逗号分隔,如 tools,reasoning'' AFTER model', |
| | | 'SELECT ''column capabilities already exists'' ' |
| | | ); |
| | | PREPARE stmt_llm_route_alter FROM @add_llm_route_capabilities_sql; |
| | | EXECUTE stmt_llm_route_alter; |
| | | DEALLOCATE PREPARE stmt_llm_route_alter; |
| | | |
| | | SET @llm_route_request_options_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = @current_db |
| | | AND TABLE_NAME = 'sys_llm_route' |
| | | AND COLUMN_NAME = 'request_options' |
| | | ); |
| | | |
| | | SET @add_llm_route_request_options_sql := IF( |
| | | @llm_route_request_options_exists = 0, |
| | | 'ALTER TABLE sys_llm_route ADD COLUMN request_options TEXT DEFAULT NULL COMMENT ''接口请求扩展参数JSON'' AFTER capabilities', |
| | | 'SELECT ''column request_options already exists'' ' |
| | | ); |
| | | PREPARE stmt_llm_route_alter FROM @add_llm_route_request_options_sql; |
| | | EXECUTE stmt_llm_route_alter; |
| | | DEALLOCATE PREPARE stmt_llm_route_alter; |
| | | |
| | | -- 6. 说明 |
| | | -- 自动调参 Prompt 最终态由 AiPromptTemplateInitializer 在缺失时按 AiPromptUtils 默认分段初始化, |
| | | -- clean 数据库无需 SQL 历史兼容 UPDATE/REPLACE。 |
| | | |
| | | -- 7. 执行后检查 |
| | | SELECT id, name, code, value, type, status, select_type |
| | | FROM sys_config |
| | | WHERE code IN ( |
| | | 'aiAutoTuneAnalysisOnly', |
| | | 'aiAutoTuneEnabled', |
| | | 'aiAutoTuneIntervalMinutes', |
| | | 'aiAutoTunePromptLogLimit', |
| | | 'conveyorStationTaskLimit' |
| | | 'aiAutoTuneRoutePressureBlockedWeightPercent', |
| | | 'aiAutoTuneRoutePressureHighPercent', |
| | | 'aiAutoTuneRoutePressureMediumPercent', |
| | | 'aiAutoTuneRoutePressureNonAutoingWeightPercent', |
| | | 'aiAutoTuneRoutePressureOccupiedWeightPercent', |
| | | 'aiAutoTuneRoutePressurePassWeightPercent', |
| | | 'aiAutoTuneRoutePressureRunBlockWeightPercent', |
| | | 'aiAutoTuneRoutePressureSegmentWindowSize', |
| | | 'conveyorStationTaskLimit', |
| | | 'crnOutBatchRunningLimit' |
| | | ) |
| | | ORDER BY code; |
| | | |
| | | SHOW COLUMNS FROM asr_bas_station LIKE 'out_buffer_capacity'; |
| | | |
| | | SHOW COLUMNS FROM sys_llm_route |
| | | WHERE Field IN ( |
| | | 'provider_type', |
| | | 'protocol_type', |
| | | 'endpoint_path', |
| | | 'auth_type', |
| | | 'auth_header_name', |
| | | 'capabilities', |
| | | 'request_options' |
| | | ); |
| | | |
| | | SELECT id, code, name, resource_id, level, sort, status |
| | | FROM sys_resource |
| | | WHERE code IN ( |
| | |
| | | .summary-grid { |
| | | margin-top: 12px; |
| | | display: grid; |
| | | grid-template-columns: repeat(6, minmax(0, 1fr)); |
| | | grid-template-columns: repeat(7, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | } |
| | | .summary-card { |
| | |
| | | .layout { |
| | | margin-top: 12px; |
| | | display: grid; |
| | | grid-template-columns: minmax(360px, 0.9fr) minmax(520px, 1.4fr); |
| | | grid-template-columns: minmax(460px, 1.05fr) minmax(560px, 1.25fr); |
| | | gap: 12px; |
| | | } |
| | | .panel { |
| | |
| | | font-size: 22px; |
| | | font-weight: 720; |
| | | } |
| | | .route-pressure-metrics { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | gap: 8px; |
| | | margin-bottom: 12px; |
| | | } |
| | | .route-pressure-section + .route-pressure-section { |
| | | margin-top: 12px; |
| | | } |
| | | .route-pressure-section-title { |
| | | color: #61748a; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | margin-bottom: 8px; |
| | | } |
| | | .map-list { |
| | | margin-top: 10px; |
| | | display: grid; |
| | |
| | | color: #8b9ab0; |
| | | font-weight: 500; |
| | | } |
| | | .map-title-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | .pill-row { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | max-height: 120px; |
| | | overflow: auto; |
| | | } |
| | | .pill-row-expanded { |
| | | max-height: 168px; |
| | | } |
| | | .kv-pill { |
| | | border-radius: 999px; |
| | |
| | | .kv-pill .code-name { |
| | | color: #7d8da2; |
| | | margin-left: 3px; |
| | | } |
| | | .insight-stack { |
| | | display: grid; |
| | | gap: 10px; |
| | | margin-top: 10px; |
| | | } |
| | | .wide-map-box { |
| | | min-height: auto; |
| | | } |
| | | .distribution-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | } |
| | | .distribution-section { |
| | | min-width: 0; |
| | | border-radius: 10px; |
| | | border: 1px solid #edf2f7; |
| | | background: #fff; |
| | | padding: 8px; |
| | | } |
| | | .distribution-label { |
| | | margin-bottom: 6px; |
| | | color: #7a8aa0; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | } |
| | | .rule-table-wrap { |
| | | max-height: 260px; |
| | | overflow: auto; |
| | | border: 1px solid #edf2f7; |
| | | border-radius: 10px; |
| | | } |
| | | .rule-table { |
| | | width: 100%; |
| | | border-collapse: separate; |
| | | border-spacing: 0; |
| | | font-size: 12px; |
| | | color: #42566f; |
| | | } |
| | | .rule-table th { |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: 1; |
| | | background: #f7f9fc; |
| | | color: #2e3a4d; |
| | | font-weight: 700; |
| | | text-align: left; |
| | | } |
| | | .rule-table th, |
| | | .rule-table td { |
| | | padding: 8px 10px; |
| | | border-bottom: 1px solid #edf2f7; |
| | | vertical-align: top; |
| | | white-space: nowrap; |
| | | } |
| | | .rule-table tr:last-child td { |
| | | border-bottom: none; |
| | | } |
| | | .rule-table .rule-target-cell { |
| | | min-width: 170px; |
| | | white-space: normal; |
| | | } |
| | | .rule-table .rule-dynamic-cell { |
| | | min-width: 150px; |
| | | white-space: normal; |
| | | } |
| | | .rule-table .code-name { |
| | | display: block; |
| | | margin-top: 2px; |
| | | color: #8b9ab0; |
| | | font-size: 11px; |
| | | word-break: break-all; |
| | | } |
| | | .split-grid { |
| | | display: grid; |
| | |
| | | .job-expand-actions .job-expand-all { |
| | | font-weight: 600; |
| | | } |
| | | .job-expand-all-row { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin: 8px 0 10px; |
| | | } |
| | | .json-dialog-body { |
| | | margin: -8px -6px 0; |
| | | } |
| | |
| | | .layout { grid-template-columns: 1fr; } |
| | | } |
| | | @media (max-width: 760px) { |
| | | .summary-grid, .param-grid, .map-list, .split-grid { grid-template-columns: 1fr; } |
| | | .summary-grid, .param-grid, .route-pressure-metrics, .map-list, .distribution-grid, .split-grid { grid-template-columns: 1fr; } |
| | | .hero-actions { justify-content: flex-start; } |
| | | } |
| | | </style> |
| | |
| | | <div v-html="headerIcon" style="display:flex;"></div> |
| | | <div> |
| | | <div class="main">AI自动调参控制台</div> |
| | | <div class="sub">手动触发 Agent、查看实时快照、审计调参动作和回滚最近成功调参</div> |
| | | <div class="sub">{{ controlModeHeroText }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="hero-actions"> |
| | | <el-button type="primary" size="mini" :loading="snapshotLoading" @click="refreshAll">刷新数据</el-button> |
| | | <el-button type="success" size="mini" :loading="agentLoading" @click="triggerManual">手动触发Agent</el-button> |
| | | <el-button type="success" size="mini" :loading="agentLoading" @click="triggerManual">{{ manualTriggerButtonText }}</el-button> |
| | | <el-button size="mini" :loading="schedulerLoading" @click="triggerScheduler">按后台规则触发</el-button> |
| | | <el-button type="danger" plain size="mini" :loading="rollbackLoading" @click="confirmRollback">回滚最近成功调参</el-button> |
| | | <el-button type="danger" plain size="mini" :loading="rollbackLoading" |
| | | :disabled="isAnalysisOnlyMode" |
| | | :title="isAnalysisOnlyMode ? '仅分析模式禁止回滚' : ''" |
| | | @click="confirmRollback">回滚最近成功调参</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="summary-grid"> |
| | | <div class="summary-card"> |
| | | <div class="k">运行模式</div> |
| | | <div class="v">{{ controlModeDisplay.label }}</div> |
| | | <div class="hint">{{ controlModeDisplay.hint }}</div> |
| | | </div> |
| | | <div class="summary-card"> |
| | | <div class="k">活动任务</div> |
| | | <div class="v">{{ taskSnapshot.activeTaskCount || 0 }}</div> |
| | |
| | | <span class="small-muted" v-if="combineTaskLimitEntries(parameterSnapshot.dualCrnMaxOutTask, parameterSnapshot.dualCrnMaxInTask).length === 0">暂无数据</span> |
| | | </div> |
| | | </div> |
| | | <div class="map-box"> |
| | | </div> |
| | | <div class="insight-stack"> |
| | | <div class="map-box wide-map-box"> |
| | | <div class="map-title">任务分布</div> |
| | | <div class="pill-row"> |
| | | <span class="kv-pill" v-for="item in mapEntries(taskSnapshot.byTargetStation)" :key="'t_' + item.key">站点{{ item.key }}: {{ item.value }}</span> |
| | | <span class="kv-pill" v-for="item in mapEntries(taskSnapshot.byBatch)" :key="'b_' + item.key">批次{{ item.key }}: {{ item.value }}</span> |
| | | <span class="small-muted" v-if="mapEntries(taskSnapshot.byTargetStation).length === 0 && mapEntries(taskSnapshot.byBatch).length === 0">暂无活动任务</span> |
| | | <div class="distribution-grid"> |
| | | <div class="distribution-section"> |
| | | <div class="distribution-label">按目标站点</div> |
| | | <div class="pill-row pill-row-expanded"> |
| | | <span class="kv-pill" v-for="item in mapEntries(taskSnapshot.byTargetStation)" :key="'t_' + item.key">站点{{ item.key }}: {{ item.value }}</span> |
| | | <span class="small-muted" v-if="mapEntries(taskSnapshot.byTargetStation).length === 0">暂无数据</span> |
| | | </div> |
| | | </div> |
| | | <div class="distribution-section"> |
| | | <div class="distribution-label">按任务批次</div> |
| | | <div class="pill-row pill-row-expanded"> |
| | | <span class="kv-pill" v-for="item in mapEntries(taskSnapshot.byBatch)" :key="'b_' + item.key">批次{{ item.key }}: {{ item.value }}</span> |
| | | <span class="small-muted" v-if="mapEntries(taskSnapshot.byBatch).length === 0">暂无数据</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="map-box"> |
| | | <div class="map-title"> |
| | | <div class="map-box wide-map-box"> |
| | | <div class="map-title map-title-row"> |
| | | <span>调参规则 <span class="code-name">ruleSnapshot</span></span> |
| | | <el-button type="text" size="mini" @click="openJsonDialog('调参规则 ruleSnapshot', ruleSnapshot)">JSON</el-button> |
| | | </div> |
| | | <div class="pill-row"> |
| | | <span class="kv-pill" v-for="item in ruleSnapshot" :key="'r_' + item.targetType + '_' + item.targetKey"> |
| | | {{ formatRuleSnapshotText(item) }} |
| | | </span> |
| | | <span class="small-muted" v-if="ruleSnapshot.length === 0">暂无数据</span> |
| | | <div class="rule-table-wrap" v-if="ruleSnapshot.length > 0"> |
| | | <table class="rule-table"> |
| | | <thead> |
| | | <tr> |
| | | <th>参数</th> |
| | | <th>范围</th> |
| | | <th>单次步长</th> |
| | | <th>冷却</th> |
| | | <th>动态上限</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr v-for="item in ruleSnapshot" :key="'r_' + item.targetType + '_' + item.targetKey"> |
| | | <td class="rule-target-cell"> |
| | | {{ targetTypeDisplayName(item.targetType) }} / {{ parameterDisplayName(item.targetKey) }} |
| | | <span class="code-name">{{ valueOrDash(item.targetType) }}/{{ valueOrDash(item.targetKey) }}</span> |
| | | </td> |
| | | <td>{{ valueOrDash(item.minValue) }} ~ {{ valueOrDash(item.maxValue) }}</td> |
| | | <td>{{ valueOrDash(item.maxStep) }}</td> |
| | | <td>{{ valueOrDash(item.cooldownMinutes) }} 分钟</td> |
| | | <td class="rule-dynamic-cell"> |
| | | {{ valueOrDash(item.dynamicMaxValue) }} |
| | | <span class="code-name" v-if="hasText(item.dynamicMaxSource)">来源: {{ item.dynamicMaxSource }}</span> |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | <span class="small-muted" v-if="ruleSnapshot.length === 0">暂无数据</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | <el-tag size="mini">工具: {{ valueOrDash(agentResult && agentResult.toolCallCount) }}</el-tag> |
| | | <el-tag size="mini">LLM: {{ valueOrDash(agentResult && agentResult.llmCallCount) }}</el-tag> |
| | | <el-tag size="mini">Tokens: {{ valueOrDash(agentResult && agentResult.totalTokens) }}</el-tag> |
| | | <el-tag size="mini" :type="agentExecutionModeTagType">{{ agentExecutionModeText }}</el-tag> |
| | | </div> |
| | | <div class="markdown-body" v-html="renderMarkdownText(agentSummaryText())"></div> |
| | | </div> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="panel" style="margin-top:12px;"> |
| | | <div class="panel-head"> |
| | | <div> |
| | | <div class="panel-title">路径局部压力</div> |
| | | <div class="panel-tip">按目标站与热点路径段输出评分事实,Agent 负责最终调参判断</div> |
| | | </div> |
| | | <div> |
| | | <el-button size="mini" plain @click="openJsonDialog('路径压力规则 routePressureRuleSnapshot', routePressureSnapshot.routePressureRuleSnapshot)">规则</el-button> |
| | | <el-button size="mini" plain @click="openJsonDialog('路径局部压力 routePressureSnapshot', routePressureSnapshot)">JSON</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="panel-body"> |
| | | <div class="route-pressure-metrics"> |
| | | <div class="param-card"> |
| | | <div class="k">分析任务</div> |
| | | <div class="v">{{ valueOrDash(routePressureSnapshot.analyzedTaskCount) }}</div> |
| | | </div> |
| | | <div class="param-card"> |
| | | <div class="k">Trace路径</div> |
| | | <div class="v">{{ valueOrDash(routePressureSnapshot.tracePathCount) }}</div> |
| | | </div> |
| | | <div class="param-card"> |
| | | <div class="k">预估路径</div> |
| | | <div class="v">{{ valueOrDash(routePressureSnapshot.estimatedPathCount) }}</div> |
| | | </div> |
| | | <div class="param-card"> |
| | | <div class="k">路径异常</div> |
| | | <div class="v">{{ valueOrDash(routePressureSnapshot.pathErrorCount) }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="route-pressure-section"> |
| | | <div class="route-pressure-section-title">目标站路径压力</div> |
| | | <el-table :data="routePressureSnapshot.targetStationRoutePressure" border stripe height="220" size="mini" |
| | | v-loading="snapshotLoading" |
| | | :header-cell-style="{background:'#f7f9fc', color:'#2e3a4d', fontWeight:600}"> |
| | | <el-table-column prop="targetStationId" label="目标站" width="80"></el-table-column> |
| | | <el-table-column label="压力" width="80"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="mini" :type="pressureTagType(scope.row.pressureLevel)"> |
| | | {{ valueOrDash(scope.row.pressureLevel) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="pressureScore" label="评分" width="80"></el-table-column> |
| | | <el-table-column prop="confidence" label="置信度" width="80"></el-table-column> |
| | | <el-table-column label="后端提示" width="130"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="mini" :type="directionTagType(scope.row.heuristicDirection || scope.row.recommendedDirection)"> |
| | | {{ valueOrDash(scope.row.heuristicDirection || scope.row.recommendedDirection) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="blockedTaskCount" label="阻塞任务" width="80"></el-table-column> |
| | | <el-table-column prop="routeTaskCount" label="路径任务" width="80"></el-table-column> |
| | | <el-table-column label="主热点段" min-width="150"> |
| | | <template slot-scope="scope">{{ arrayPreview(scope.row.mainHotSegments) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="建议目标" min-width="150"> |
| | | <template slot-scope="scope">{{ arrayPreview(scope.row.recommendedTargets) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="事实依据" min-width="260"> |
| | | <template slot-scope="scope"> |
| | | <div class="table-text-clamp" :title="scope.row.evidenceText || scope.row.reason || '-'">{{ valueOrDash(scope.row.evidenceText || scope.row.reason) }}</div> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div class="route-pressure-section"> |
| | | <div class="route-pressure-section-title">Top 热点路径段</div> |
| | | <el-table :data="topHotPathSegments" border stripe height="260" size="mini" |
| | | v-loading="snapshotLoading" |
| | | :header-cell-style="{background:'#f7f9fc', color:'#2e3a4d', fontWeight:600}"> |
| | | <el-table-column prop="segmentKey" label="segmentKey" min-width="150"></el-table-column> |
| | | <el-table-column label="站点序列" min-width="150"> |
| | | <template slot-scope="scope">{{ arrayPreview(scope.row.stationIds) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="passTaskCount" label="passTaskCount" width="112"></el-table-column> |
| | | <el-table-column prop="pressureScore" label="score" width="80"></el-table-column> |
| | | <el-table-column label="loading" width="80"> |
| | | <template slot-scope="scope">{{ routePressureCount(scope.row, 'loading') }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="taskHolding" width="100"> |
| | | <template slot-scope="scope">{{ routePressureCount(scope.row, 'taskHolding') }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="runBlock" width="90"> |
| | | <template slot-scope="scope">{{ routePressureCount(scope.row, 'runBlock') }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="nonAutoing" width="100"> |
| | | <template slot-scope="scope">{{ routePressureCount(scope.row, 'nonAutoing') }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="pressureLevel" width="120"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="mini" :type="pressureTagType(scope.row.pressureLevel)"> |
| | | {{ valueOrDash(scope.row.pressureLevel) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="relatedTargetStations" min-width="150"> |
| | | <template slot-scope="scope">{{ arrayPreview(scope.row.relatedTargetStations) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="sampleWrkNos" min-width="150"> |
| | | <template slot-scope="scope">{{ arrayPreview(scope.row.sampleWrkNos) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | @click.stop="openTextDialog('推理摘要 #' + scope.row.id, scope.row.reasoningDigest, true)">推理全文</el-button> |
| | | <el-button type="text" size="mini" :disabled="!hasText(scope.row.snapshotDigest)" |
| | | @click.stop="openTextDialog('快照摘要 #' + scope.row.id, scope.row.snapshotDigest, false)">快照全文</el-button> |
| | | <el-button type="text" size="mini" class="job-expand-all" |
| | | @click.stop="openJobFullText(scope.row)">查看全部</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="markdown-body compact-summary" style="margin-bottom:10px;" |
| | | v-html="renderMarkdownText(scope.row.summary || '-')"></div> |
| | | <div class="job-expand-all-row"> |
| | | <el-button type="primary" plain size="mini" class="job-expand-all" |
| | | @click.stop="openJobFullText(scope.row)">查看全部</el-button> |
| | | </div> |
| | | <div class="small-muted" style="margin:8px 0;">MCP调用记录</div> |
| | | <el-table :data="mcpCallList(scope.row)" border size="mini" empty-text="暂无MCP调用记录" |
| | | style="margin-bottom:10px;" |
| | |
| | | <el-tag size="mini" :type="statusType(mcpScope.row.status)">{{ mcpScope.row.status || '-' }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="dryRun" width="80"> |
| | | <template slot-scope="mcpScope">{{ formatBooleanLabel(mcpScope.row.dryRun) }}</template> |
| | | <el-table-column label="dryRun" width="100"> |
| | | <template slot-scope="mcpScope">{{ formatMcpDryRunLabel(mcpScope.row) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="写入行为" width="110"> |
| | | <template slot-scope="mcpScope"> |
| | | <el-tag size="mini" :type="writeBehaviorTagType(mcpWriteBehavior(mcpScope.row))"> |
| | | {{ mcpWriteBehaviorLabel(mcpScope.row) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="applyJobId" label="jobId" width="95"></el-table-column> |
| | | <el-table-column label="成功/拒绝" width="95"> |
| | |
| | | <el-table-column prop="oldValue" label="原值" width="80"></el-table-column> |
| | | <el-table-column prop="requestedValue" label="请求值" width="80"></el-table-column> |
| | | <el-table-column prop="appliedValue" label="生效值" width="80"></el-table-column> |
| | | <el-table-column prop="resultStatus" label="结果" width="90"></el-table-column> |
| | | <el-table-column label="结果" width="130"> |
| | | <template slot-scope="changeScope">{{ formatResultStatus(changeScope.row.resultStatus) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="写入行为" width="110"> |
| | | <template slot-scope="changeScope"> |
| | | <el-tag size="mini" :type="writeBehaviorTagType(changeWriteBehavior(changeScope.row))"> |
| | | {{ changeWriteBehaviorLabel(changeScope.row) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="拒绝原因" min-width="220"> |
| | | <template slot-scope="changeScope"> |
| | | <div class="table-text-clamp" |
| | |
| | | <el-table-column label="MCP" width="76"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="mini" type="info">{{ mcpCallCount(scope.row) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="写入行为" width="110"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="mini" :type="writeBehaviorTagType(jobWriteBehavior(scope.row))"> |
| | | {{ jobWriteBehaviorLabel(scope.row) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalTokens" label="Tokens" width="90"></el-table-column> |
| | |
| | | cycleLoad: function() { |
| | | return this.snapshot.cycleLoadSnapshot || {}; |
| | | }, |
| | | routePressureSnapshot: function() { |
| | | var source = this.snapshot && this.snapshot.routePressureSnapshot |
| | | ? this.snapshot.routePressureSnapshot |
| | | : {}; |
| | | return { |
| | | analyzedTaskCount: source.analyzedTaskCount || 0, |
| | | tracePathCount: source.tracePathCount || 0, |
| | | estimatedPathCount: source.estimatedPathCount || 0, |
| | | pathErrorCount: source.pathErrorCount || 0, |
| | | confidence: source.confidence || '-', |
| | | routePressureRuleSnapshot: source.routePressureRuleSnapshot || {}, |
| | | taskRouteSamples: Array.isArray(source.taskRouteSamples) ? source.taskRouteSamples : [], |
| | | hotPathSegments: Array.isArray(source.hotPathSegments) ? source.hotPathSegments : [], |
| | | targetStationRoutePressure: Array.isArray(source.targetStationRoutePressure) |
| | | ? source.targetStationRoutePressure |
| | | : [] |
| | | }; |
| | | }, |
| | | topHotPathSegments: function() { |
| | | var hotPathSegments = this.routePressureSnapshot.hotPathSegments; |
| | | if (!Array.isArray(hotPathSegments)) { |
| | | return []; |
| | | } |
| | | return hotPathSegments.slice(0, 10); |
| | | }, |
| | | ruleSnapshot: function() { |
| | | return this.snapshot.ruleSnapshot || []; |
| | | }, |
| | | controlModeSnapshot: function() { |
| | | return this.snapshot.controlModeSnapshot || {}; |
| | | }, |
| | | isAnalysisOnlyMode: function() { |
| | | var controlModeSnapshot = this.controlModeSnapshot; |
| | | return this.isExplicitTrue(controlModeSnapshot.analysisOnly) |
| | | || this.isExplicitFalse(controlModeSnapshot.allowApply); |
| | | }, |
| | | controlModeDisplay: function() { |
| | | var controlModeSnapshot = this.controlModeSnapshot; |
| | | var modeLabel = this.hasText(controlModeSnapshot.modeLabel) |
| | | ? String(controlModeSnapshot.modeLabel) |
| | | : ''; |
| | | var backgroundHint = this.isExplicitFalse(controlModeSnapshot.enabled) ? '后台关闭/' : ''; |
| | | |
| | | if (this.isAnalysisOnlyMode) { |
| | | return { |
| | | label: '仅分析', |
| | | hint: backgroundHint ? '后台关闭/手动分析不写参数' : '不写参数' |
| | | }; |
| | | } |
| | | if (this.isExplicitFalse(controlModeSnapshot.analysisOnly) |
| | | && this.isExplicitTrue(controlModeSnapshot.allowApply)) { |
| | | return { |
| | | label: '正式调参', |
| | | hint: backgroundHint ? '后台关闭/手动允许写参数' : '允许写参数' |
| | | }; |
| | | } |
| | | if (modeLabel) { |
| | | return { |
| | | label: modeLabel, |
| | | hint: backgroundHint + '等待模式快照' |
| | | }; |
| | | } |
| | | return { |
| | | label: '-', |
| | | hint: backgroundHint + '等待模式快照' |
| | | }; |
| | | }, |
| | | controlModeHeroText: function() { |
| | | if (this.isExplicitFalse(this.controlModeSnapshot.enabled)) { |
| | | return this.isAnalysisOnlyMode |
| | | ? '后台自动调参已关闭;可手动触发分析、查看快照和审计记录' |
| | | : '后台自动调参已关闭;可手动触发 Agent、查看快照和审计记录'; |
| | | } |
| | | if (this.isAnalysisOnlyMode) { |
| | | return '仅分析模式:Agent 只分析、试算和审计,不写运行参数、不执行回滚'; |
| | | } |
| | | return '正式调参模式:手动触发 Agent、查看实时快照、审计调参动作和回滚最近成功调参'; |
| | | }, |
| | | manualTriggerButtonText: function() { |
| | | return this.isAnalysisOnlyMode ? '手动触发分析' : '手动触发Agent'; |
| | | }, |
| | | agentExecutionModeText: function() { |
| | | if (!this.agentResult) { |
| | | return '模式: 未触发'; |
| | | } |
| | | return '模式: ' + this.resolveExecutionModeText(this.agentResult); |
| | | }, |
| | | agentExecutionModeTagType: function() { |
| | | if (!this.agentResult) { |
| | | return 'info'; |
| | | } |
| | | return this.isResultApplyAllowed(this.agentResult) ? 'success' : 'warning'; |
| | | }, |
| | | stationBusyCount: function() { |
| | | var count = 0; |
| | |
| | | } |
| | | }, |
| | | methods: { |
| | | isExplicitFalse: function(value) { |
| | | return value === false || value === 'false' || value === 0 || value === '0'; |
| | | }, |
| | | isExplicitTrue: function(value) { |
| | | return value === true || value === 'true' || value === 1 || value === '1'; |
| | | }, |
| | | isResultApplyAllowed: function(result) { |
| | | var safeResult = result || {}; |
| | | if (this.isExplicitTrue(safeResult.analysisOnly) || this.isExplicitFalse(safeResult.allowApply)) { |
| | | return false; |
| | | } |
| | | if (this.isExplicitTrue(safeResult.allowApply)) { |
| | | return true; |
| | | } |
| | | return !this.isAnalysisOnlyMode; |
| | | }, |
| | | resolveExecutionModeText: function(result) { |
| | | var safeResult = result || {}; |
| | | if (this.hasText(safeResult.executionMode)) { |
| | | return String(safeResult.executionMode); |
| | | } |
| | | if (!this.isResultApplyAllowed(safeResult)) { |
| | | return '仅分析/不允许正式应用'; |
| | | } |
| | | return '正式调参/允许应用'; |
| | | }, |
| | | authHeaders: function() { |
| | | return { 'token': localStorage.getItem('token') }; |
| | | }, |
| | |
| | | }, |
| | | triggerManual: function() { |
| | | var self = this; |
| | | self.$confirm('手动触发会立即调用 Agent,并可能通过 MCP 执行 dry-run 与实际调参。是否继续?', '手动触发Agent', { |
| | | var confirmTitle = self.isAnalysisOnlyMode ? '手动触发分析' : '手动触发Agent'; |
| | | var confirmMessage = self.isAnalysisOnlyMode |
| | | ? '仅分析模式会立即调用 Agent,只做分析、试算和审计,不会实际应用运行参数,也不会执行回滚。是否继续?' |
| | | : '手动触发会立即调用 Agent,可能先 dry-run 再正式应用运行参数。是否继续?'; |
| | | self.$confirm(confirmMessage, confirmTitle, { |
| | | type: 'warning' |
| | | }).then(function() { |
| | | self.agentLoading = true; |
| | |
| | | }, |
| | | confirmRollback: function() { |
| | | var self = this; |
| | | if (self.isAnalysisOnlyMode) { |
| | | self.$message.warning('仅分析模式禁止回滚'); |
| | | return; |
| | | } |
| | | self.$prompt('请输入回滚原因。建议写明来自快照或审计记录的异常证据。', '回滚最近成功调参', { |
| | | confirmButtonText: '回滚', |
| | | cancelButtonText: '取消', |
| | |
| | | } |
| | | return this.mcpCallList(job).length; |
| | | }, |
| | | formatMcpDryRunLabel: function(call) { |
| | | var safeCall = call || {}; |
| | | if (safeCall.dryRun === true) { |
| | | return '是'; |
| | | } |
| | | if (safeCall.dryRun === false) { |
| | | return '否'; |
| | | } |
| | | return '-'; |
| | | }, |
| | | formatResultStatus: function(resultStatus) { |
| | | var normalizedResultStatus = this.hasText(resultStatus) |
| | | ? String(resultStatus).toLowerCase() |
| | | : ''; |
| | | var resultStatusMap = { |
| | | dry_run: '试算通过未写入', |
| | | no_change: '无变更', |
| | | rejected: '被拒绝', |
| | | success: '已生效', |
| | | pending: '待应用' |
| | | }; |
| | | return resultStatusMap[normalizedResultStatus] || this.valueOrDash(resultStatus); |
| | | }, |
| | | mcpWriteBehavior: function(call) { |
| | | return this.resolveWriteBehavior(call, this.legacyMcpWriteBehavior); |
| | | }, |
| | | mcpWriteBehaviorLabel: function(call) { |
| | | return this.resolveWriteBehaviorLabel(call, this.legacyMcpWriteBehavior); |
| | | }, |
| | | jobWriteBehavior: function(job) { |
| | | return this.resolveWriteBehavior(job, this.legacyJobWriteBehavior); |
| | | }, |
| | | jobWriteBehaviorLabel: function(job) { |
| | | return this.resolveWriteBehaviorLabel(job, this.legacyJobWriteBehavior); |
| | | }, |
| | | changeWriteBehavior: function(change) { |
| | | return this.resolveWriteBehavior(change, this.legacyChangeWriteBehavior); |
| | | }, |
| | | changeWriteBehaviorLabel: function(change) { |
| | | return this.resolveWriteBehaviorLabel(change, this.legacyChangeWriteBehavior); |
| | | }, |
| | | resolveWriteBehavior: function(source, fallback) { |
| | | var safeSource = source || {}; |
| | | if (this.hasText(safeSource.writeBehavior)) { |
| | | return this.normalizeWriteBehavior(safeSource.writeBehavior); |
| | | } |
| | | if (typeof fallback === 'function') { |
| | | return this.normalizeWriteBehavior(fallback.call(this, safeSource)); |
| | | } |
| | | return 'unknown'; |
| | | }, |
| | | resolveWriteBehaviorLabel: function(source, fallback) { |
| | | var safeSource = source || {}; |
| | | if (this.hasText(safeSource.writeBehaviorLabel)) { |
| | | return String(safeSource.writeBehaviorLabel); |
| | | } |
| | | return this.writeBehaviorLabelFromValue(this.resolveWriteBehavior(safeSource, fallback)); |
| | | }, |
| | | normalizeWriteBehavior: function(writeBehavior) { |
| | | if (!this.hasText(writeBehavior)) { |
| | | return 'unknown'; |
| | | } |
| | | var normalizedBehavior = String(writeBehavior).toLowerCase(); |
| | | var behaviorMap = { |
| | | '仅分析': 'analysis_only', |
| | | '试算': 'dry_run', |
| | | '正式写入': 'apply', |
| | | '正式应用': 'apply', |
| | | '回滚': 'rollback', |
| | | '无变更': 'no_change', |
| | | '只读': 'read_only', |
| | | '无工具': 'read_only', |
| | | '失败': 'failed', |
| | | '已拒绝': 'rejected', |
| | | '未知': 'unknown' |
| | | }; |
| | | return behaviorMap[writeBehavior] || behaviorMap[normalizedBehavior] || normalizedBehavior; |
| | | }, |
| | | writeBehaviorLabelFromValue: function(writeBehavior) { |
| | | var normalizedBehavior = this.normalizeWriteBehavior(writeBehavior); |
| | | var labelMap = { |
| | | analysis_only: '仅分析', |
| | | dry_run: '试算', |
| | | apply: '正式写入', |
| | | rollback: '回滚', |
| | | no_change: '无变更', |
| | | read_only: '只读', |
| | | failed: '失败', |
| | | rejected: '已拒绝', |
| | | unknown: '未知' |
| | | }; |
| | | return labelMap[normalizedBehavior] || '未知'; |
| | | }, |
| | | legacyMcpWriteBehavior: function(call) { |
| | | var safeCall = call || {}; |
| | | if (this.isReadOnlyToolName(safeCall.toolName)) { |
| | | return 'read_only'; |
| | | } |
| | | if (this.isRollbackToolName(safeCall.toolName)) { |
| | | return this.legacyRollbackCallWriteBehavior(safeCall); |
| | | } |
| | | if (safeCall.dryRun === true) { |
| | | return 'dry_run'; |
| | | } |
| | | if (safeCall.dryRun === false && this.isApplyToolName(safeCall.toolName)) { |
| | | return this.legacyApplyCallWriteBehavior(safeCall); |
| | | } |
| | | return 'unknown'; |
| | | }, |
| | | legacyApplyCallWriteBehavior: function(call) { |
| | | if (this.summaryIndicatesAnalysisOnly(call.errorMessage)) { |
| | | return 'analysis_only'; |
| | | } |
| | | if (call.successCount > 0) { |
| | | return 'apply'; |
| | | } |
| | | if (call.status === 'success' && call.successCount === 0 && call.rejectCount === 0) { |
| | | return 'no_change'; |
| | | } |
| | | if (call.status === 'failed') { |
| | | return 'failed'; |
| | | } |
| | | if (call.status === 'rejected' || call.rejectCount > 0) { |
| | | return 'rejected'; |
| | | } |
| | | return 'unknown'; |
| | | }, |
| | | legacyRollbackCallWriteBehavior: function(call) { |
| | | if (this.summaryIndicatesAnalysisOnly(call.errorMessage)) { |
| | | return 'analysis_only'; |
| | | } |
| | | if (call.successCount > 0) { |
| | | return 'rollback'; |
| | | } |
| | | if (call.status === 'success' && call.successCount === 0 && call.rejectCount === 0) { |
| | | return 'no_change'; |
| | | } |
| | | if (call.status === 'failed') { |
| | | return 'failed'; |
| | | } |
| | | if (call.status === 'rejected' || call.rejectCount > 0) { |
| | | return 'rejected'; |
| | | } |
| | | return 'unknown'; |
| | | }, |
| | | legacyJobWriteBehavior: function(job) { |
| | | var safeJob = job || {}; |
| | | if (safeJob.triggerType === 'rollback') { |
| | | if (safeJob.successCount > 0) { |
| | | return 'rollback'; |
| | | } |
| | | if (safeJob.status === 'failed') { |
| | | return 'failed'; |
| | | } |
| | | if (safeJob.status === 'rejected' || safeJob.rejectCount > 0) { |
| | | return 'rejected'; |
| | | } |
| | | if (safeJob.status === 'no_change' || safeJob.successCount === 0) { |
| | | return 'no_change'; |
| | | } |
| | | } |
| | | var mcpCalls = this.mcpCallList(safeJob); |
| | | if (this.containsLegacyWriteBehavior(mcpCalls, this.legacyMcpWriteBehavior, 'rollback')) { |
| | | return 'rollback'; |
| | | } |
| | | var changes = Array.isArray(safeJob.changes) ? safeJob.changes : []; |
| | | if (this.containsLegacyWriteBehavior(mcpCalls, this.legacyMcpWriteBehavior, 'apply') |
| | | || this.containsLegacyWriteBehavior(changes, this.legacyChangeWriteBehavior, 'apply')) { |
| | | return 'apply'; |
| | | } |
| | | if (this.containsLegacyWriteBehavior(changes, this.legacyChangeWriteBehavior, 'analysis_only') |
| | | || this.summaryIndicatesAnalysisOnly(safeJob.summary)) { |
| | | return 'analysis_only'; |
| | | } |
| | | if (this.allLegacyWriteBehavior(mcpCalls, this.legacyMcpWriteBehavior, 'read_only')) { |
| | | return 'read_only'; |
| | | } |
| | | if (safeJob.status === 'no_change' |
| | | || this.allLegacyWriteBehavior(changes, this.legacyChangeWriteBehavior, 'no_change') |
| | | || this.containsLegacyWriteBehavior(mcpCalls, this.legacyMcpWriteBehavior, 'no_change')) { |
| | | return 'no_change'; |
| | | } |
| | | if (this.containsLegacyWriteBehavior(mcpCalls, this.legacyMcpWriteBehavior, 'dry_run') |
| | | || this.containsLegacyWriteBehavior(changes, this.legacyChangeWriteBehavior, 'dry_run')) { |
| | | return 'dry_run'; |
| | | } |
| | | if (safeJob.status === 'failed' |
| | | || this.containsLegacyWriteBehavior(mcpCalls, this.legacyMcpWriteBehavior, 'failed') |
| | | || this.containsLegacyWriteBehavior(changes, this.legacyChangeWriteBehavior, 'failed')) { |
| | | return 'failed'; |
| | | } |
| | | if (safeJob.status === 'rejected' |
| | | || this.containsLegacyWriteBehavior(mcpCalls, this.legacyMcpWriteBehavior, 'rejected') |
| | | || this.containsLegacyWriteBehavior(changes, this.legacyChangeWriteBehavior, 'rejected')) { |
| | | return 'rejected'; |
| | | } |
| | | return 'unknown'; |
| | | }, |
| | | legacyChangeWriteBehavior: function(change) { |
| | | var safeChange = change || {}; |
| | | var resultStatus = this.hasText(safeChange.resultStatus) |
| | | ? String(safeChange.resultStatus).toLowerCase() |
| | | : ''; |
| | | if (resultStatus === 'dry_run') { |
| | | return 'dry_run'; |
| | | } |
| | | if (resultStatus === 'success') { |
| | | return 'apply'; |
| | | } |
| | | if (resultStatus === 'no_change') { |
| | | return 'no_change'; |
| | | } |
| | | if (resultStatus === 'rejected' && this.summaryIndicatesAnalysisOnly(safeChange.rejectReason)) { |
| | | return 'analysis_only'; |
| | | } |
| | | if (resultStatus === 'rejected') { |
| | | return 'rejected'; |
| | | } |
| | | if (resultStatus === 'failed') { |
| | | return 'failed'; |
| | | } |
| | | return 'unknown'; |
| | | }, |
| | | containsLegacyWriteBehavior: function(items, resolver, expectedBehavior) { |
| | | if (!Array.isArray(items) || items.length === 0) { |
| | | return false; |
| | | } |
| | | for (var itemIndex = 0; itemIndex < items.length; itemIndex++) { |
| | | var itemBehavior = this.normalizeWriteBehavior(resolver.call(this, items[itemIndex])); |
| | | if (itemBehavior === expectedBehavior) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | }, |
| | | allLegacyWriteBehavior: function(items, resolver, expectedBehavior) { |
| | | if (!Array.isArray(items) || items.length === 0) { |
| | | return false; |
| | | } |
| | | for (var itemIndex = 0; itemIndex < items.length; itemIndex++) { |
| | | var itemBehavior = this.normalizeWriteBehavior(resolver.call(this, items[itemIndex])); |
| | | if (itemBehavior !== expectedBehavior) { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | }, |
| | | isReadOnlyToolName: function(toolName) { |
| | | if (!this.hasText(toolName)) { |
| | | return false; |
| | | } |
| | | var normalizedToolName = String(toolName).toLowerCase(); |
| | | return normalizedToolName.indexOf('get_auto_tune_snapshot') >= 0 |
| | | || normalizedToolName.indexOf('get_recent_auto_tune_jobs') >= 0; |
| | | }, |
| | | isApplyToolName: function(toolName) { |
| | | if (!this.hasText(toolName)) { |
| | | return false; |
| | | } |
| | | return String(toolName).toLowerCase().indexOf('apply_auto_tune_changes') >= 0; |
| | | }, |
| | | isRollbackToolName: function(toolName) { |
| | | if (!this.hasText(toolName)) { |
| | | return false; |
| | | } |
| | | var normalizedToolName = String(toolName).toLowerCase(); |
| | | return normalizedToolName.indexOf('revert_last_auto_tune_job') >= 0 |
| | | || normalizedToolName.indexOf('rollback') >= 0; |
| | | }, |
| | | summaryIndicatesAnalysisOnly: function(summary) { |
| | | if (!this.hasText(summary)) { |
| | | return false; |
| | | } |
| | | var summaryText = String(summary).toLowerCase(); |
| | | return summaryText.indexOf('仅分析') >= 0 |
| | | || summaryText.indexOf('analysisonly=true') >= 0 |
| | | || summaryText.indexOf('"analysisonly":true') >= 0 |
| | | || summaryText.indexOf('noapply=true') >= 0 |
| | | || summaryText.indexOf('"noapply":true') >= 0 |
| | | || summaryText.indexOf('禁止实际应用') >= 0; |
| | | }, |
| | | writeBehaviorTagType: function(writeBehavior) { |
| | | var normalizedBehavior = this.normalizeWriteBehavior(writeBehavior); |
| | | if (normalizedBehavior === 'apply') { |
| | | return 'success'; |
| | | } |
| | | if (normalizedBehavior === 'rollback') { |
| | | return 'danger'; |
| | | } |
| | | if (normalizedBehavior === 'failed' || normalizedBehavior === 'rejected') { |
| | | return 'danger'; |
| | | } |
| | | if (normalizedBehavior === 'analysis_only' || normalizedBehavior === 'dry_run') { |
| | | return 'warning'; |
| | | } |
| | | return 'info'; |
| | | }, |
| | | formatBooleanLabel: function(value) { |
| | | if (value === true) { |
| | | return '是'; |
| | |
| | | if (data && data.agentResult) { |
| | | return data.agentResult; |
| | | } |
| | | var controlModeSnapshot = this.controlModeSnapshot; |
| | | return { |
| | | success: false, |
| | | triggerType: defaultTriggerType || '-', |
| | | summary: 'Agent未触发: ' + ((data && data.reason) ? data.reason : '-'), |
| | | analysisOnly: data && data.analysisOnly !== undefined ? data.analysisOnly : controlModeSnapshot.analysisOnly, |
| | | allowApply: data && data.allowApply !== undefined ? data.allowApply : controlModeSnapshot.allowApply, |
| | | executionMode: data && data.executionMode ? data.executionMode : controlModeSnapshot.modeCode, |
| | | toolCallCount: 0, |
| | | llmCallCount: 0, |
| | | promptTokens: 0, |
| | |
| | | } |
| | | return value; |
| | | }, |
| | | pressureTagType: function(level) { |
| | | if (level === 'high') { |
| | | return 'danger'; |
| | | } |
| | | if (level === 'medium') { |
| | | return 'warning'; |
| | | } |
| | | if (level === 'low') { |
| | | return 'success'; |
| | | } |
| | | return 'info'; |
| | | }, |
| | | directionTagType: function(direction) { |
| | | if (direction === 'increase' || direction === 'increase_candidate') { |
| | | return 'success'; |
| | | } |
| | | if (direction === 'decrease' || direction === 'decrease_candidate') { |
| | | return 'danger'; |
| | | } |
| | | if (direction === 'hold' || direction === 'review') { |
| | | return 'warning'; |
| | | } |
| | | return 'info'; |
| | | }, |
| | | arrayPreview: function(value) { |
| | | if (Array.isArray(value)) { |
| | | return value.length > 0 ? value.join(', ') : '-'; |
| | | } |
| | | return this.valueOrDash(value); |
| | | }, |
| | | routePressureCount: function(row, key) { |
| | | var safeRow = row || {}; |
| | | var countKey = key + 'Count'; |
| | | if (safeRow[key] !== null && safeRow[key] !== undefined && safeRow[key] !== '') { |
| | | return safeRow[key]; |
| | | } |
| | | return this.valueOrDash(safeRow[countKey]); |
| | | }, |
| | | mapEntries: function(source) { |
| | | var result = []; |
| | | if (!source) { |
| | |
| | | import com.zy.ai.entity.AiAutoTuneChange; |
| | | import com.zy.ai.entity.AiAutoTuneJob; |
| | | import com.zy.ai.service.impl.AutoTuneApplyServiceImpl; |
| | | import com.zy.ai.service.impl.AutoTuneControlModeServiceImpl; |
| | | import com.zy.asrs.entity.BasCrnp; |
| | | import com.zy.asrs.entity.BasDualCrnp; |
| | | import com.zy.asrs.entity.BasStation; |
| | |
| | | ReflectionTestUtils.setField(service, "aiAutoTuneJobService", aiAutoTuneJobService); |
| | | ReflectionTestUtils.setField(service, "aiAutoTuneChangeService", aiAutoTuneChangeService); |
| | | ReflectionTestUtils.setField(service, "configService", configService); |
| | | ReflectionTestUtils.setField(service, "autoTuneControlModeService", |
| | | new AutoTuneControlModeServiceImpl(configService)); |
| | | ReflectionTestUtils.setField(service, "basStationService", basStationService); |
| | | ReflectionTestUtils.setField(service, "basCrnpService", basCrnpService); |
| | | ReflectionTestUtils.setField(service, "basDualCrnpService", basDualCrnpService); |
| | |
| | | when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(Collections.emptyList()); |
| | | when(wrkMastService.count(any(Wrapper.class))).thenReturn(0L); |
| | | when(configService.getConfigValue(eq("aiAutoTuneIntervalMinutes"), any())).thenReturn("10"); |
| | | when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("N"); |
| | | when(configService.saveConfigValue(any(), any())).thenReturn(true); |
| | | when(basStationService.update(any(Wrapper.class))).thenReturn(true); |
| | | when(basCrnpService.update(any(Wrapper.class))).thenReturn(true); |
| | |
| | | } |
| | | |
| | | @Test |
| | | void analysisOnlyRealApplyWritesRejectedAuditAndDoesNotAcquireApplyLock() { |
| | | when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("Y"); |
| | | |
| | | AutoTuneApplyResult result = service.apply(request(false, |
| | | command("sys_config", null, "conveyorStationTaskLimit", "15"))); |
| | | |
| | | List<AiAutoTuneChange> changes = savedChanges(); |
| | | AiAutoTuneJob job = updatedJob(); |
| | | assertFalse(result.getSuccess()); |
| | | assertTrue(result.getAnalysisOnly()); |
| | | assertTrue(result.getNoApply()); |
| | | assertEquals("rejected", job.getStatus()); |
| | | assertEquals("rejected", changes.get(0).getResultStatus()); |
| | | assertTrue(changes.get(0).getRejectReason().contains("仅分析模式禁止实际应用/回滚")); |
| | | verify(redisUtil, never()).trySetStringIfAbsent(anyString(), anyString(), anyLong()); |
| | | verify(configService, never()).saveConfigValue(any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void realApplyResultUsesEntryControlModeSnapshotWhenConfigFlips() { |
| | | when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("N", "Y"); |
| | | when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10")); |
| | | |
| | | AutoTuneApplyResult result = service.apply(request(false, |
| | | command("sys_config", null, "conveyorStationTaskLimit", "15"))); |
| | | |
| | | assertTrue(result.getSuccess()); |
| | | assertFalse(result.getAnalysisOnly()); |
| | | verify(configService, times(1)).getConfigValue("aiAutoTuneAnalysisOnly", "Y"); |
| | | } |
| | | |
| | | @Test |
| | | void acceptedDryRunDoesNotWriteTargetStores() { |
| | | when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10")); |
| | | |
| | |
| | | } |
| | | |
| | | @Test |
| | | void analysisOnlyRollbackWritesRejectedAuditAndDoesNotAcquireApplyLock() { |
| | | when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("Y"); |
| | | |
| | | AutoTuneApplyResult result = service.rollbackLastSuccessfulJob("manual rollback"); |
| | | |
| | | List<AiAutoTuneChange> changes = savedChanges(); |
| | | AiAutoTuneJob job = updatedJob(); |
| | | assertFalse(result.getSuccess()); |
| | | assertTrue(result.getAnalysisOnly()); |
| | | assertTrue(result.getNoApply()); |
| | | assertEquals("rejected", job.getStatus()); |
| | | assertEquals("rejected", changes.get(0).getResultStatus()); |
| | | assertEquals("rollback", changes.get(0).getTargetKey()); |
| | | verify(redisUtil, never()).trySetStringIfAbsent(anyString(), anyString(), anyLong()); |
| | | } |
| | | |
| | | @Test |
| | | void rollbackResultUsesEntryControlModeSnapshotWhenConfigFlips() { |
| | | when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("N", "Y"); |
| | | AiAutoTuneJob latestRealJob = job(10L, "manual", "success"); |
| | | AiAutoTuneChange configChange = successChange(10L, "sys_config", "", "conveyorStationTaskLimit", "10", "15"); |
| | | when(aiAutoTuneChangeService.list(any(Wrapper.class))) |
| | | .thenReturn(List.of(configChange)) |
| | | .thenReturn(List.of(configChange)); |
| | | when(aiAutoTuneJobService.getById(10L)).thenReturn(latestRealJob); |
| | | when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "15")); |
| | | |
| | | AutoTuneApplyResult result = service.rollbackLastSuccessfulJob("manual rollback"); |
| | | |
| | | assertTrue(result.getSuccess()); |
| | | assertFalse(result.getAnalysisOnly()); |
| | | verify(configService, times(1)).getConfigValue("aiAutoTuneAnalysisOnly", "Y"); |
| | | } |
| | | |
| | | @Test |
| | | void applyJobRecordsActiveTasksWhenCountIsPositive() { |
| | | when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10")); |
| | | when(wrkMastService.count(any(Wrapper.class))).thenReturn(1L); |
| | |
| | | package com.zy.ai.service; |
| | | |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.baomidou.mybatisplus.core.conditions.AbstractWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| | | import com.zy.ai.domain.autotune.AutoTuneApplyRequest; |
| | | import com.zy.ai.domain.autotune.AutoTuneApplyResult; |
| | |
| | | import com.zy.ai.mcp.service.SpringAiMcpToolManager; |
| | | import com.zy.ai.mcp.tool.AutoTuneMcpTools; |
| | | import com.zy.ai.service.impl.AutoTuneAgentServiceImpl; |
| | | import com.zy.ai.service.impl.AutoTuneControlModeServiceImpl; |
| | | import com.zy.ai.service.impl.AutoTuneCoordinatorServiceImpl; |
| | | import com.zy.asrs.service.WrkMastService; |
| | | import com.zy.common.utils.RedisUtil; |
| | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertFalse; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertNull; |
| | | import static org.junit.jupiter.api.Assertions.assertSame; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | |
| | | import static org.mockito.ArgumentMatchers.anyString; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.doThrow; |
| | | import static org.mockito.Mockito.lenient; |
| | | import static org.mockito.Mockito.never; |
| | | import static org.mockito.Mockito.times; |
| | | import static org.mockito.Mockito.verify; |
| | |
| | | private AiPromptTemplateService aiPromptTemplateService; |
| | | @Mock |
| | | private ConfigService configService; |
| | | private AutoTuneControlModeService autoTuneControlModeService; |
| | | @Mock |
| | | private WrkMastService wrkMastService; |
| | | @Mock |
| | |
| | | |
| | | @BeforeEach |
| | | void setUp() { |
| | | autoTuneControlModeService = new AutoTuneControlModeServiceImpl(configService); |
| | | tools = new AutoTuneMcpTools( |
| | | autoTuneSnapshotService, |
| | | autoTuneApplyService, |
| | | aiAutoTuneJobService, |
| | | aiAutoTuneChangeService, |
| | | aiAutoTuneMcpCallService); |
| | | lenient().when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("Y"); |
| | | lenient().when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("N"); |
| | | } |
| | | |
| | | @Test |
| | |
| | | job.setSummary("applied"); |
| | | job.setSuccessCount(1); |
| | | job.setRejectCount(0); |
| | | AiAutoTuneChange change = new AiAutoTuneChange(); |
| | | change.setJobId(70L); |
| | | change.setTargetType("sys_config"); |
| | | change.setTargetKey("conveyorStationTaskLimit"); |
| | | change.setRequestedValue("12"); |
| | | change.setResultStatus("success"); |
| | | AiAutoTuneChange agentChange = new AiAutoTuneChange(); |
| | | agentChange.setJobId(7L); |
| | | agentChange.setTargetType("station"); |
| | | agentChange.setTargetId("101"); |
| | | agentChange.setTargetKey("outTaskLimit"); |
| | | agentChange.setRequestedValue("3"); |
| | | agentChange.setResultStatus("success"); |
| | | AiAutoTuneChange applyChange = new AiAutoTuneChange(); |
| | | applyChange.setJobId(70L); |
| | | applyChange.setTargetType("sys_config"); |
| | | applyChange.setTargetKey("conveyorStationTaskLimit"); |
| | | applyChange.setRequestedValue("12"); |
| | | applyChange.setResultStatus("success"); |
| | | com.zy.ai.entity.AiAutoTuneMcpCall mcpCall = new com.zy.ai.entity.AiAutoTuneMcpCall(); |
| | | mcpCall.setAgentJobId(7L); |
| | | mcpCall.setCallSeq(1); |
| | |
| | | |
| | | when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(job)); |
| | | when(aiAutoTuneMcpCallService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(mcpCall)); |
| | | when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(change)); |
| | | List<AiAutoTuneChange> auditChanges = new ArrayList<>(); |
| | | auditChanges.add(agentChange); |
| | | auditChanges.add(applyChange); |
| | | when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(auditChanges); |
| | | |
| | | List<Map<String, Object>> result = tools.getRecentAutoTuneJobs(99); |
| | | |
| | |
| | | assertFalse(result.get(0).containsKey("reasoningDigest")); |
| | | assertEquals(1, result.get(0).get("mcpCallCount")); |
| | | List<?> changes = (List<?>) result.get(0).get("changes"); |
| | | assertEquals(1, changes.size()); |
| | | assertEquals(2, changes.size()); |
| | | assertEquals(7L, ((Map<?, ?>) changes.get(0)).get("jobId")); |
| | | assertEquals(70L, ((Map<?, ?>) changes.get(1)).get("jobId")); |
| | | |
| | | ArgumentCaptor<Wrapper<AiAutoTuneJob>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class); |
| | | verify(aiAutoTuneJobService).list(wrapperCaptor.capture()); |
| | | assertTrue(wrapperCaptor.getValue().getSqlSegment().contains("limit 20")); |
| | | |
| | | ArgumentCaptor<Wrapper<AiAutoTuneChange>> changeWrapperCaptor = ArgumentCaptor.forClass(Wrapper.class); |
| | | verify(aiAutoTuneChangeService).list(changeWrapperCaptor.capture()); |
| | | Wrapper<AiAutoTuneChange> changeWrapper = changeWrapperCaptor.getValue(); |
| | | assertTrue(changeWrapper.getSqlSegment().contains("job_id IN")); |
| | | List<Object> changeQueryParams = wrapperParamValues(changeWrapper); |
| | | assertTrue(changeQueryParams.contains(7L)); |
| | | assertTrue(changeQueryParams.contains(70L)); |
| | | } |
| | | |
| | | @Test |
| | | void recentJobsMarksRollbackMcpChangesAsRollback() { |
| | | AiAutoTuneJob job = new AiAutoTuneJob(); |
| | | job.setId(8L); |
| | | job.setTriggerType("manual"); |
| | | job.setStatus("success"); |
| | | job.setSuccessCount(1); |
| | | job.setRejectCount(0); |
| | | AiAutoTuneChange rollbackChange = new AiAutoTuneChange(); |
| | | rollbackChange.setJobId(80L); |
| | | rollbackChange.setTargetType("sys_config"); |
| | | rollbackChange.setTargetKey("conveyorStationTaskLimit"); |
| | | rollbackChange.setResultStatus("success"); |
| | | com.zy.ai.entity.AiAutoTuneMcpCall mcpCall = new com.zy.ai.entity.AiAutoTuneMcpCall(); |
| | | mcpCall.setAgentJobId(8L); |
| | | mcpCall.setCallSeq(1); |
| | | mcpCall.setToolName("wcs_local_dispatch_revert_last_auto_tune_job"); |
| | | mcpCall.setStatus("success"); |
| | | mcpCall.setApplyJobId(80L); |
| | | mcpCall.setSuccessCount(1); |
| | | |
| | | when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(job)); |
| | | when(aiAutoTuneMcpCallService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(mcpCall)); |
| | | when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(rollbackChange)); |
| | | |
| | | List<Map<String, Object>> result = tools.getRecentAutoTuneJobs(1); |
| | | |
| | | assertEquals("rollback", result.get(0).get("writeBehavior")); |
| | | List<?> changes = (List<?>) result.get(0).get("changes"); |
| | | assertEquals(1, changes.size()); |
| | | assertEquals("rollback", ((Map<?, ?>) changes.get(0)).get("writeBehavior")); |
| | | } |
| | | |
| | | @Test |
| | |
| | | AutoTuneApplyResult expected = new AutoTuneApplyResult(); |
| | | expected.setDryRun(true); |
| | | expected.setSuccess(true); |
| | | expected.setChanges(Collections.singletonList(applyResultChange("dry_run"))); |
| | | when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(expected); |
| | | |
| | | AutoTuneChangeCommand command = new AutoTuneChangeCommand(); |
| | |
| | | } |
| | | |
| | | @Test |
| | | void applyToolRejectsDuplicateDryRunTargetsBeforeServiceCall() { |
| | | List<AutoTuneChangeCommand> changes = new ArrayList<>(); |
| | | changes.add(change(" sys_config ", "ignored-first", " conveyorStationTaskLimit ", "12")); |
| | | changes.add(change("SYS_CONFIG", "ignored-second", "conveyorStationTaskLimit", "13")); |
| | | |
| | | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, |
| | | () -> tools.applyAutoTuneChanges("duplicate target", 10, "agent", true, null, changes)); |
| | | |
| | | assertTrue(exception.getMessage().contains("Duplicate auto-tune change target")); |
| | | assertTrue(exception.getMessage().contains("targetType=sys_config")); |
| | | assertTrue(exception.getMessage().contains("targetKey=conveyorStationTaskLimit")); |
| | | verify(autoTuneApplyService, never()).apply(any(AutoTuneApplyRequest.class)); |
| | | } |
| | | |
| | | @Test |
| | | void applyToolRejectsRealApplyWithoutDryRunTokenInAnalysisOnlyMode() { |
| | | AutoTuneChangeCommand command = change("sys_config", null, "conveyorStationTaskLimit", "12"); |
| | | |
| | | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, |
| | | () -> tools.applyAutoTuneChanges("direct apply", 10, "agent", false, null, |
| | | Collections.singletonList(command))); |
| | | |
| | | assertTrue(exception.getMessage().contains("dryRunToken is required")); |
| | | verify(autoTuneApplyService, never()).apply(any(AutoTuneApplyRequest.class)); |
| | | } |
| | | |
| | | @Test |
| | | void applyToolAllowsRealApplyOnlyWithMatchingDryRunToken() { |
| | | AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult(); |
| | | dryRunResult.setDryRun(true); |
| | | dryRunResult.setSuccess(true); |
| | | dryRunResult.setChanges(Collections.singletonList(applyResultChange("dry_run"))); |
| | | AutoTuneApplyResult applyResult = new AutoTuneApplyResult(); |
| | | applyResult.setDryRun(false); |
| | | applyResult.setSuccess(true); |
| | |
| | | AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult(); |
| | | dryRunResult.setDryRun(true); |
| | | dryRunResult.setSuccess(true); |
| | | dryRunResult.setChanges(Collections.singletonList(applyResultChange("dry_run"))); |
| | | when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(dryRunResult); |
| | | |
| | | AutoTuneApplyResult preview = tools.applyAutoTuneChanges("preview", 10, "agent", true, null, |
| | |
| | | AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult(); |
| | | dryRunResult.setDryRun(true); |
| | | dryRunResult.setSuccess(true); |
| | | dryRunResult.setChanges(Collections.singletonList(applyResultChange("dry_run"))); |
| | | when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(dryRunResult); |
| | | List<AutoTuneChangeCommand> changes = Collections.singletonList( |
| | | change("sys_config", null, "conveyorStationTaskLimit", "12")); |
| | |
| | | } |
| | | |
| | | @Test |
| | | void applyToolDoesNotIssueDryRunTokenWhenAllChangesAreNoChange() { |
| | | AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult(); |
| | | dryRunResult.setDryRun(true); |
| | | dryRunResult.setSuccess(true); |
| | | dryRunResult.setSuccessCount(0); |
| | | dryRunResult.setRejectCount(0); |
| | | dryRunResult.setChanges(Collections.singletonList(applyResultChange("no_change"))); |
| | | when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(dryRunResult); |
| | | |
| | | AutoTuneApplyResult preview = tools.applyAutoTuneChanges("preview", 10, "agent", true, null, |
| | | Collections.singletonList(change("sys_config", null, "conveyorStationTaskLimit", "12"))); |
| | | |
| | | assertNull(preview.getDryRunToken()); |
| | | verify(autoTuneApplyService, times(1)).apply(any(AutoTuneApplyRequest.class)); |
| | | } |
| | | |
| | | @Test |
| | | void rollbackToolDelegatesToApplyServiceRollback() { |
| | | AutoTuneApplyResult expected = new AutoTuneApplyResult(); |
| | | when(autoTuneApplyService.rollbackLastSuccessfulJob("bad result")).thenReturn(expected); |
| | | |
| | | AutoTuneApplyResult result = tools.revertLastAutoTuneJob("bad result"); |
| | | |
| | | assertSame(expected, result); |
| | | verify(autoTuneApplyService).rollbackLastSuccessfulJob("bad result"); |
| | | } |
| | | |
| | | @Test |
| | | void rollbackToolBlocksRollbackInAnalysisOnlyMode() { |
| | | AutoTuneApplyResult expected = new AutoTuneApplyResult(); |
| | | expected.setSuccess(false); |
| | | expected.setAnalysisOnly(true); |
| | | expected.setNoApply(true); |
| | | expected.setRejectCount(1); |
| | | when(autoTuneApplyService.rollbackLastSuccessfulJob("bad result")).thenReturn(expected); |
| | | |
| | | AutoTuneApplyResult result = tools.revertLastAutoTuneJob("bad result"); |
| | |
| | | AutoTuneAgentServiceImpl service = new AutoTuneAgentServiceImpl( |
| | | llmChatService, |
| | | mcpToolManager, |
| | | aiPromptTemplateService); |
| | | aiPromptTemplateService, |
| | | autoTuneControlModeService); |
| | | AiPromptTemplate promptTemplate = new AiPromptTemplate(); |
| | | promptTemplate.setContent("system prompt"); |
| | | when(aiPromptTemplateService.resolvePublished("wcs_auto_tune_dispatch")).thenReturn(promptTemplate); |
| | |
| | | } |
| | | |
| | | @Test |
| | | void agentCallsMcpForRealApplyInAnalysisOnlyMode() { |
| | | when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("Y"); |
| | | AutoTuneAgentServiceImpl service = agentService(); |
| | | when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools()); |
| | | when(mcpToolManager.callTool(any(), any(JSONObject.class))).thenAnswer(invocation -> { |
| | | String toolName = invocation.getArgument(0); |
| | | if ("wcs_local_dispatch_apply_auto_tune_changes".equals(toolName)) { |
| | | return rejectedAnalysisOnlyResult(false); |
| | | } |
| | | return Collections.singletonMap("ok", true); |
| | | }); |
| | | when(llmChatService.chatCompletionOrThrow(any(), anyDouble(), anyInt(), any())) |
| | | .thenReturn( |
| | | response("snapshot", toolCall("call_1", "wcs_local_dispatch_get_auto_tune_snapshot", |
| | | "{}"), 10, 5), |
| | | response("apply", toolCall("call_2", "wcs_local_dispatch_apply_auto_tune_changes", |
| | | "{\"dryRun\":false,\"changes\":[{\"targetType\":\"sys_config\",\"targetKey\":\"conveyorStationTaskLimit\",\"newValue\":\"12\"}]}"), 10, 5), |
| | | response("stop", null, 10, 5) |
| | | ); |
| | | |
| | | AutoTuneAgentService.AutoTuneAgentResult result = service.runAutoTune("manual"); |
| | | |
| | | assertFalse(result.getSuccess()); |
| | | assertTrue(result.getAnalysisOnly()); |
| | | assertFalse(result.getAllowApply()); |
| | | assertEquals("analysis_only", result.getExecutionMode()); |
| | | assertFalse(result.getActualApplyCalled()); |
| | | assertEquals(1, result.getRejectCount()); |
| | | assertTrue(result.getSummary().contains("仅分析模式禁止实际应用/回滚")); |
| | | verify(mcpToolManager).callTool(eq("wcs_local_dispatch_get_auto_tune_snapshot"), any(JSONObject.class)); |
| | | verify(mcpToolManager).callTool(eq("wcs_local_dispatch_apply_auto_tune_changes"), any(JSONObject.class)); |
| | | } |
| | | |
| | | @Test |
| | | void agentCallsMcpForRollbackInAnalysisOnlyMode() { |
| | | when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("Y"); |
| | | AutoTuneAgentServiceImpl service = agentService(); |
| | | when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools()); |
| | | when(mcpToolManager.callTool(any(), any(JSONObject.class))).thenAnswer(invocation -> { |
| | | String toolName = invocation.getArgument(0); |
| | | if ("wcs_local_dispatch_revert_last_auto_tune_job".equals(toolName)) { |
| | | return rejectedAnalysisOnlyResult(false); |
| | | } |
| | | return Collections.singletonMap("ok", true); |
| | | }); |
| | | when(llmChatService.chatCompletionOrThrow(any(), anyDouble(), anyInt(), any())) |
| | | .thenReturn( |
| | | response("snapshot", toolCall("call_1", "wcs_local_dispatch_get_auto_tune_snapshot", |
| | | "{}"), 10, 5), |
| | | response("rollback", toolCall("call_2", "wcs_local_dispatch_revert_last_auto_tune_job", |
| | | "{\"reason\":\"bad result\"}"), 10, 5), |
| | | response("stop", null, 10, 5) |
| | | ); |
| | | |
| | | AutoTuneAgentService.AutoTuneAgentResult result = service.runAutoTune("manual"); |
| | | |
| | | assertFalse(result.getSuccess()); |
| | | assertTrue(result.getAnalysisOnly()); |
| | | assertTrue(result.getRollbackCalled()); |
| | | assertEquals(1, result.getRejectCount()); |
| | | assertTrue(result.getSummary().contains("仅分析模式禁止实际应用/回滚")); |
| | | verify(mcpToolManager).callTool(eq("wcs_local_dispatch_get_auto_tune_snapshot"), any(JSONObject.class)); |
| | | verify(mcpToolManager).callTool(eq("wcs_local_dispatch_revert_last_auto_tune_job"), any(JSONObject.class)); |
| | | } |
| | | |
| | | @Test |
| | | void agentMarksSnapshotOnlyRunAsNoActualMutationEvenIfAssistantClaimsApplied() { |
| | | AutoTuneAgentServiceImpl service = agentService(); |
| | | when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools()); |
| | |
| | | assertFalse(result.getActualApplyCalled()); |
| | | assertFalse(result.getRollbackCalled()); |
| | | assertEquals(0, result.getSuccessCount()); |
| | | assertTrue(result.getSummary().startsWith("自动调参 Agent 未调用实际应用或回滚工具,未修改运行参数。")); |
| | | assertTrue(result.getSummary().contains("自动调参 Agent 未调用实际应用或回滚工具,未修改运行参数。")); |
| | | } |
| | | |
| | | @Test |
| | | void agentDoesNotMarkNoChangeRealApplyAsActualApplyWhenSuccessCountIsPositive() { |
| | | AutoTuneAgentServiceImpl service = agentService(); |
| | | when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools()); |
| | | when(mcpToolManager.callTool(any(), any(JSONObject.class))).thenAnswer(invocation -> { |
| | | String toolName = invocation.getArgument(0); |
| | | if ("wcs_local_dispatch_apply_auto_tune_changes".equals(toolName)) { |
| | | LinkedHashMap<String, Object> noChange = new LinkedHashMap<>(); |
| | | noChange.put("resultStatus", "no_change"); |
| | | |
| | | LinkedHashMap<String, Object> applyOutput = new LinkedHashMap<>(); |
| | | applyOutput.put("success", true); |
| | | applyOutput.put("dryRun", false); |
| | | applyOutput.put("successCount", 1); |
| | | applyOutput.put("rejectCount", 0); |
| | | applyOutput.put("changes", Collections.singletonList(noChange)); |
| | | return applyOutput; |
| | | } |
| | | return Collections.singletonMap("ok", true); |
| | | }); |
| | | when(llmChatService.chatCompletionOrThrow(any(), anyDouble(), anyInt(), any())) |
| | | .thenReturn( |
| | | response("snapshot", toolCall("call_1", "wcs_local_dispatch_get_auto_tune_snapshot", |
| | | "{}"), 10, 5), |
| | | response("apply no change", toolCall("call_2", "wcs_local_dispatch_apply_auto_tune_changes", |
| | | "{\"dryRun\":false,\"dryRunToken\":\"token-123\",\"changes\":[{\"targetType\":\"sys_config\",\"targetKey\":\"conveyorStationTaskLimit\",\"newValue\":\"12\"}]}"), 10, 5), |
| | | response("无变更", null, 10, 5) |
| | | ); |
| | | |
| | | AutoTuneAgentService.AutoTuneAgentResult result = service.runAutoTune("manual"); |
| | | |
| | | assertTrue(result.getSuccess()); |
| | | assertFalse(result.getActualApplyCalled()); |
| | | assertEquals(1, result.getSuccessCount()); |
| | | assertEquals(0, result.getRejectCount()); |
| | | } |
| | | |
| | | @Test |
| | |
| | | AiPromptTemplate promptTemplate = new AiPromptTemplate(); |
| | | promptTemplate.setContent("system prompt"); |
| | | when(aiPromptTemplateService.resolvePublished("wcs_auto_tune_dispatch")).thenReturn(promptTemplate); |
| | | return new AutoTuneAgentServiceImpl(llmChatService, mcpToolManager, aiPromptTemplateService); |
| | | return new AutoTuneAgentServiceImpl( |
| | | llmChatService, |
| | | mcpToolManager, |
| | | aiPromptTemplateService, |
| | | autoTuneControlModeService); |
| | | } |
| | | |
| | | private AutoTuneCoordinatorServiceImpl coordinatorService() { |
| | | return new AutoTuneCoordinatorServiceImpl( |
| | | configService, |
| | | autoTuneControlModeService, |
| | | wrkMastService, |
| | | aiAutoTuneJobService, |
| | | aiAutoTuneMcpCallService, |
| | |
| | | return result; |
| | | } |
| | | |
| | | private AutoTuneApplyResult rejectedAnalysisOnlyResult(Boolean dryRun) { |
| | | AutoTuneApplyResult result = new AutoTuneApplyResult(); |
| | | result.setDryRun(dryRun); |
| | | result.setSuccess(false); |
| | | result.setAnalysisOnly(true); |
| | | result.setNoApply(true); |
| | | result.setSuccessCount(0); |
| | | result.setRejectCount(1); |
| | | result.setSummary("仅分析模式禁止实际应用/回滚,未修改运行参数"); |
| | | result.setChanges(new ArrayList<>()); |
| | | return result; |
| | | } |
| | | |
| | | private AutoTuneChangeCommand change(String targetType, String targetId, String targetKey, String newValue) { |
| | | AutoTuneChangeCommand command = new AutoTuneChangeCommand(); |
| | | command.setTargetType(targetType); |
| | |
| | | return command; |
| | | } |
| | | |
| | | private AiAutoTuneChange applyResultChange(String resultStatus) { |
| | | AiAutoTuneChange change = new AiAutoTuneChange(); |
| | | change.setResultStatus(resultStatus); |
| | | return change; |
| | | } |
| | | |
| | | private List<Object> wrapperParamValues(Wrapper<?> wrapper) { |
| | | if (!(wrapper instanceof AbstractWrapper<?, ?, ?> abstractWrapper)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return new ArrayList<>(abstractWrapper.getParamNameValuePairs().values()); |
| | | } |
| | | |
| | | private List<Object> allowedOpenAiTools() { |
| | | List<Object> tools = new ArrayList<>(); |
| | | tools.add(openAiTool("wcs_local_dispatch_get_auto_tune_snapshot")); |
| | |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| | | import com.zy.ai.domain.autotune.AutoTuneRoutePressureSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneRuleSnapshotItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneStationRuntimeItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskDetailItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskSnapshot; |
| | | import com.zy.ai.service.FlowTopologySnapshotService; |
| | | import com.zy.ai.service.RoutePressureSnapshotService; |
| | | import com.zy.asrs.entity.BasDevp; |
| | | import com.zy.asrs.entity.BasStation; |
| | | import com.zy.asrs.entity.WrkMast; |
| | |
| | | import com.zy.core.enums.WrkIoType; |
| | | import com.zy.core.enums.WrkStsType; |
| | | import com.zy.core.model.StationObjModel; |
| | | import com.zy.core.model.protocol.StationProtocol; |
| | | import com.zy.system.service.ConfigService; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.ArgumentCaptor; |
| | |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNull; |
| | | import static org.junit.jupiter.api.Assertions.assertSame; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.Mockito.mock; |
| | |
| | | |
| | | @BeforeEach |
| | | void setUp() { |
| | | ConfigService configService = mock(ConfigService.class); |
| | | service = new AutoTuneSnapshotServiceImpl(); |
| | | ReflectionTestUtils.setField(service, "configService", configService); |
| | | ReflectionTestUtils.setField(service, "autoTuneControlModeService", |
| | | new AutoTuneControlModeServiceImpl(configService)); |
| | | } |
| | | |
| | | @Test |
| | |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotIncludesRoutePressureSnapshot() { |
| | | WrkMastService wrkMastService = mock(WrkMastService.class); |
| | | FlowTopologySnapshotService flowTopologySnapshotService = mock(FlowTopologySnapshotService.class); |
| | | RoutePressureSnapshotService routePressureSnapshotService = mock(RoutePressureSnapshotService.class); |
| | | AutoTuneRoutePressureSnapshot routePressureSnapshot = new AutoTuneRoutePressureSnapshot(); |
| | | List<WrkMast> activeTasks = Collections.singletonList( |
| | | outboundTask(190263, WrkStsType.NEW_OUTBOUND.sts, 101, 1, |
| | | "BATCH", 8, "目标出库站:101 已达出库任务上限,当前=1,上限=1") |
| | | ); |
| | | when(wrkMastService.list(any(Wrapper.class))).thenReturn(activeTasks); |
| | | when(flowTopologySnapshotService.buildSnapshot(any())).thenReturn(Collections.emptyList()); |
| | | when(routePressureSnapshotService.buildSnapshot(any(), any(), any())).thenReturn(routePressureSnapshot); |
| | | ReflectionTestUtils.setField(service, "wrkMastService", wrkMastService); |
| | | ReflectionTestUtils.setField(service, "flowTopologySnapshotService", flowTopologySnapshotService); |
| | | ReflectionTestUtils.setField(service, "routePressureSnapshotService", routePressureSnapshotService); |
| | | |
| | | AutoTuneSnapshot snapshot = service.buildSnapshot(); |
| | | |
| | | assertSame(routePressureSnapshot, snapshot.getRoutePressureSnapshot()); |
| | | assertEquals(1, snapshot.getTaskSnapshot().getActiveTaskCount()); |
| | | |
| | | ArgumentCaptor<List<WrkMast>> activeTasksCaptor = ArgumentCaptor.forClass(List.class); |
| | | ArgumentCaptor<AutoTuneTaskSnapshot> taskSnapshotCaptor = ArgumentCaptor.forClass(AutoTuneTaskSnapshot.class); |
| | | ArgumentCaptor<List<AutoTuneStationRuntimeItem>> stationRuntimeCaptor = ArgumentCaptor.forClass(List.class); |
| | | verify(routePressureSnapshotService).buildSnapshot( |
| | | activeTasksCaptor.capture(), |
| | | taskSnapshotCaptor.capture(), |
| | | stationRuntimeCaptor.capture() |
| | | ); |
| | | assertSame(activeTasks, activeTasksCaptor.getValue()); |
| | | assertSame(snapshot.getTaskSnapshot(), taskSnapshotCaptor.getValue()); |
| | | assertSame(snapshot.getStationRuntimeSnapshot(), stationRuntimeCaptor.getValue()); |
| | | verify(wrkMastService).list(any(Wrapper.class)); |
| | | } |
| | | |
| | | @Test |
| | | void loadOutStationListOnlyQueriesBasDevpOutStations() { |
| | | BasStationService basStationService = mock(BasStationService.class); |
| | | BasDevpService basDevpService = mock(BasDevpService.class); |
| | |
| | | verify(basStationService, never()).list(any(Wrapper.class)); |
| | | } |
| | | |
| | | @Test |
| | | void toRuntimeItemExposesRunBlockForRoutePressure() { |
| | | StationProtocol protocol = new StationProtocol(); |
| | | protocol.setStationId(101); |
| | | protocol.setAutoing(true); |
| | | protocol.setLoading(false); |
| | | protocol.setTaskNo(0); |
| | | protocol.setRunBlock(true); |
| | | |
| | | AutoTuneStationRuntimeItem item = ReflectionTestUtils.invokeMethod(service, "toRuntimeItem", protocol); |
| | | |
| | | assertEquals(1, item.getRunBlock()); |
| | | } |
| | | |
| | | private BasStation station(Integer stationId, Integer outTaskLimit) { |
| | | return station(stationId, outTaskLimit, null); |
| | | } |
| New file |
| | |
| | | package com.zy.ai.service.impl; |
| | | |
| | | import com.zy.ai.domain.autotune.AutoTuneRoutePressureSnapshot; |
| | | import com.zy.ai.domain.autotune.AutoTuneHotPathSegmentItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneStationRuntimeItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTargetStationRoutePressureItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskDetailItem; |
| | | import com.zy.ai.domain.autotune.AutoTuneTaskSnapshot; |
| | | import com.zy.asrs.domain.vo.StationTaskTraceVo; |
| | | import com.zy.asrs.entity.WrkMast; |
| | | import com.zy.common.model.NavigateNode; |
| | | import com.zy.common.utils.NavigateUtils; |
| | | import com.zy.core.enums.WrkIoType; |
| | | import com.zy.core.enums.WrkStsType; |
| | | import com.zy.core.trace.StationTaskTraceRegistry; |
| | | import com.zy.core.utils.station.StationOutboundDecisionSupport; |
| | | import com.zy.system.service.ConfigService; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.never; |
| | | import static org.mockito.Mockito.verify; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | class RoutePressureSnapshotServiceImplTest { |
| | | |
| | | private RoutePressureSnapshotServiceImpl service; |
| | | private StationTaskTraceRegistry traceRegistry; |
| | | private NavigateUtils navigateUtils; |
| | | private StationOutboundDecisionSupport stationOutboundDecisionSupport; |
| | | private ConfigService configService; |
| | | |
| | | @BeforeEach |
| | | void setUp() { |
| | | service = new RoutePressureSnapshotServiceImpl(); |
| | | traceRegistry = mock(StationTaskTraceRegistry.class); |
| | | navigateUtils = mock(NavigateUtils.class); |
| | | stationOutboundDecisionSupport = mock(StationOutboundDecisionSupport.class); |
| | | configService = mock(ConfigService.class); |
| | | ReflectionTestUtils.setField(service, "stationTaskTraceRegistry", traceRegistry); |
| | | ReflectionTestUtils.setField(service, "navigateUtils", navigateUtils); |
| | | ReflectionTestUtils.setField(service, "stationOutboundDecisionSupport", stationOutboundDecisionSupport); |
| | | ReflectionTestUtils.setField(service, "configService", configService); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotPrefixesCurrentStationBeforePendingTracePath() { |
| | | StationTaskTraceVo trace = new StationTaskTraceVo(); |
| | | trace.setTaskNo(190263); |
| | | trace.setCurrentStationId(196); |
| | | trace.setPendingStationIds(Arrays.asList(186, 101)); |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList(trace)); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(outboundTask(190263, 196, 101, 8)), |
| | | taskSnapshotWithBlockedTask(190263), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | assertEquals(1, snapshot.getAnalyzedTaskCount()); |
| | | assertEquals(1, snapshot.getTracePathCount()); |
| | | assertEquals(0, snapshot.getEstimatedPathCount()); |
| | | assertEquals(0, snapshot.getPathErrorCount()); |
| | | assertEquals("trace_pending", snapshot.getTaskRouteSamples().get(0).getPathSource()); |
| | | assertEquals(Arrays.asList(196, 186, 101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); |
| | | verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotMarksRunningStationTaskMissingWhenTraceAbsent() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.emptyList()); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(outboundTask(190263, 196, 101, 8)), |
| | | emptyTaskSnapshot(), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | assertEquals(1, snapshot.getAnalyzedTaskCount()); |
| | | assertEquals(0, snapshot.getTracePathCount()); |
| | | assertEquals(0, snapshot.getEstimatedPathCount()); |
| | | assertEquals(1, snapshot.getPathErrorCount()); |
| | | assertEquals("missing", snapshot.getTaskRouteSamples().get(0).getPathSource()); |
| | | assertEquals("missing station trace for running station task", |
| | | snapshot.getTaskRouteSamples().get(0).getPathError()); |
| | | verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotEstimatesPathFromSourceStaNoToTargetStaNoForNonRunningTask() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.emptyList()); |
| | | when(stationOutboundDecisionSupport.resolveOutboundPathLenFactor(any())).thenReturn(0.0d); |
| | | when(navigateUtils.calcOptimalPathByStationId(eq(196), eq(101), eq(190263), eq(0.0d))) |
| | | .thenReturn(navigateNodes(196, 186, 101)); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(nonRunningOutboundTask(190263, 196, 101, 8)), |
| | | emptyTaskSnapshot(), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | assertEquals(1, snapshot.getAnalyzedTaskCount()); |
| | | assertEquals(0, snapshot.getTracePathCount()); |
| | | assertEquals(1, snapshot.getEstimatedPathCount()); |
| | | assertEquals(0, snapshot.getPathErrorCount()); |
| | | assertEquals("estimated", snapshot.getTaskRouteSamples().get(0).getPathSource()); |
| | | assertEquals(Arrays.asList(196, 186, 101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); |
| | | verify(navigateUtils).calcOptimalPathByStationId(196, 101, 190263, 0.0d); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotEstimatesNewOutboundTaskEvenWhenResidualTraceExists() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( |
| | | trace(190263, 196, 888, 101) |
| | | )); |
| | | when(stationOutboundDecisionSupport.resolveOutboundPathLenFactor(any())).thenReturn(0.0d); |
| | | when(navigateUtils.calcOptimalPathByStationId(eq(196), eq(101), eq(190263), eq(0.0d))) |
| | | .thenReturn(navigateNodes(196, 186, 101)); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(nonRunningOutboundTask(190263, 196, 101, 8)), |
| | | emptyTaskSnapshot(), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | assertEquals(1, snapshot.getAnalyzedTaskCount()); |
| | | assertEquals(0, snapshot.getTracePathCount()); |
| | | assertEquals(1, snapshot.getEstimatedPathCount()); |
| | | assertEquals(0, snapshot.getPathErrorCount()); |
| | | assertEquals("estimated", snapshot.getTaskRouteSamples().get(0).getPathSource()); |
| | | assertEquals(Arrays.asList(196, 186, 101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); |
| | | verify(navigateUtils).calcOptimalPathByStationId(196, 101, 190263, 0.0d); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotMarksRunningStationTaskMissingWhenTraceHasNoUsableStationPath() { |
| | | StationTaskTraceVo trace = new StationTaskTraceVo(); |
| | | trace.setTaskNo(190263); |
| | | trace.setPendingStationIds(Collections.emptyList()); |
| | | trace.setLatestIssuedSegmentPath(Collections.emptyList()); |
| | | trace.setIssuedStationIds(Collections.emptyList()); |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList(trace)); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(outboundTask(190263, 196, 101, 8)), |
| | | emptyTaskSnapshot(), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | assertEquals(1, snapshot.getAnalyzedTaskCount()); |
| | | assertEquals(0, snapshot.getTracePathCount()); |
| | | assertEquals(0, snapshot.getEstimatedPathCount()); |
| | | assertEquals(1, snapshot.getPathErrorCount()); |
| | | assertEquals("missing", snapshot.getTaskRouteSamples().get(0).getPathSource()); |
| | | assertEquals("missing usable station trace path for running station task", |
| | | snapshot.getTaskRouteSamples().get(0).getPathError()); |
| | | verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotDoesNotReuseEstimatedPathAcrossDifferentTaskNos() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.emptyList()); |
| | | when(stationOutboundDecisionSupport.resolveOutboundPathLenFactor(any())).thenReturn(0.0d); |
| | | when(navigateUtils.calcOptimalPathByStationId(eq(196), eq(101), eq(190263), eq(0.0d))) |
| | | .thenReturn(navigateNodes(196, 186, 101)); |
| | | when(navigateUtils.calcOptimalPathByStationId(eq(196), eq(101), eq(190264), eq(0.0d))) |
| | | .thenReturn(navigateNodes(196, 194, 101)); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Arrays.asList( |
| | | nonRunningOutboundTask(190263, 196, 101, 8), |
| | | nonRunningOutboundTask(190264, 196, 101, 9) |
| | | ), |
| | | emptyTaskSnapshot(), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | assertEquals(2, snapshot.getEstimatedPathCount()); |
| | | assertEquals(190263, snapshot.getTaskRouteSamples().get(0).getWrkNo()); |
| | | assertEquals(Arrays.asList(196, 186, 101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); |
| | | assertEquals(190264, snapshot.getTaskRouteSamples().get(1).getWrkNo()); |
| | | assertEquals(Arrays.asList(196, 194, 101), snapshot.getTaskRouteSamples().get(1).getPathStationIds()); |
| | | verify(navigateUtils).calcOptimalPathByStationId(196, 101, 190263, 0.0d); |
| | | verify(navigateUtils).calcOptimalPathByStationId(196, 101, 190264, 0.0d); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotMarksMissingWhenSourceOrTargetStationIsMissing() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.emptyList()); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(nonRunningOutboundTask(190263, null, 101, 8)), |
| | | emptyTaskSnapshot(), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | assertEquals(1, snapshot.getAnalyzedTaskCount()); |
| | | assertEquals(0, snapshot.getTracePathCount()); |
| | | assertEquals(0, snapshot.getEstimatedPathCount()); |
| | | assertEquals(1, snapshot.getPathErrorCount()); |
| | | assertEquals("missing", snapshot.getTaskRouteSamples().get(0).getPathSource()); |
| | | assertTrue(snapshot.getTaskRouteSamples().get(0).getPathError().contains("missing sourceStaNo or staNo")); |
| | | verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotUsesCurrentStationOnlyWhenCurrentIsTargetAndPendingIsEmpty() { |
| | | StationTaskTraceVo trace = new StationTaskTraceVo(); |
| | | trace.setTaskNo(190263); |
| | | trace.setCurrentStationId(101); |
| | | trace.setPendingStationIds(Collections.emptyList()); |
| | | trace.setLatestIssuedSegmentPath(Arrays.asList(196, 186, 101)); |
| | | trace.setIssuedStationIds(Arrays.asList(196, 186, 101)); |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList(trace)); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(outboundTask(190263, 196, 101, 8)), |
| | | taskSnapshotWithBlockedTask(190263), |
| | | Collections.singletonList(runtimeItem(101, 1, 1, 190263, 1)) |
| | | ); |
| | | |
| | | assertEquals(1, snapshot.getTaskRouteSamples().size()); |
| | | assertEquals("trace_pending", snapshot.getTaskRouteSamples().get(0).getPathSource()); |
| | | assertEquals(Arrays.asList(101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); |
| | | assertEquals(1, snapshot.getTaskRouteSamples().get(0).getPathLength()); |
| | | assertEquals(1, snapshot.getTracePathCount()); |
| | | assertEquals(0, snapshot.getEstimatedPathCount()); |
| | | assertEquals(0, snapshot.getPathErrorCount()); |
| | | assertTrue(snapshot.getHotPathSegments().isEmpty()); |
| | | assertTrue(snapshot.getTargetStationRoutePressure().isEmpty()); |
| | | verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotKeepsSingleStationSampleOutOfPressureAggregation() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( |
| | | trace(190263, 101) |
| | | )); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(outboundTask(190263, 101, 101, 8)), |
| | | taskSnapshotWithBlockedTask(190263), |
| | | Collections.singletonList(runtimeItem(101, 1, 1, 190263, 1)) |
| | | ); |
| | | |
| | | assertEquals(1, snapshot.getTaskRouteSamples().size()); |
| | | assertEquals(Arrays.asList(101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); |
| | | assertEquals(1, snapshot.getTaskRouteSamples().get(0).getPathLength()); |
| | | assertEquals(1, snapshot.getTracePathCount()); |
| | | assertTrue(snapshot.getHotPathSegments().isEmpty()); |
| | | assertTrue(snapshot.getTargetStationRoutePressure().isEmpty()); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotAggregatesOverlappingRouteSegmentsByPassCountAndTargets() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Arrays.asList( |
| | | trace(190263, 1, 2, 3, 4, 5), |
| | | trace(190264, 1, 2, 3, 4, 6), |
| | | trace(190265, 7, 1, 2, 3, 4) |
| | | )); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Arrays.asList( |
| | | outboundTask(190263, 1, 101, 1), |
| | | outboundTask(190264, 1, 102, 2), |
| | | outboundTask(190265, 7, 101, 3) |
| | | ), |
| | | emptyTaskSnapshot(), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | AutoTuneHotPathSegmentItem sharedSegment = findHotPathSegment(snapshot, "1-2-3-4"); |
| | | |
| | | assertNotNull(sharedSegment); |
| | | assertEquals(Arrays.asList(1, 2, 3, 4), sharedSegment.getStationIds()); |
| | | assertEquals(3, sharedSegment.getPassTaskCount()); |
| | | assertEquals(Arrays.asList(101, 102), sharedSegment.getRelatedTargetStations()); |
| | | assertEquals(Arrays.asList(190263, 190264, 190265), sharedSegment.getSampleWrkNos()); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotUsesOnlyValidRouteSamplesForSegmentPassRatio() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( |
| | | trace(190263, 1, 2, 3, 4) |
| | | )); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Arrays.asList( |
| | | outboundTask(190263, 1, 101, 1), |
| | | outboundTask(190264, 5, 101, 2) |
| | | ), |
| | | emptyTaskSnapshot(), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | AutoTuneHotPathSegmentItem segment = findHotPathSegment(snapshot, "1-2-3-4"); |
| | | |
| | | assertNotNull(segment); |
| | | assertEquals(2, snapshot.getAnalyzedTaskCount()); |
| | | assertEquals(1, snapshot.getPathErrorCount()); |
| | | assertEquals(1, segment.getPassTaskCount()); |
| | | assertEquals(100, segment.getPressureFactors().get("passRatio")); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotExposesRoutePressureRuleSnapshotFromSysConfig() { |
| | | when(configService.getConfigValue(eq("aiAutoTuneRoutePressureMediumPercent"), eq("50"))).thenReturn("40"); |
| | | when(configService.getConfigValue(eq("aiAutoTuneRoutePressureHighPercent"), eq("75"))).thenReturn("70"); |
| | | when(configService.getConfigValue(eq("aiAutoTuneRoutePressurePassWeightPercent"), eq("35"))).thenReturn("60"); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.emptyList(), |
| | | emptyTaskSnapshot(), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | assertNotNull(snapshot.getRoutePressureRuleSnapshot()); |
| | | assertEquals(4, snapshot.getRoutePressureRuleSnapshot().getSegmentWindowSize()); |
| | | assertEquals(40, snapshot.getRoutePressureRuleSnapshot().getMediumPercent()); |
| | | assertEquals(70, snapshot.getRoutePressureRuleSnapshot().getHighPercent()); |
| | | assertEquals(60, snapshot.getRoutePressureRuleSnapshot().getPassWeightPercent()); |
| | | assertEquals(25, snapshot.getRoutePressureRuleSnapshot().getOccupiedWeightPercent()); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotTreatsRunBlockAsWeightedFactorInsteadOfDirectHighPressure() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( |
| | | trace(190263, 1, 2, 3, 4) |
| | | )); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(outboundTask(190263, 1, 101, 1)), |
| | | emptyTaskSnapshot(), |
| | | Collections.singletonList(runtimeItem(2, 1, 0, 0, 1)) |
| | | ); |
| | | |
| | | AutoTuneHotPathSegmentItem segment = findHotPathSegment(snapshot, "1-2-3-4"); |
| | | |
| | | assertNotNull(segment); |
| | | assertEquals("low", segment.getPressureLevel()); |
| | | assertEquals(38, segment.getPressureScore()); |
| | | assertEquals(25, segment.getPressureFactors().get("runBlockRatio")); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotUsesBlockedTasksAsIncreaseCandidateWhenPressureIsNotHigh() { |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( |
| | | trace(190263, 1, 2, 101) |
| | | )); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | Collections.singletonList(outboundTask(190263, 1, 101, 1)), |
| | | taskSnapshotWithBlockedTask(190263), |
| | | runtimeItems() |
| | | ); |
| | | |
| | | AutoTuneTargetStationRoutePressureItem targetPressure = findTargetPressure(snapshot, 101); |
| | | |
| | | assertNotNull(targetPressure); |
| | | assertEquals("medium", targetPressure.getPressureLevel()); |
| | | assertEquals(55, targetPressure.getPressureScore()); |
| | | assertEquals("increase_candidate", targetPressure.getRecommendedDirection()); |
| | | assertTrue(targetPressure.getReason().contains("路径事实")); |
| | | assertTrue(targetPressure.getReason().contains("blockedTaskCount=1")); |
| | | } |
| | | |
| | | @Test |
| | | void buildSnapshotMarksHighPressureWithPercentageScoreAndFactEvidence() { |
| | | List<WrkMast> activeTasks = new ArrayList<>(); |
| | | List<StationTaskTraceVo> traces = new ArrayList<>(); |
| | | for (int index = 0; index < 10; index++) { |
| | | int wrkNo = 190300 + index; |
| | | activeTasks.add(outboundTask(wrkNo, 1, 101, index)); |
| | | traces.add(trace(wrkNo, 1, 2, 3, 4)); |
| | | } |
| | | when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(traces); |
| | | |
| | | AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( |
| | | activeTasks, |
| | | emptyTaskSnapshot(), |
| | | Arrays.asList( |
| | | runtimeItem(1, 0, 1, 190300, 1), |
| | | runtimeItem(2, 0, 1, 190301, 1), |
| | | runtimeItem(3, 0, 1, 190302, 1), |
| | | runtimeItem(4, 0, 1, 190303, 1) |
| | | ) |
| | | ); |
| | | |
| | | AutoTuneTargetStationRoutePressureItem targetPressure = findTargetPressure(snapshot, 101); |
| | | |
| | | assertNotNull(targetPressure); |
| | | assertEquals("high", targetPressure.getPressureLevel()); |
| | | assertEquals("decrease_candidate", targetPressure.getRecommendedDirection()); |
| | | assertEquals("decrease_candidate", targetPressure.getHeuristicDirection()); |
| | | assertTrue(targetPressure.getPressureScore() >= 75); |
| | | assertTrue(targetPressure.getRecommendedTargets().contains("station/101/outTaskLimit")); |
| | | assertTrue(targetPressure.getReason().contains("路径事实")); |
| | | assertTrue(targetPressure.getReason().contains("blockedTaskCount=0")); |
| | | assertTrue(targetPressure.getReason().contains("routeTaskCount=10")); |
| | | assertTrue(targetPressure.getPressureFactors().containsKey("highestSegmentScore")); |
| | | } |
| | | |
| | | private WrkMast outboundTask(Integer wrkNo, Integer sourceStaNo, Integer targetStaNo, Integer batchSeq) { |
| | | return outboundTask(wrkNo, sourceStaNo, targetStaNo, batchSeq, WrkStsType.STATION_RUN.sts); |
| | | } |
| | | |
| | | private WrkMast nonRunningOutboundTask(Integer wrkNo, |
| | | Integer sourceStaNo, |
| | | Integer targetStaNo, |
| | | Integer batchSeq) { |
| | | return outboundTask(wrkNo, sourceStaNo, targetStaNo, batchSeq, WrkStsType.NEW_OUTBOUND.sts); |
| | | } |
| | | |
| | | private WrkMast outboundTask(Integer wrkNo, |
| | | Integer sourceStaNo, |
| | | Integer targetStaNo, |
| | | Integer batchSeq, |
| | | long wrkSts) { |
| | | WrkMast task = new WrkMast(); |
| | | task.setWrkNo(wrkNo); |
| | | task.setIoType(WrkIoType.OUT.id); |
| | | task.setWrkSts(wrkSts); |
| | | task.setBatch("MANUAL_OUT_20260427181201"); |
| | | task.setBatchSeq(batchSeq); |
| | | task.setSourceStaNo(sourceStaNo); |
| | | task.setStaNo(targetStaNo); |
| | | return task; |
| | | } |
| | | |
| | | private AutoTuneTaskSnapshot taskSnapshotWithBlockedTask(Integer wrkNo) { |
| | | AutoTuneTaskDetailItem blockedTask = new AutoTuneTaskDetailItem(); |
| | | blockedTask.setWrkNo(wrkNo); |
| | | AutoTuneTaskSnapshot snapshot = new AutoTuneTaskSnapshot(); |
| | | snapshot.setStationLimitBlockedTasks(Collections.singletonList(blockedTask)); |
| | | return snapshot; |
| | | } |
| | | |
| | | private AutoTuneTaskSnapshot emptyTaskSnapshot() { |
| | | AutoTuneTaskSnapshot snapshot = new AutoTuneTaskSnapshot(); |
| | | snapshot.setStationLimitBlockedTasks(Collections.emptyList()); |
| | | return snapshot; |
| | | } |
| | | |
| | | private List<AutoTuneStationRuntimeItem> runtimeItems() { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | private AutoTuneStationRuntimeItem runtimeItem(Integer stationId, |
| | | Integer autoing, |
| | | Integer loading, |
| | | Integer taskNo, |
| | | Integer runBlock) { |
| | | AutoTuneStationRuntimeItem runtimeItem = new AutoTuneStationRuntimeItem(); |
| | | runtimeItem.setStationId(stationId); |
| | | runtimeItem.setAutoing(autoing); |
| | | runtimeItem.setLoading(loading); |
| | | runtimeItem.setTaskNo(taskNo); |
| | | runtimeItem.setRunBlock(runBlock); |
| | | return runtimeItem; |
| | | } |
| | | |
| | | private StationTaskTraceVo trace(Integer wrkNo, Integer... pendingStationIds) { |
| | | StationTaskTraceVo trace = new StationTaskTraceVo(); |
| | | trace.setTaskNo(wrkNo); |
| | | trace.setPendingStationIds(Arrays.asList(pendingStationIds)); |
| | | return trace; |
| | | } |
| | | |
| | | private AutoTuneHotPathSegmentItem findHotPathSegment(AutoTuneRoutePressureSnapshot snapshot, String segmentKey) { |
| | | return snapshot.getHotPathSegments().stream() |
| | | .filter(segment -> segmentKey.equals(segment.getSegmentKey())) |
| | | .findFirst() |
| | | .orElse(null); |
| | | } |
| | | |
| | | private AutoTuneTargetStationRoutePressureItem findTargetPressure(AutoTuneRoutePressureSnapshot snapshot, |
| | | Integer targetStationId) { |
| | | return snapshot.getTargetStationRoutePressure().stream() |
| | | .filter(item -> targetStationId.equals(item.getTargetStationId())) |
| | | .findFirst() |
| | | .orElse(null); |
| | | } |
| | | |
| | | private List<NavigateNode> navigateNodes(Integer... stationIds) { |
| | | return Arrays.stream(stationIds) |
| | | .map(this::navigateNode) |
| | | .toList(); |
| | | } |
| | | |
| | | private NavigateNode navigateNode(Integer stationId) { |
| | | NavigateNode navigateNode = new NavigateNode(); |
| | | navigateNode.setStationId(stationId); |
| | | return navigateNode; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.ai.utils; |
| | | |
| | | import com.zy.ai.entity.AiAutoTuneChange; |
| | | import com.zy.ai.entity.AiAutoTuneJob; |
| | | import com.zy.ai.entity.AiAutoTuneMcpCall; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | |
| | | class AutoTuneWriteBehaviorUtilsTest { |
| | | |
| | | @Test |
| | | void resolveMcpCallDistinguishesDryRunAndAnalysisOnlyApply() { |
| | | AiAutoTuneMcpCall dryRunCall = new AiAutoTuneMcpCall(); |
| | | dryRunCall.setToolName("wcs_local_dispatch_apply_auto_tune_changes"); |
| | | dryRunCall.setDryRun(1); |
| | | |
| | | AiAutoTuneMcpCall analysisOnlyCall = new AiAutoTuneMcpCall(); |
| | | analysisOnlyCall.setToolName("wcs_local_dispatch_apply_auto_tune_changes"); |
| | | analysisOnlyCall.setDryRun(0); |
| | | analysisOnlyCall.setStatus("success"); |
| | | analysisOnlyCall.setResponseJson("{\"analysisOnly\":true,\"noApply\":true,\"summary\":\"仅分析模式禁止实际应用\"}"); |
| | | |
| | | assertEquals("dry_run", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(dryRunCall)); |
| | | assertEquals("analysis_only", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(analysisOnlyCall)); |
| | | } |
| | | |
| | | @Test |
| | | void resolveChangeDistinguishesNoChangeAndRejectedAnalysisOnly() { |
| | | AiAutoTuneChange noChange = new AiAutoTuneChange(); |
| | | noChange.setResultStatus("no_change"); |
| | | |
| | | AiAutoTuneChange analysisOnly = new AiAutoTuneChange(); |
| | | analysisOnly.setResultStatus("rejected"); |
| | | analysisOnly.setRejectReason("仅分析模式禁止实际应用/回滚,未修改运行参数"); |
| | | |
| | | assertEquals("no_change", AutoTuneWriteBehaviorUtils.resolveChangeWriteBehavior(noChange)); |
| | | assertEquals("analysis_only", AutoTuneWriteBehaviorUtils.resolveChangeWriteBehavior(analysisOnly)); |
| | | } |
| | | |
| | | @Test |
| | | void resolveMcpCallClassifiesRollbackOnlyWhenSuccessfulChangeExists() { |
| | | AiAutoTuneMcpCall countSuccessCall = rollbackCall("success"); |
| | | countSuccessCall.setSuccessCount(1); |
| | | |
| | | AiAutoTuneMcpCall responseSuccessCall = rollbackCall("failed"); |
| | | responseSuccessCall.setResponseJson("{\"changes\":[{\"resultStatus\":\"success\"}]}"); |
| | | |
| | | assertEquals("rollback", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(countSuccessCall)); |
| | | assertEquals("rollback", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(responseSuccessCall)); |
| | | } |
| | | |
| | | @Test |
| | | void resolveMcpCallClassifiesRejectedAndFailedRollbackWithoutSuccessfulChange() { |
| | | AiAutoTuneMcpCall rejectedCall = rollbackCall("rejected"); |
| | | rejectedCall.setResponseJson("{\"success\":false,\"rejectCount\":1}"); |
| | | |
| | | AiAutoTuneMcpCall failedCall = rollbackCall("failed"); |
| | | failedCall.setErrorMessage("rollback write failed"); |
| | | |
| | | assertEquals("rejected", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(rejectedCall)); |
| | | assertEquals("failed", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(failedCall)); |
| | | } |
| | | |
| | | @Test |
| | | void resolveMcpCallPrefersAnalysisOnlyAndNoChangeForRollback() { |
| | | AiAutoTuneMcpCall analysisOnlyCall = rollbackCall("rejected"); |
| | | analysisOnlyCall.setResponseJson("{\"analysisOnly\":true,\"noApply\":true}"); |
| | | |
| | | AiAutoTuneMcpCall noChangeCall = rollbackCall("success"); |
| | | noChangeCall.setResponseJson("{\"success\":true,\"successCount\":0,\"rejectCount\":0}"); |
| | | |
| | | assertEquals("analysis_only", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(analysisOnlyCall)); |
| | | assertEquals("no_change", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(noChangeCall)); |
| | | } |
| | | |
| | | @Test |
| | | void resolveMcpCallDoesNotTreatAnalysisOnlyFalseTextAsAnalysisOnly() { |
| | | AiAutoTuneMcpCall applyCall = new AiAutoTuneMcpCall(); |
| | | applyCall.setToolName("wcs_local_dispatch_apply_auto_tune_changes"); |
| | | applyCall.setDryRun(0); |
| | | applyCall.setStatus("success"); |
| | | applyCall.setSuccessCount(1); |
| | | applyCall.setResponseJson("{\"success\":true,\"successCount\":1,\"summary\":\"analysisOnly=false, allowApply=true\"}"); |
| | | |
| | | assertEquals("apply", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(applyCall)); |
| | | } |
| | | |
| | | @Test |
| | | void resolveMcpCallUsesResponseChangeDetailsBeforeSuccessCountForApply() { |
| | | AiAutoTuneMcpCall noChangeCall = applyCall(0, "success"); |
| | | noChangeCall.setSuccessCount(1); |
| | | noChangeCall.setResponseJson("{\"success\":true,\"successCount\":1," |
| | | + "\"changes\":[{\"resultStatus\":\"no_change\"},{\"resultStatus\":\"no_change\"}]}"); |
| | | |
| | | AiAutoTuneMcpCall mixedSuccessCall = applyCall(0, "success"); |
| | | mixedSuccessCall.setSuccessCount(0); |
| | | mixedSuccessCall.setResponseJson("{\"success\":true,\"successCount\":0," |
| | | + "\"changes\":[{\"resultStatus\":\"no_change\"},{\"resultStatus\":\"success\"}]}"); |
| | | |
| | | assertEquals("no_change", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(noChangeCall)); |
| | | assertEquals("apply", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(mixedSuccessCall)); |
| | | } |
| | | |
| | | @Test |
| | | void resolveMcpCallPrefersFailedAndRejectedOverDryRunAndNoChange() { |
| | | AiAutoTuneMcpCall failedDryRunCall = applyCall(1, "failed"); |
| | | |
| | | AiAutoTuneMcpCall rejectedNoChangeCall = applyCall(0, "rejected"); |
| | | rejectedNoChangeCall.setResponseJson("{\"success\":true,\"successCount\":0,\"rejectCount\":0}"); |
| | | |
| | | assertEquals("failed", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(failedDryRunCall)); |
| | | assertEquals("rejected", AutoTuneWriteBehaviorUtils.resolveMcpWriteBehavior(rejectedNoChangeCall)); |
| | | } |
| | | |
| | | @Test |
| | | void resolveChangeUsesOwnerTriggerTypeForRollbackSuccess() { |
| | | AiAutoTuneChange successChange = new AiAutoTuneChange(); |
| | | successChange.setResultStatus("success"); |
| | | |
| | | assertEquals("apply", AutoTuneWriteBehaviorUtils.resolveChangeWriteBehavior(successChange)); |
| | | assertEquals("rollback", AutoTuneWriteBehaviorUtils.resolveChangeWriteBehavior(successChange, "rollback")); |
| | | assertEquals("apply", AutoTuneWriteBehaviorUtils.resolveChangeWriteBehavior(successChange, "manual")); |
| | | } |
| | | |
| | | @Test |
| | | void resolveChangeClassifiesRejectedAndFailed() { |
| | | AiAutoTuneChange rejectedChange = new AiAutoTuneChange(); |
| | | rejectedChange.setResultStatus("rejected"); |
| | | rejectedChange.setRejectReason("超出允许范围"); |
| | | |
| | | AiAutoTuneChange failedChange = new AiAutoTuneChange(); |
| | | failedChange.setResultStatus("failed"); |
| | | |
| | | assertEquals("rejected", AutoTuneWriteBehaviorUtils.resolveChangeWriteBehavior(rejectedChange)); |
| | | assertEquals("failed", AutoTuneWriteBehaviorUtils.resolveChangeWriteBehavior(failedChange)); |
| | | } |
| | | |
| | | @Test |
| | | void writeBehaviorLabelIncludesRejectedAndFailed() { |
| | | assertEquals("已拒绝", AutoTuneWriteBehaviorUtils.writeBehaviorLabel("rejected")); |
| | | assertEquals("失败", AutoTuneWriteBehaviorUtils.writeBehaviorLabel("failed")); |
| | | } |
| | | |
| | | @Test |
| | | void resolveJobPrefersReadOnlyWhenOnlySnapshotToolsWereCalled() { |
| | | AiAutoTuneJob job = new AiAutoTuneJob(); |
| | | job.setStatus("success"); |
| | | |
| | | Map<String, Object> snapshotCall = new LinkedHashMap<>(); |
| | | snapshotCall.put("writeBehavior", "read_only"); |
| | | |
| | | String writeBehavior = AutoTuneWriteBehaviorUtils.resolveJobWriteBehavior( |
| | | job, |
| | | Collections.singletonList(snapshotCall), |
| | | List.of() |
| | | ); |
| | | |
| | | assertEquals("read_only", writeBehavior); |
| | | } |
| | | |
| | | @Test |
| | | void resolveJobPrefersFailedAndRejectedOverDryRunNoChangeAndReadOnly() { |
| | | AiAutoTuneJob failedDryRunJob = new AiAutoTuneJob(); |
| | | failedDryRunJob.setStatus("failed"); |
| | | |
| | | Map<String, Object> dryRunCall = new LinkedHashMap<>(); |
| | | dryRunCall.put("writeBehavior", "dry_run"); |
| | | |
| | | AiAutoTuneJob rejectedNoChangeJob = new AiAutoTuneJob(); |
| | | rejectedNoChangeJob.setStatus("rejected"); |
| | | |
| | | Map<String, Object> noChange = new LinkedHashMap<>(); |
| | | noChange.put("writeBehavior", "no_change"); |
| | | |
| | | AiAutoTuneJob failedReadOnlyJob = new AiAutoTuneJob(); |
| | | failedReadOnlyJob.setStatus("failed"); |
| | | |
| | | Map<String, Object> readOnlyCall = new LinkedHashMap<>(); |
| | | readOnlyCall.put("writeBehavior", "read_only"); |
| | | |
| | | assertEquals("failed", AutoTuneWriteBehaviorUtils.resolveJobWriteBehavior( |
| | | failedDryRunJob, |
| | | List.of(dryRunCall), |
| | | List.of() |
| | | )); |
| | | assertEquals("rejected", AutoTuneWriteBehaviorUtils.resolveJobWriteBehavior( |
| | | rejectedNoChangeJob, |
| | | List.of(), |
| | | List.of(noChange) |
| | | )); |
| | | assertEquals("failed", AutoTuneWriteBehaviorUtils.resolveJobWriteBehavior( |
| | | failedReadOnlyJob, |
| | | List.of(readOnlyCall), |
| | | List.of() |
| | | )); |
| | | } |
| | | |
| | | @Test |
| | | void resolveJobClassifiesDirectSuccessfulRollbackWithoutMcpCalls() { |
| | | AiAutoTuneJob job = new AiAutoTuneJob(); |
| | | job.setTriggerType("rollback"); |
| | | job.setStatus("success"); |
| | | job.setSuccessCount(1); |
| | | |
| | | String writeBehavior = AutoTuneWriteBehaviorUtils.resolveJobWriteBehavior( |
| | | job, |
| | | List.of(), |
| | | List.of() |
| | | ); |
| | | |
| | | assertEquals("rollback", writeBehavior); |
| | | } |
| | | |
| | | @Test |
| | | void resolveJobPreservesRollbackWhenSomeRollbackChangesFailed() { |
| | | AiAutoTuneJob job = new AiAutoTuneJob(); |
| | | job.setTriggerType("rollback"); |
| | | job.setStatus("partial_success"); |
| | | |
| | | Map<String, Object> successChange = new LinkedHashMap<>(); |
| | | successChange.put("writeBehavior", "rollback"); |
| | | successChange.put("resultStatus", "success"); |
| | | |
| | | Map<String, Object> failedChange = new LinkedHashMap<>(); |
| | | failedChange.put("writeBehavior", "failed"); |
| | | failedChange.put("resultStatus", "failed"); |
| | | |
| | | String writeBehavior = AutoTuneWriteBehaviorUtils.resolveJobWriteBehavior( |
| | | job, |
| | | List.of(), |
| | | List.of(successChange, failedChange) |
| | | ); |
| | | |
| | | assertEquals("rollback", writeBehavior); |
| | | } |
| | | |
| | | private AiAutoTuneMcpCall applyCall(Integer dryRun, String status) { |
| | | AiAutoTuneMcpCall mcpCall = new AiAutoTuneMcpCall(); |
| | | mcpCall.setToolName("wcs_local_dispatch_apply_auto_tune_changes"); |
| | | mcpCall.setDryRun(dryRun); |
| | | mcpCall.setStatus(status); |
| | | return mcpCall; |
| | | } |
| | | |
| | | private AiAutoTuneMcpCall rollbackCall(String status) { |
| | | AiAutoTuneMcpCall mcpCall = new AiAutoTuneMcpCall(); |
| | | mcpCall.setToolName("wcs_local_dispatch_revert_last_auto_tune_job"); |
| | | mcpCall.setStatus(status); |
| | | mcpCall.setSuccessCount(0); |
| | | return mcpCall; |
| | | } |
| | | } |