From dc3f9cc91759823ce59486f19b138be4b296a0f1 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期二, 28 四月 2026 09:43:28 +0800
Subject: [PATCH] #

---
 /dev/null                                                                 |    9 -
 src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java         |   24 +-
 src/test/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImplTest.java |   20 ++
 src/main/java/com/zy/ai/utils/AiPromptUtils.java                          |    9 
 src/main/resources/sql/20260428_ai_auto_tune_consolidated.sql             |  256 ++++++++++++++++++++++++++++
 src/main/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImpl.java     |   23 --
 src/main/java/com/zy/ai/domain/autotune/AutoTuneRuleDefinition.java       |  149 ++++++++++++++--
 src/main/webapp/views/ai/auto_tune.html                                   |   25 ++
 8 files changed, 450 insertions(+), 65 deletions(-)

diff --git a/src/main/java/com/zy/ai/domain/autotune/AutoTuneRuleDefinition.java b/src/main/java/com/zy/ai/domain/autotune/AutoTuneRuleDefinition.java
index bff033e..1b3cd79 100644
--- a/src/main/java/com/zy/ai/domain/autotune/AutoTuneRuleDefinition.java
+++ b/src/main/java/com/zy/ai/domain/autotune/AutoTuneRuleDefinition.java
@@ -6,6 +6,12 @@
 
 public final class AutoTuneRuleDefinition {
 
+    private static final String DEFAULT_RULE_NOTE = "鍗曟璋冩暣骞呭害涓嶈兘瓒呰繃 maxStep銆�";
+    private static final String STATION_OUT_TASK_LIMIT_DYNAMIC_MAX_SOURCE =
+            "currentParameterSnapshot.stationOutBufferCapacities[targetId]";
+    private static final String STATION_OUT_TASK_LIMIT_NOTE =
+            "鍗曟璋冩暣骞呭害涓嶈兘瓒呰繃 maxStep锛涘澶ф椂涓嶅緱瓒呰繃瀵瑰簲绔欑偣 outBufferCapacity銆�";
+
     private static final Map<String, Rule> RULE_MAP = buildRuleMap();
 
     private AutoTuneRuleDefinition() {
@@ -24,27 +30,58 @@
 
     private static Map<String, Rule> buildRuleMap() {
         LinkedHashMap<String, Rule> ruleMap = new LinkedHashMap<>();
-        add(ruleMap, AutoTuneTargetType.SYS_CONFIG, "aiAutoTuneIntervalMinutes", 5, 60, 5, 30, false);
-        add(ruleMap, AutoTuneTargetType.SYS_CONFIG, "conveyorStationTaskLimit", 5, 200, 5, 20, false);
-        add(ruleMap, AutoTuneTargetType.SYS_CONFIG, "crnOutBatchRunningLimit", 1, 20, 3, 20, false);
-        add(ruleMap, AutoTuneTargetType.STATION, "outTaskLimit", 0, null, 3, 10, true);
-        add(ruleMap, AutoTuneTargetType.CRN, "maxOutTask", 0, 10, 3, 10, false);
-        add(ruleMap, AutoTuneTargetType.CRN, "maxInTask", 0, 10, 3, 10, false);
-        add(ruleMap, AutoTuneTargetType.DUAL_CRN, "maxOutTask", 0, 10, 3, 10, false);
-        add(ruleMap, AutoTuneTargetType.DUAL_CRN, "maxInTask", 0, 10, 3, 10, false);
+        add(ruleMap, rule(AutoTuneTargetType.SYS_CONFIG, "aiAutoTuneIntervalMinutes")
+                .minValue(5)
+                .maxValue(60)
+                .maxStep(5)
+                .cooldownMinutes(30));
+        add(ruleMap, rule(AutoTuneTargetType.SYS_CONFIG, "conveyorStationTaskLimit")
+                .minValue(5)
+                .maxValue(200)
+                .maxStep(5)
+                .cooldownMinutes(20));
+        add(ruleMap, rule(AutoTuneTargetType.SYS_CONFIG, "crnOutBatchRunningLimit")
+                .minValue(1)
+                .maxValue(20)
+                .maxStep(3)
+                .cooldownMinutes(20));
+        add(ruleMap, rule(AutoTuneTargetType.STATION, "outTaskLimit")
+                .minValue(0)
+                .maxStep(3)
+                .cooldownMinutes(10)
+                .dynamicMaxSource(STATION_OUT_TASK_LIMIT_DYNAMIC_MAX_SOURCE)
+                .note(STATION_OUT_TASK_LIMIT_NOTE));
+        add(ruleMap, rule(AutoTuneTargetType.CRN, "maxOutTask")
+                .minValue(0)
+                .maxValue(10)
+                .maxStep(3)
+                .cooldownMinutes(10));
+        add(ruleMap, rule(AutoTuneTargetType.CRN, "maxInTask")
+                .minValue(0)
+                .maxValue(10)
+                .maxStep(3)
+                .cooldownMinutes(10));
+        add(ruleMap, rule(AutoTuneTargetType.DUAL_CRN, "maxOutTask")
+                .minValue(0)
+                .maxValue(10)
+                .maxStep(3)
+                .cooldownMinutes(10));
+        add(ruleMap, rule(AutoTuneTargetType.DUAL_CRN, "maxInTask")
+                .minValue(0)
+                .maxValue(10)
+                .maxStep(3)
+                .cooldownMinutes(10));
+
         return Collections.unmodifiableMap(ruleMap);
     }
 
-    private static void add(LinkedHashMap<String, Rule> ruleMap,
-                            AutoTuneTargetType targetType,
-                            String targetKey,
-                            Integer minValue,
-                            Integer maxValue,
-                            int maxStep,
-                            int cooldownMinutes,
-                            boolean dynamicMaxValue) {
-        Rule rule = new Rule(targetType, targetKey, minValue, maxValue, maxStep, cooldownMinutes, dynamicMaxValue);
-        ruleMap.put(buildKey(targetType.getCode(), targetKey), rule);
+    private static RuleSpec rule(AutoTuneTargetType targetType, String targetKey) {
+        return new RuleSpec(targetType, targetKey);
+    }
+
+    private static void add(LinkedHashMap<String, Rule> ruleMap, RuleSpec ruleSpec) {
+        Rule rule = ruleSpec.toRule();
+        ruleMap.put(buildKey(rule.getTargetType().getCode(), rule.getTargetKey()), rule);
     }
 
     private static String buildKey(String targetType, String targetKey) {
@@ -59,6 +96,8 @@
         private final int maxStep;
         private final int cooldownMinutes;
         private final boolean dynamicMaxValue;
+        private final String dynamicMaxSource;
+        private final String note;
 
         private Rule(AutoTuneTargetType targetType,
                      String targetKey,
@@ -66,7 +105,9 @@
                      Integer maxValue,
                      int maxStep,
                      int cooldownMinutes,
-                     boolean dynamicMaxValue) {
+                     boolean dynamicMaxValue,
+                     String dynamicMaxSource,
+                     String note) {
             this.targetType = targetType;
             this.targetKey = targetKey;
             this.minValue = minValue;
@@ -74,6 +115,8 @@
             this.maxStep = maxStep;
             this.cooldownMinutes = cooldownMinutes;
             this.dynamicMaxValue = dynamicMaxValue;
+            this.dynamicMaxSource = dynamicMaxSource;
+            this.note = note;
         }
 
         public AutoTuneTargetType getTargetType() {
@@ -103,5 +146,73 @@
         public boolean isDynamicMaxValue() {
             return dynamicMaxValue;
         }
+
+        public String getDynamicMaxSource() {
+            return dynamicMaxSource;
+        }
+
+        public String getNote() {
+            return note;
+        }
+    }
+
+    private static final class RuleSpec {
+        private final AutoTuneTargetType targetType;
+        private final String targetKey;
+        private Integer minValue;
+        private Integer maxValue;
+        private int maxStep;
+        private int cooldownMinutes;
+        private String dynamicMaxSource;
+        private String note = DEFAULT_RULE_NOTE;
+
+        private RuleSpec(AutoTuneTargetType targetType, String targetKey) {
+            this.targetType = targetType;
+            this.targetKey = targetKey;
+        }
+
+        private RuleSpec minValue(Integer minValue) {
+            this.minValue = minValue;
+            return this;
+        }
+
+        private RuleSpec maxValue(Integer maxValue) {
+            this.maxValue = maxValue;
+            return this;
+        }
+
+        private RuleSpec maxStep(int maxStep) {
+            this.maxStep = maxStep;
+            return this;
+        }
+
+        private RuleSpec cooldownMinutes(int cooldownMinutes) {
+            this.cooldownMinutes = cooldownMinutes;
+            return this;
+        }
+
+        private RuleSpec dynamicMaxSource(String dynamicMaxSource) {
+            this.dynamicMaxSource = dynamicMaxSource;
+            return this;
+        }
+
+        private RuleSpec note(String note) {
+            this.note = note;
+            return this;
+        }
+
+        private Rule toRule() {
+            return new Rule(
+                    targetType,
+                    targetKey,
+                    minValue,
+                    maxValue,
+                    maxStep,
+                    cooldownMinutes,
+                    dynamicMaxSource != null,
+                    dynamicMaxSource,
+                    note
+            );
+        }
     }
 }
diff --git a/src/main/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImpl.java b/src/main/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImpl.java
index 8b56cf2..a609520 100644
--- a/src/main/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImpl.java
+++ b/src/main/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImpl.java
@@ -6,7 +6,6 @@
 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.AutoTuneTargetType;
 import com.zy.ai.domain.autotune.AutoTuneTaskSnapshot;
 import com.zy.ai.service.AutoTuneSnapshotService;
 import com.zy.ai.service.FlowTopologySnapshotService;
@@ -105,29 +104,11 @@
             item.setMaxStep(rule.getMaxStep());
             item.setCooldownMinutes(rule.getCooldownMinutes());
             item.setDynamicMaxValue(rule.isDynamicMaxValue());
-            item.setDynamicMaxSource(resolveDynamicMaxSource(rule));
-            item.setNote(resolveRuleNote(rule));
+            item.setDynamicMaxSource(rule.getDynamicMaxSource());
+            item.setNote(rule.getNote());
             result.add(item);
         }
         return result;
-    }
-
-    private String resolveDynamicMaxSource(AutoTuneRuleDefinition.Rule rule) {
-        if (!AutoTuneTargetType.STATION.equals(rule.getTargetType())) {
-            return null;
-        }
-        if (!"outTaskLimit".equals(rule.getTargetKey())) {
-            return null;
-        }
-        return "currentParameterSnapshot.stationOutBufferCapacities[targetId]";
-    }
-
-    private String resolveRuleNote(AutoTuneRuleDefinition.Rule rule) {
-        if (AutoTuneTargetType.STATION.equals(rule.getTargetType())
-                && "outTaskLimit".equals(rule.getTargetKey())) {
-            return "鍗曟璋冩暣骞呭害涓嶈兘瓒呰繃 maxStep锛涘澶ф椂涓嶅緱瓒呰繃瀵瑰簲绔欑偣 outBufferCapacity銆�";
-        }
-        return "鍗曟璋冩暣骞呭害涓嶈兘瓒呰繃 maxStep銆�";
     }
 
     private AutoTuneTaskSnapshot buildTaskSnapshot() {
diff --git a/src/main/java/com/zy/ai/utils/AiPromptUtils.java b/src/main/java/com/zy/ai/utils/AiPromptUtils.java
index 4d8baa9..d7be3d2 100644
--- a/src/main/java/com/zy/ai/utils/AiPromptUtils.java
+++ b/src/main/java/com/zy/ai/utils/AiPromptUtils.java
@@ -9,6 +9,11 @@
 @Component
 public class AiPromptUtils {
 
+    private static final String AUTO_TUNE_RULE_SNAPSHOT_INSTRUCTIONS =
+            "Step 4 璇诲彇璋冨弬瑙勫垯\n" +
+                    "- 蹇呴』璇诲彇 snapshot.ruleSnapshot 涓殑 minValue銆乵axValue銆乵axStep銆乧ooldownMinutes銆乨ynamicMaxValue 鍜� dynamicMaxSource銆俓n" +
+                    "- 姣忎釜鐩爣鍙傛暟鐨勬柊鍊间笌褰撳墠鍊煎樊鍊间笉鑳借秴杩囧搴� maxStep锛沷utTaskLimit 鐨勪笂璋冭繕蹇呴』鍙� stationOutBufferCapacities[targetId] 绾︽潫銆�";
+
     public String getDefaultPrompt(String sceneCode) {
         AiPromptScene scene = AiPromptScene.ofCode(sceneCode);
         if (scene == null) {
@@ -158,9 +163,7 @@
                             "- maxOutTask锛氬搴� asr_bas_crnp.max_out_task / asr_bas_dual_crnp.max_out_task\n" +
                             "- maxInTask锛氬搴� asr_bas_crnp.max_in_task / asr_bas_dual_crnp.max_in_task\n\n" +
                             "娉ㄦ剰锛歛sr_bas_station.out_buffer_capacity 鏄汉宸ョ淮鎶ょ殑鍑哄簱缂撳瓨瀹归噺锛屽彧鐢ㄤ簬璇佹槑 outTaskLimit 鍙笂璋冧笂闄愶紝Agent 涓嶅厑璁镐慨鏀硅瀛楁锛涘澶� outTaskLimit 鏃跺缓璁�间笉寰楄秴杩囧搴旂珯鐐� outBufferCapacity銆俓n\n" +
-                            "Step 4 璇诲彇璋冨弬瑙勫垯\n" +
-                            "- 蹇呴』璇诲彇 snapshot.ruleSnapshot 涓殑 minValue銆乵axValue銆乵axStep銆乧ooldownMinutes銆乨ynamicMaxValue 鍜� dynamicMaxSource銆俓n" +
-                            "- 姣忎釜鐩爣鍙傛暟鐨勬柊鍊间笌褰撳墠鍊煎樊鍊间笉鑳借秴杩囧搴� maxStep锛沷utTaskLimit 鐨勪笂璋冭繕蹇呴』鍙� stationOutBufferCapacities[targetId] 绾︽潫銆俓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" +
diff --git a/src/main/resources/sql/20260427_add_ai_auto_tune_console_menu.sql b/src/main/resources/sql/20260427_add_ai_auto_tune_console_menu.sql
deleted file mode 100644
index 771f0cf..0000000
--- a/src/main/resources/sql/20260427_add_ai_auto_tune_console_menu.sql
+++ /dev/null
@@ -1,72 +0,0 @@
--- AI鑷姩璋冨弬鎺у埗鍙拌彍鍗曞閲忚剼鏈�
--- 鎵ц鍚庤鍦ㄢ�滆鑹叉巿鏉冣�濋噷缁欏搴旇鑹插嬀閫� AI绠$悊 -> AI鑷姩璋冨弬鎺у埗鍙般��
-
-SET @ai_manage_id := COALESCE(
-  (
-    SELECT id
-    FROM sys_resource
-    WHERE code = 'aiManage' AND level = 1
-    ORDER BY id
-    LIMIT 1
-  ),
-  (
-    SELECT id
-    FROM sys_resource
-    WHERE name = 'AI绠$悊' AND level = 1
-    ORDER BY id
-    LIMIT 1
-  )
-);
-
-INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
-SELECT 'ai/auto_tune.html', 'AI鑷姩璋冨弬鎺у埗鍙�', @ai_manage_id, 2, 4, 1
-FROM dual
-WHERE @ai_manage_id IS NOT NULL
-  AND NOT EXISTS (
-    SELECT 1
-    FROM sys_resource
-    WHERE code = 'ai/auto_tune.html' AND level = 2
-  );
-
-UPDATE sys_resource
-SET name = 'AI鑷姩璋冨弬鎺у埗鍙�',
-    resource_id = @ai_manage_id,
-    level = 2,
-    sort = 4,
-    status = 1
-WHERE code = 'ai/auto_tune.html' AND level = 2;
-
-SET @ai_auto_tune_id := (
-  SELECT id
-  FROM sys_resource
-  WHERE code = 'ai/auto_tune.html' AND level = 2
-  ORDER BY id
-  LIMIT 1
-);
-
-INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
-SELECT 'ai/auto_tune.html#view', '鏌ョ湅', @ai_auto_tune_id, 3, 1, 1
-FROM dual
-WHERE @ai_auto_tune_id IS NOT NULL
-  AND NOT EXISTS (
-    SELECT 1
-    FROM sys_resource
-    WHERE code = 'ai/auto_tune.html#view' AND level = 3
-  );
-
-UPDATE sys_resource
-SET name = '鏌ョ湅',
-    resource_id = @ai_auto_tune_id,
-    level = 3,
-    sort = 1,
-    status = 1
-WHERE code = 'ai/auto_tune.html#view' AND level = 3;
-
-SELECT id, code, name, resource_id, level, sort, status
-FROM sys_resource
-WHERE code IN (
-  'aiManage',
-  'ai/auto_tune.html',
-  'ai/auto_tune.html#view'
-)
-ORDER BY level, sort, id;
diff --git a/src/main/resources/sql/20260427_add_ai_auto_tune_sys_configs.sql b/src/main/resources/sql/20260427_add_ai_auto_tune_sys_configs.sql
deleted file mode 100644
index 2cc66b0..0000000
--- a/src/main/resources/sql/20260427_add_ai_auto_tune_sys_configs.sql
+++ /dev/null
@@ -1,47 +0,0 @@
--- WCS鑷姩璋冨弬绯荤粺閰嶇疆
-
-INSERT INTO sys_config(name, code, value, type, status, select_type)
-SELECT 'AI鑷姩璋冨弬寮�鍏�', 'aiAutoTuneEnabled', 'N', 1, 1, 'system'
-FROM dual
-WHERE NOT EXISTS (
-    SELECT 1
-    FROM sys_config
-    WHERE code = 'aiAutoTuneEnabled'
-);
-
-INSERT INTO sys_config(name, code, value, type, status, select_type)
-SELECT 'AI鑷姩璋冨弬闂撮殧(鍒嗛挓)', 'aiAutoTuneIntervalMinutes', '10', 1, 1, 'system'
-FROM dual
-WHERE NOT EXISTS (
-    SELECT 1
-    FROM sys_config
-    WHERE code = 'aiAutoTuneIntervalMinutes'
-);
-
-INSERT INTO sys_config(name, code, value, type, status, select_type)
-SELECT 'AI鑷姩璋冨弬Prompt鏃ュ織淇濈暀涓婇檺', 'aiAutoTunePromptLogLimit', '500', 1, 1, 'system'
-FROM dual
-WHERE NOT EXISTS (
-    SELECT 1
-    FROM sys_config
-    WHERE code = 'aiAutoTunePromptLogLimit'
-);
-
-INSERT INTO sys_config(name, code, value, type, status, select_type)
-SELECT '杈撻�佺珯鍑哄簱浠诲姟鍏ㄥ眬涓婇檺', 'conveyorStationTaskLimit', '30', 1, 1, 'system'
-FROM dual
-WHERE NOT EXISTS (
-    SELECT 1
-    FROM sys_config
-    WHERE code = 'conveyorStationTaskLimit'
-);
-
-SELECT id, name, code, value, type, status, select_type
-FROM sys_config
-WHERE code IN (
-    'aiAutoTuneEnabled',
-    'aiAutoTuneIntervalMinutes',
-    'aiAutoTunePromptLogLimit',
-    'conveyorStationTaskLimit'
-)
-ORDER BY code;
diff --git a/src/main/resources/sql/20260427_add_out_buffer_capacity_to_asr_bas_station.sql b/src/main/resources/sql/20260427_add_out_buffer_capacity_to_asr_bas_station.sql
deleted file mode 100644
index 5e82347..0000000
--- a/src/main/resources/sql/20260427_add_out_buffer_capacity_to_asr_bas_station.sql
+++ /dev/null
@@ -1,55 +0,0 @@
--- asr_bas_station 澧炲姞鍑哄簱缂撳瓨瀹归噺閰嶇疆锛屾浛浠� asr_station_flow_capacity
--- 鐢ㄩ�旓細璇佹槑鍑哄簱绔欑偣 out_task_limit 鐨勫彲涓婅皟涓婇檺
--- 閫傜敤鏁版嵁搴擄細MySQL
-
-SET @current_db := DATABASE();
-
-SET @out_buffer_capacity_exists := (
-  SELECT COUNT(1)
-  FROM information_schema.COLUMNS
-  WHERE TABLE_SCHEMA = @current_db
-    AND TABLE_NAME = 'asr_bas_station'
-    AND COLUMN_NAME = 'out_buffer_capacity'
-);
-
-SET @add_out_buffer_capacity_sql := IF(
-  @out_buffer_capacity_exists = 0,
-  'ALTER TABLE asr_bas_station ADD COLUMN out_buffer_capacity INT NULL COMMENT ''鍑哄簱缂撳瓨瀹归噺锛岀敤浜庤瘉鏄� out_task_limit 涓婇檺'' AFTER out_task_limit',
-  'SELECT ''column out_buffer_capacity already exists'' '
-);
-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;
-
-SHOW COLUMNS FROM asr_bas_station LIKE 'out_buffer_capacity';
diff --git a/src/main/resources/sql/20260427_create_ai_auto_tune_tables.sql b/src/main/resources/sql/20260427_create_ai_auto_tune_tables.sql
deleted file mode 100644
index 275f5fd..0000000
--- a/src/main/resources/sql/20260427_create_ai_auto_tune_tables.sql
+++ /dev/null
@@ -1,47 +0,0 @@
-CREATE TABLE IF NOT EXISTS `sys_ai_auto_tune_job` (
-  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
-  `trigger_type` VARCHAR(32) NOT NULL COMMENT '瑙﹀彂绫诲瀷:auto/manual/revert',
-  `status` VARCHAR(32) NOT NULL COMMENT '鎵ц鐘舵��:running/success/failed/rejected',
-  `start_time` DATETIME NOT NULL COMMENT '寮�濮嬫椂闂�',
-  `finish_time` DATETIME DEFAULT NULL COMMENT '缁撴潫鏃堕棿',
-  `has_active_tasks` TINYINT NOT NULL DEFAULT 0 COMMENT '鎵ц鏃舵槸鍚﹀瓨鍦ㄦ椿鍔ㄤ换鍔�:1鏄�0鍚�',
-  `prompt_scene_code` VARCHAR(64) NOT NULL COMMENT 'Prompt鍦烘櫙缂栫爜',
-  `summary` VARCHAR(512) DEFAULT NULL COMMENT '鎵ц鎽樿',
-  `reasoning_digest` MEDIUMTEXT COMMENT '鎺ㄧ悊鎽樿',
-  `snapshot_digest` MEDIUMTEXT COMMENT '蹇収鎽樿',
-  `interval_before` INT DEFAULT NULL COMMENT '璋冨弬鍓嶈嚜鍔ㄨ皟鍙傞棿闅斿垎閽�',
-  `interval_after` INT DEFAULT NULL COMMENT '璋冨弬鍚庤嚜鍔ㄨ皟鍙傞棿闅斿垎閽�',
-  `success_count` INT NOT NULL DEFAULT 0 COMMENT '鎴愬姛鍙樻洿鏁�',
-  `reject_count` INT NOT NULL DEFAULT 0 COMMENT '鎷掔粷鍙樻洿鏁�',
-  `error_message` VARCHAR(1024) DEFAULT NULL COMMENT '閿欒淇℃伅',
-  `llm_call_count` INT NOT NULL DEFAULT 0 COMMENT 'LLM璋冪敤娆℃暟',
-  `prompt_tokens` INT NOT NULL DEFAULT 0 COMMENT 'Prompt tokens',
-  `completion_tokens` INT NOT NULL DEFAULT 0 COMMENT 'Completion tokens',
-  `total_tokens` INT NOT NULL DEFAULT 0 COMMENT '鎬籺okens',
-  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
-  PRIMARY KEY (`id`),
-  KEY `idx_sys_ai_auto_tune_job_status` (`status`),
-  KEY `idx_sys_ai_auto_tune_job_start_time` (`start_time`),
-  KEY `idx_sys_ai_auto_tune_job_finish_time` (`finish_time`),
-  KEY `idx_sys_ai_auto_tune_job_create_time` (`create_time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI鑷姩璋冨弬浠诲姟瀹¤琛�';
-
-CREATE TABLE IF NOT EXISTS `sys_ai_auto_tune_change` (
-  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
-  `job_id` BIGINT NOT NULL COMMENT '鑷姩璋冨弬浠诲姟ID',
-  `target_type` VARCHAR(64) NOT NULL COMMENT '鐩爣绫诲瀷:sys_config/station/crnp/dual_crnp',
-  `target_id` VARCHAR(64) DEFAULT NULL COMMENT '鐩爣ID',
-  `target_key` VARCHAR(128) NOT NULL COMMENT '鐩爣鍙傛暟閿�',
-  `old_value` VARCHAR(255) DEFAULT NULL COMMENT '鍘熷��',
-  `requested_value` VARCHAR(255) DEFAULT NULL COMMENT '鐢宠鍊�',
-  `applied_value` VARCHAR(255) DEFAULT NULL COMMENT '瀹為檯搴旂敤鍊�',
-  `result_status` VARCHAR(32) NOT NULL COMMENT '缁撴灉鐘舵��:success/rejected/failed/dry_run',
-  `reject_reason` VARCHAR(512) DEFAULT NULL COMMENT '鎷掔粷鍘熷洜',
-  `cooldown_expire_time` DATETIME DEFAULT NULL COMMENT '鍐峰嵈鎴鏃堕棿',
-  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
-  PRIMARY KEY (`id`),
-  KEY `idx_sys_ai_auto_tune_change_job_id` (`job_id`),
-  KEY `idx_sys_ai_auto_tune_change_result_status` (`result_status`),
-  KEY `idx_sys_ai_auto_tune_change_cooldown` (`cooldown_expire_time`),
-  KEY `idx_sys_ai_auto_tune_change_create_time` (`create_time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI鑷姩璋冨弬鍙樻洿瀹¤琛�';
diff --git a/src/main/resources/sql/20260427_update_auto_tune_prompt_out_buffer_capacity.sql b/src/main/resources/sql/20260427_update_auto_tune_prompt_out_buffer_capacity.sql
deleted file mode 100644
index 16d9292..0000000
--- a/src/main/resources/sql/20260427_update_auto_tune_prompt_out_buffer_capacity.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-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娉ㄦ剰锛歛sr_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 鏄汉宸ョ淮鎶ょ殑鍑哄簱缂撳瓨瀹归噺%';
diff --git a/src/main/resources/sql/20260427_update_auto_tune_prompt_rule_snapshot.sql b/src/main/resources/sql/20260427_update_auto_tune_prompt_rule_snapshot.sql
deleted file mode 100644
index d043366..0000000
--- a/src/main/resources/sql/20260427_update_auto_tune_prompt_rule_snapshot.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-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琛ュ厖锛氳鍙栬皟鍙傝鍒橽n- 蹇呴』璇诲彇 snapshot.ruleSnapshot 涓殑 minValue銆乵axValue銆乵axStep銆乧ooldownMinutes銆乨ynamicMaxValue 鍜� dynamicMaxSource銆俓n- 姣忎釜鐩爣鍙傛暟鐨勬柊鍊间笌褰撳墠鍊煎樊鍊间笉鑳借秴杩囧搴� maxStep锛沷utTaskLimit 鐨勪笂璋冭繕蹇呴』鍙� stationOutBufferCapacities[targetId] 绾︽潫銆�'
-)
-WHERE template.scene_code = 'wcs_auto_tune_dispatch'
-  AND block.block_type = 'scene_playbook'
-  AND block.content NOT LIKE '%snapshot.ruleSnapshot%';
diff --git a/src/main/resources/sql/20260428_ai_auto_tune_consolidated.sql b/src/main/resources/sql/20260428_ai_auto_tune_consolidated.sql
new file mode 100644
index 0000000..632f917
--- /dev/null
+++ b/src/main/resources/sql/20260428_ai_auto_tune_consolidated.sql
@@ -0,0 +1,256 @@
+-- AI鑷姩璋冨弬鍚堝苟澧為噺鑴氭湰
+-- 鍚堝苟鑼冨洿锛�2026-04-27 鑷� 2026-04-28 鑷姩璋冨弬鐩稿叧 SQL 鍙樻洿銆�
+-- 鎵ц璇存槑锛氬湪鐩爣搴撲腑鎵ц鏈枃浠朵竴娆″嵆鍙紱鑴氭湰鎸夊箓绛夋柟寮忕紪鍐欙紝宸插瓨鍦ㄥ璞′細璺宠繃鎴栨洿鏂般��
+-- 涓嶅寘鍚凡搴熷純鐨� asr_station_flow_capacity 寤鸿〃鑴氭湰锛涘鍘嗗彶搴撳瓨鍦ㄨ琛紝浼氳縼绉� OUT 瀹归噺鍒� asr_bas_station.out_buffer_capacity 鍚庡垹闄ゆ棫琛ㄣ��
+-- 鏉ユ簮鑴氭湰锛�
+-- - 20260427_create_ai_auto_tune_tables.sql
+-- - 20260427_add_ai_auto_tune_sys_configs.sql
+-- - 20260427_add_out_buffer_capacity_to_asr_bas_station.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
+
+-- 1. AI鑷姩璋冨弬瀹¤琛�
+CREATE TABLE IF NOT EXISTS `sys_ai_auto_tune_job` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
+  `trigger_type` VARCHAR(32) NOT NULL COMMENT '瑙﹀彂绫诲瀷:auto/manual/revert',
+  `status` VARCHAR(32) NOT NULL COMMENT '鎵ц鐘舵��:running/success/failed/rejected',
+  `start_time` DATETIME NOT NULL COMMENT '寮�濮嬫椂闂�',
+  `finish_time` DATETIME DEFAULT NULL COMMENT '缁撴潫鏃堕棿',
+  `has_active_tasks` TINYINT NOT NULL DEFAULT 0 COMMENT '鎵ц鏃舵槸鍚﹀瓨鍦ㄦ椿鍔ㄤ换鍔�:1鏄�0鍚�',
+  `prompt_scene_code` VARCHAR(64) NOT NULL COMMENT 'Prompt鍦烘櫙缂栫爜',
+  `summary` VARCHAR(512) DEFAULT NULL COMMENT '鎵ц鎽樿',
+  `reasoning_digest` MEDIUMTEXT COMMENT '鎺ㄧ悊鎽樿',
+  `snapshot_digest` MEDIUMTEXT COMMENT '蹇収鎽樿',
+  `interval_before` INT DEFAULT NULL COMMENT '璋冨弬鍓嶈嚜鍔ㄨ皟鍙傞棿闅斿垎閽�',
+  `interval_after` INT DEFAULT NULL COMMENT '璋冨弬鍚庤嚜鍔ㄨ皟鍙傞棿闅斿垎閽�',
+  `success_count` INT NOT NULL DEFAULT 0 COMMENT '鎴愬姛鍙樻洿鏁�',
+  `reject_count` INT NOT NULL DEFAULT 0 COMMENT '鎷掔粷鍙樻洿鏁�',
+  `error_message` VARCHAR(1024) DEFAULT NULL COMMENT '閿欒淇℃伅',
+  `llm_call_count` INT NOT NULL DEFAULT 0 COMMENT 'LLM璋冪敤娆℃暟',
+  `prompt_tokens` INT NOT NULL DEFAULT 0 COMMENT 'Prompt tokens',
+  `completion_tokens` INT NOT NULL DEFAULT 0 COMMENT 'Completion tokens',
+  `total_tokens` INT NOT NULL DEFAULT 0 COMMENT '鎬籺okens',
+  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  PRIMARY KEY (`id`),
+  KEY `idx_sys_ai_auto_tune_job_status` (`status`),
+  KEY `idx_sys_ai_auto_tune_job_start_time` (`start_time`),
+  KEY `idx_sys_ai_auto_tune_job_finish_time` (`finish_time`),
+  KEY `idx_sys_ai_auto_tune_job_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI鑷姩璋冨弬浠诲姟瀹¤琛�';
+
+CREATE TABLE IF NOT EXISTS `sys_ai_auto_tune_change` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
+  `job_id` BIGINT NOT NULL COMMENT '鑷姩璋冨弬浠诲姟ID',
+  `target_type` VARCHAR(64) NOT NULL COMMENT '鐩爣绫诲瀷:sys_config/station/crnp/dual_crnp',
+  `target_id` VARCHAR(64) DEFAULT NULL COMMENT '鐩爣ID',
+  `target_key` VARCHAR(128) NOT NULL COMMENT '鐩爣鍙傛暟閿�',
+  `old_value` VARCHAR(255) DEFAULT NULL COMMENT '鍘熷��',
+  `requested_value` VARCHAR(255) DEFAULT NULL COMMENT '鐢宠鍊�',
+  `applied_value` VARCHAR(255) DEFAULT NULL COMMENT '瀹為檯搴旂敤鍊�',
+  `result_status` VARCHAR(32) NOT NULL COMMENT '缁撴灉鐘舵��:success/rejected/failed/dry_run',
+  `reject_reason` VARCHAR(512) DEFAULT NULL COMMENT '鎷掔粷鍘熷洜',
+  `cooldown_expire_time` DATETIME DEFAULT NULL COMMENT '鍐峰嵈鎴鏃堕棿',
+  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  PRIMARY KEY (`id`),
+  KEY `idx_sys_ai_auto_tune_change_job_id` (`job_id`),
+  KEY `idx_sys_ai_auto_tune_change_result_status` (`result_status`),
+  KEY `idx_sys_ai_auto_tune_change_cooldown` (`cooldown_expire_time`),
+  KEY `idx_sys_ai_auto_tune_change_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI鑷姩璋冨弬鍙樻洿瀹¤琛�';
+
+-- 2. AI鑷姩璋冨弬绯荤粺閰嶇疆
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT 'AI鑷姩璋冨弬寮�鍏�', 'aiAutoTuneEnabled', 'N', 1, 1, 'system'
+FROM dual
+WHERE NOT EXISTS (
+    SELECT 1
+    FROM sys_config
+    WHERE code = 'aiAutoTuneEnabled'
+);
+
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT 'AI鑷姩璋冨弬闂撮殧(鍒嗛挓)', 'aiAutoTuneIntervalMinutes', '10', 1, 1, 'system'
+FROM dual
+WHERE NOT EXISTS (
+    SELECT 1
+    FROM sys_config
+    WHERE code = 'aiAutoTuneIntervalMinutes'
+);
+
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT 'AI鑷姩璋冨弬Prompt鏃ュ織淇濈暀涓婇檺', 'aiAutoTunePromptLogLimit', '500', 1, 1, 'system'
+FROM dual
+WHERE NOT EXISTS (
+    SELECT 1
+    FROM sys_config
+    WHERE code = 'aiAutoTunePromptLogLimit'
+);
+
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT '杈撻�佺珯鍑哄簱浠诲姟鍏ㄥ眬涓婇檺', 'conveyorStationTaskLimit', '30', 1, 1, 'system'
+FROM dual
+WHERE NOT EXISTS (
+    SELECT 1
+    FROM sys_config
+    WHERE code = 'conveyorStationTaskLimit'
+);
+
+-- 3. asr_bas_station 澧炲姞鍑哄簱缂撳瓨瀹归噺閰嶇疆锛屾浛浠� asr_station_flow_capacity
+SET @current_db := DATABASE();
+
+SET @out_buffer_capacity_exists := (
+  SELECT COUNT(1)
+  FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = @current_db
+    AND TABLE_NAME = 'asr_bas_station'
+    AND COLUMN_NAME = 'out_buffer_capacity'
+);
+
+SET @add_out_buffer_capacity_sql := IF(
+  @out_buffer_capacity_exists = 0,
+  'ALTER TABLE asr_bas_station ADD COLUMN out_buffer_capacity INT NULL COMMENT ''鍑哄簱缂撳瓨瀹归噺锛岀敤浜庤瘉鏄� out_task_limit 涓婇檺'' AFTER out_task_limit',
+  'SELECT ''column out_buffer_capacity already exists'' '
+);
+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鑷姩璋冨弬鎺у埗鍙般��
+SET @ai_manage_id := COALESCE(
+  (
+    SELECT id
+    FROM sys_resource
+    WHERE code = 'aiManage' AND level = 1
+    ORDER BY id
+    LIMIT 1
+  ),
+  (
+    SELECT id
+    FROM sys_resource
+    WHERE name = 'AI绠$悊' AND level = 1
+    ORDER BY id
+    LIMIT 1
+  )
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'ai/auto_tune.html', 'AI鑷姩璋冨弬鎺у埗鍙�', @ai_manage_id, 2, 4, 1
+FROM dual
+WHERE @ai_manage_id IS NOT NULL
+  AND NOT EXISTS (
+    SELECT 1
+    FROM sys_resource
+    WHERE code = 'ai/auto_tune.html' AND level = 2
+  );
+
+UPDATE sys_resource
+SET name = 'AI鑷姩璋冨弬鎺у埗鍙�',
+    resource_id = @ai_manage_id,
+    level = 2,
+    sort = 4,
+    status = 1
+WHERE code = 'ai/auto_tune.html' AND level = 2;
+
+SET @ai_auto_tune_id := (
+  SELECT id
+  FROM sys_resource
+  WHERE code = 'ai/auto_tune.html' AND level = 2
+  ORDER BY id
+  LIMIT 1
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'ai/auto_tune.html#view', '鏌ョ湅', @ai_auto_tune_id, 3, 1, 1
+FROM dual
+WHERE @ai_auto_tune_id IS NOT NULL
+  AND NOT EXISTS (
+    SELECT 1
+    FROM sys_resource
+    WHERE code = 'ai/auto_tune.html#view' AND level = 3
+  );
+
+UPDATE sys_resource
+SET name = '鏌ョ湅',
+    resource_id = @ai_auto_tune_id,
+    level = 3,
+    sort = 1,
+    status = 1
+WHERE code = 'ai/auto_tune.html#view' AND level = 3;
+
+-- 5. 鍘嗗彶宸插彂甯� Prompt 琛ヤ竵锛歰utBufferCapacity 绾︽潫璇存槑
+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娉ㄦ剰锛歛sr_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 鏄汉宸ョ淮鎶ょ殑鍑哄簱缂撳瓨瀹归噺%';
+
+-- 6. 鍘嗗彶宸插彂甯� Prompt 琛ヤ竵锛歳uleSnapshot 瑙勫垯璇存槑
+-- 杩藉姞鏂囨湰闇�涓� 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銆乵axValue銆乵axStep銆乧ooldownMinutes銆乨ynamicMaxValue 鍜� dynamicMaxSource銆俓n- 姣忎釜鐩爣鍙傛暟鐨勬柊鍊间笌褰撳墠鍊煎樊鍊间笉鑳借秴杩囧搴� maxStep锛沷utTaskLimit 鐨勪笂璋冭繕蹇呴』鍙� stationOutBufferCapacities[targetId] 绾︽潫銆�'
+)
+WHERE template.scene_code = 'wcs_auto_tune_dispatch'
+  AND block.block_type = 'scene_playbook'
+  AND block.content NOT LIKE '%snapshot.ruleSnapshot%';
+
+-- 7. 鎵ц鍚庢鏌�
+SELECT id, name, code, value, type, status, select_type
+FROM sys_config
+WHERE code IN (
+    'aiAutoTuneEnabled',
+    'aiAutoTuneIntervalMinutes',
+    'aiAutoTunePromptLogLimit',
+    'conveyorStationTaskLimit'
+)
+ORDER BY code;
+
+SHOW COLUMNS FROM asr_bas_station LIKE 'out_buffer_capacity';
+
+SELECT id, code, name, resource_id, level, sort, status
+FROM sys_resource
+WHERE code IN (
+  'aiManage',
+  'ai/auto_tune.html',
+  'ai/auto_tune.html#view'
+)
+ORDER BY level, sort, id;
diff --git a/src/main/webapp/views/ai/auto_tune.html b/src/main/webapp/views/ai/auto_tune.html
index 1c52d1f..1113755 100644
--- a/src/main/webapp/views/ai/auto_tune.html
+++ b/src/main/webapp/views/ai/auto_tune.html
@@ -372,10 +372,13 @@
               </div>
             </div>
             <div class="map-box">
-              <div class="map-title">璋冨弬瑙勫垯 maxStep/cooldown</div>
+              <div class="map-title">
+                <span>璋冨弬瑙勫垯 ruleSnapshot</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">
-                  {{ item.targetType }}/{{ item.targetKey }}: 姝{ item.maxStep }} / 鍐穥{ item.cooldownMinutes }}m
+                  {{ formatRuleSnapshotText(item) }}
                 </span>
                 <span class="small-muted" v-if="ruleSnapshot.length === 0">鏆傛棤鏁版嵁</span>
               </div>
@@ -786,6 +789,24 @@
         }
         return result;
       },
+      formatRuleSnapshotText: function(rule) {
+        var safeRule = rule || {};
+        var targetName = this.valueOrDash(safeRule.targetType) + '/' + this.valueOrDash(safeRule.targetKey);
+        var minValue = this.valueOrDash(safeRule.minValue);
+        var maxValue = this.valueOrDash(safeRule.maxValue);
+        var dynamicMaxValue = this.valueOrDash(safeRule.dynamicMaxValue);
+        var maxStep = this.valueOrDash(safeRule.maxStep);
+        var cooldownMinutes = this.valueOrDash(safeRule.cooldownMinutes);
+        var dynamicMaxSource = this.valueOrDash(safeRule.dynamicMaxSource);
+
+        return targetName
+          + ': min=' + minValue
+          + ' / max=' + maxValue
+          + ' / dynamicMax=' + dynamicMaxValue
+          + ' / maxStep=' + maxStep
+          + ' / cooldown=' + cooldownMinutes + 'm'
+          + ' / dynamicMaxSource=' + dynamicMaxSource;
+      },
       statusType: function(status) {
         if (status === 'success') {
           return 'success';
diff --git a/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java b/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
index 278b1ae..1620f70 100644
--- a/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
+++ b/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
@@ -163,35 +163,41 @@
     }
 
     @Test
-    void rejectCrnOutBatchRunningLimitRangeAndStepCases() {
+    void crnOutBatchRunningLimitAllowsStepThreeAndRejectsRangeAndStepCases() {
         when(configService.getOne(any(Wrapper.class))).thenReturn(config("crnOutBatchRunningLimit", "10"));
 
         service.apply(request(true,
                 command("sys_config", null, "crnOutBatchRunningLimit", "13"),
+                command("sys_config", null, "crnOutBatchRunningLimit", "14"),
                 command("sys_config", null, "crnOutBatchRunningLimit", "21")
         ));
 
         List<AiAutoTuneChange> changes = savedChanges();
-        assertEquals("rejected", changes.get(0).getResultStatus());
-        assertTrue(changes.get(0).getRejectReason().contains("姝ラ暱涓嶈兘瓒呰繃 2"));
+        assertEquals("dry_run", changes.get(0).getResultStatus());
+        assertEquals("13", changes.get(0).getRequestedValue());
         assertEquals("rejected", changes.get(1).getResultStatus());
-        assertTrue(changes.get(1).getRejectReason().contains("1~20"));
+        assertTrue(changes.get(1).getRejectReason().contains("姝ラ暱涓嶈兘瓒呰繃 3"));
+        assertEquals("rejected", changes.get(2).getResultStatus());
+        assertTrue(changes.get(2).getRejectReason().contains("1~20"));
     }
 
     @Test
-    void rejectMaxInTaskRangeAndStepCases() {
+    void maxInTaskAllowsStepThreeAndRejectsRangeAndStepCases() {
         when(basCrnpService.getById(1)).thenReturn(crn(1, 1, 5));
 
         service.apply(request(true,
-                command("crn", "1", "maxInTask", "7"),
+                command("crn", "1", "maxInTask", "8"),
+                command("crn", "1", "maxInTask", "9"),
                 command("crn", "1", "maxInTask", "11")
         ));
 
         List<AiAutoTuneChange> changes = savedChanges();
-        assertEquals("rejected", changes.get(0).getResultStatus());
-        assertTrue(changes.get(0).getRejectReason().contains("姝ラ暱涓嶈兘瓒呰繃 1"));
+        assertEquals("dry_run", changes.get(0).getResultStatus());
+        assertEquals("8", changes.get(0).getRequestedValue());
         assertEquals("rejected", changes.get(1).getResultStatus());
-        assertTrue(changes.get(1).getRejectReason().contains("0~10"));
+        assertTrue(changes.get(1).getRejectReason().contains("姝ラ暱涓嶈兘瓒呰繃 3"));
+        assertEquals("rejected", changes.get(2).getResultStatus());
+        assertTrue(changes.get(2).getRejectReason().contains("0~10"));
     }
 
     @Test
diff --git a/src/test/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImplTest.java b/src/test/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImplTest.java
index 5aba3a8..52fab9a 100644
--- a/src/test/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImplTest.java
+++ b/src/test/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImplTest.java
@@ -73,17 +73,25 @@
         AutoTuneRuleSnapshotItem stationOutTaskRule = findRule(result, "station", "outTaskLimit");
         assertEquals(0, stationOutTaskRule.getMinValue());
         assertNull(stationOutTaskRule.getMaxValue());
-        assertEquals(1, stationOutTaskRule.getMaxStep());
+        assertEquals(3, stationOutTaskRule.getMaxStep());
         assertEquals(10, stationOutTaskRule.getCooldownMinutes());
         assertEquals(Boolean.TRUE, stationOutTaskRule.getDynamicMaxValue());
         assertEquals("currentParameterSnapshot.stationOutBufferCapacities[targetId]",
                 stationOutTaskRule.getDynamicMaxSource());
+        assertEquals("鍗曟璋冩暣骞呭害涓嶈兘瓒呰繃 maxStep锛涘澶ф椂涓嶅緱瓒呰繃瀵瑰簲绔欑偣 outBufferCapacity銆�",
+                stationOutTaskRule.getNote());
 
         AutoTuneRuleSnapshotItem crnMaxOutRule = findRule(result, "crn", "maxOutTask");
         assertEquals(3, crnMaxOutRule.getMaxStep());
+        assertNull(crnMaxOutRule.getDynamicMaxSource());
+        assertEquals("鍗曟璋冩暣骞呭害涓嶈兘瓒呰繃 maxStep銆�", crnMaxOutRule.getNote());
 
         AutoTuneRuleSnapshotItem crnMaxInRule = findRule(result, "crn", "maxInTask");
-        assertEquals(1, crnMaxInRule.getMaxStep());
+        assertEquals(3, crnMaxInRule.getMaxStep());
+
+        assertRuleMaxStep(result, "sys_config", "crnOutBatchRunningLimit", 3);
+        assertRuleMaxStep(result, "dual_crn", "maxOutTask", 3);
+        assertRuleMaxStep(result, "dual_crn", "maxInTask", 3);
     }
 
     @Test
@@ -187,4 +195,12 @@
         }
         throw new AssertionError("rule not found: " + targetType + "/" + targetKey);
     }
+
+    private void assertRuleMaxStep(List<AutoTuneRuleSnapshotItem> rules,
+                                   String targetType,
+                                   String targetKey,
+                                   int expectedMaxStep) {
+        AutoTuneRuleSnapshotItem rule = findRule(rules, targetType, targetKey);
+        assertEquals(expectedMaxStep, rule.getMaxStep());
+    }
 }

--
Gitblit v1.9.1