#
Junjie
9 天以前 dc3f9cc91759823ce59486f19b138be4b296a0f1
#
1个文件已添加
6个文件已修改
6个文件已删除
745 ■■■■■ 已修改文件
src/main/java/com/zy/ai/domain/autotune/AutoTuneRuleDefinition.java 149 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/utils/AiPromptUtils.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260427_add_ai_auto_tune_console_menu.sql 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260427_add_ai_auto_tune_sys_configs.sql 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260427_add_out_buffer_capacity_to_asr_bas_station.sql 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260427_create_ai_auto_tune_tables.sql 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260427_update_auto_tune_prompt_out_buffer_capacity.sql 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260427_update_auto_tune_prompt_rule_snapshot.sql 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260428_ai_auto_tune_consolidated.sql 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/ai/auto_tune.html 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/ai/service/impl/AutoTuneSnapshotServiceImplTest.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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
            );
        }
    }
}
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() {
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、maxValue、maxStep、cooldownMinutes、dynamicMaxValue 和 dynamicMaxSource。\n" +
                    "- 每个目标参数的新值与当前值差值不能超过对应 maxStep;outTaskLimit 的上调还必须受 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" +
                            "注意:asr_bas_station.out_buffer_capacity 是人工维护的出库缓存容量,只用于证明 outTaskLimit 可上调上限,Agent 不允许修改该字段;增大 outTaskLimit 时建议值不得超过对应站点 outBufferCapacity。\n\n" +
                            "Step 4 读取调参规则\n" +
                            "- 必须读取 snapshot.ruleSnapshot 中的 minValue、maxValue、maxStep、cooldownMinutes、dynamicMaxValue 和 dynamicMaxSource。\n" +
                            "- 每个目标参数的新值与当前值差值不能超过对应 maxStep;outTaskLimit 的上调还必须受 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" +
src/main/resources/sql/20260427_add_ai_auto_tune_console_menu.sql
File was deleted
src/main/resources/sql/20260427_add_ai_auto_tune_sys_configs.sql
File was deleted
src/main/resources/sql/20260427_add_out_buffer_capacity_to_asr_bas_station.sql
File was deleted
src/main/resources/sql/20260427_create_ai_auto_tune_tables.sql
File was deleted
src/main/resources/sql/20260427_update_auto_tune_prompt_out_buffer_capacity.sql
File was deleted
src/main/resources/sql/20260427_update_auto_tune_prompt_rule_snapshot.sql
File was deleted
src/main/resources/sql/20260428_ai_auto_tune_consolidated.sql
New file
@@ -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 '总tokens',
  `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 补丁: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 是人工维护的出库缓存容量%';
-- 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- 每个目标参数的新值与当前值差值不能超过对应 maxStep;outTaskLimit 的上调还必须受 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;
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';
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
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());
    }
}