#
Junjie
3 天以前 aeb124afcef69c8e43230bc0b31cee0616a5d9c9
#
2个文件已修改
7个文件已添加
545 ■■■■■ 已修改文件
src/main/java/com/zy/ai/controller/AiPromptTemplateController.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/entity/AiPromptTemplate.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/enums/AiPromptScene.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/mapper/AiPromptTemplateMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/AiPromptTemplateService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/WcsDiagnosisService.java 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/impl/AiPromptTemplateServiceImpl.java 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/utils/AiPromptUtils.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260312_create_sys_ai_prompt_template.sql 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/controller/AiPromptTemplateController.java
New file
@@ -0,0 +1,97 @@
package com.zy.ai.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.annotations.ManagerAuth;
import com.core.common.R;
import com.zy.ai.entity.AiPromptTemplate;
import com.zy.ai.service.AiPromptTemplateService;
import com.zy.common.web.BaseController;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/ai/prompt/template")
@RequiredArgsConstructor
public class AiPromptTemplateController extends BaseController {
    private final AiPromptTemplateService aiPromptTemplateService;
    @GetMapping("/sceneList/auth")
    @ManagerAuth
    public R sceneList() {
        return R.ok(aiPromptTemplateService.listSupportedScenes());
    }
    @GetMapping("/list/auth")
    @ManagerAuth
    public R list(@RequestParam(value = "sceneCode", required = false) String sceneCode,
                  @RequestParam(value = "published", required = false) Short published,
                  @RequestParam(value = "status", required = false) Short status) {
        QueryWrapper<AiPromptTemplate> wrapper = new QueryWrapper<>();
        if (sceneCode != null && !sceneCode.trim().isEmpty()) {
            wrapper.eq("scene_code", sceneCode.trim());
        }
        if (published != null) {
            wrapper.eq("published", published);
        }
        if (status != null) {
            wrapper.eq("status", status);
        }
        wrapper.orderByAsc("scene_code").orderByDesc("version").orderByDesc("id");
        List<AiPromptTemplate> list = aiPromptTemplateService.list(wrapper);
        return R.ok(list);
    }
    @GetMapping("/active/auth")
    @ManagerAuth
    public R active(@RequestParam("sceneCode") String sceneCode) {
        try {
            return R.ok(aiPromptTemplateService.resolvePublished(sceneCode));
        } catch (IllegalArgumentException | IllegalStateException e) {
            return R.error(e.getMessage());
        }
    }
    @PostMapping("/save/auth")
    @ManagerAuth
    public R save(@RequestBody AiPromptTemplate template) {
        try {
            return R.ok(aiPromptTemplateService.savePrompt(template, getUserId()));
        } catch (IllegalArgumentException e) {
            return R.error(e.getMessage());
        }
    }
    @PostMapping("/publish/auth")
    @ManagerAuth
    public R publish(@RequestParam("id") Long id) {
        try {
            return R.ok(aiPromptTemplateService.publishPrompt(id, getUserId()));
        } catch (IllegalArgumentException e) {
            return R.error(e.getMessage());
        }
    }
    @PostMapping("/delete/auth")
    @ManagerAuth
    public R delete(@RequestParam("id") Long id) {
        try {
            return R.ok(aiPromptTemplateService.deletePrompt(id));
        } catch (IllegalArgumentException e) {
            return R.error(e.getMessage());
        }
    }
    @PostMapping("/initDefaults/auth")
    @ManagerAuth
    public R initDefaults() {
        return R.ok(aiPromptTemplateService.initDefaultsIfMissing());
    }
}
src/main/java/com/zy/ai/entity/AiPromptTemplate.java
New file
@@ -0,0 +1,56 @@
package com.zy.ai.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("sys_ai_prompt_template")
public class AiPromptTemplate implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String name;
    @TableField("scene_code")
    private String sceneCode;
    private Integer version;
    private String content;
    /**
     * 1 启用 0 禁用
     */
    private Short status;
    /**
     * 1 已发布 0 未发布
     */
    private Short published;
    @TableField("created_by")
    private Long createdBy;
    @TableField("published_by")
    private Long publishedBy;
    @TableField("published_time")
    private Date publishedTime;
    @TableField("create_time")
    private Date createTime;
    @TableField("update_time")
    private Date updateTime;
    private String memo;
}
src/main/java/com/zy/ai/enums/AiPromptScene.java
New file
@@ -0,0 +1,35 @@
package com.zy.ai.enums;
public enum AiPromptScene {
    DIAGNOSE_STREAM("wcs_diagnose_stream", "WCS巡检诊断"),
    SENSOR_CHAT("wcs_sensor_chat", "WCS专家问答");
    private final String code;
    private final String label;
    AiPromptScene(String code, String label) {
        this.code = code;
        this.label = label;
    }
    public String getCode() {
        return code;
    }
    public String getLabel() {
        return label;
    }
    public static AiPromptScene ofCode(String code) {
        if (code == null) {
            return null;
        }
        for (AiPromptScene item : values()) {
            if (item.code.equals(code)) {
                return item;
            }
        }
        return null;
    }
}
src/main/java/com/zy/ai/mapper/AiPromptTemplateMapper.java
New file
@@ -0,0 +1,11 @@
package com.zy.ai.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zy.ai.entity.AiPromptTemplate;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface AiPromptTemplateMapper extends BaseMapper<AiPromptTemplate> {
}
src/main/java/com/zy/ai/service/AiPromptTemplateService.java
New file
@@ -0,0 +1,22 @@
package com.zy.ai.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zy.ai.entity.AiPromptTemplate;
import java.util.List;
import java.util.Map;
public interface AiPromptTemplateService extends IService<AiPromptTemplate> {
    AiPromptTemplate resolvePublished(String sceneCode);
    AiPromptTemplate savePrompt(AiPromptTemplate template, Long operatorUserId);
    AiPromptTemplate publishPrompt(Long id, Long operatorUserId);
    boolean deletePrompt(Long id);
    int initDefaultsIfMissing();
    List<Map<String, Object>> listSupportedScenes();
}
src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -2,11 +2,13 @@
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zy.ai.entity.AiPromptTemplate;
import com.zy.ai.entity.ChatCompletionRequest;
import com.zy.ai.entity.ChatCompletionResponse;
import com.zy.ai.entity.WcsDiagnosisRequest;
import com.zy.ai.enums.AiPromptScene;
import com.zy.ai.mcp.service.SpringAiMcpToolManager;
import com.zy.ai.utils.AiPromptUtils;
import com.zy.ai.service.AiPromptTemplateService;
import com.zy.ai.utils.AiUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
@@ -32,24 +34,25 @@
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private AiPromptUtils aiPromptUtils;
    @Autowired
    private AiUtils aiUtils;
    @Autowired
    private SpringAiMcpToolManager mcpToolManager;
    @Autowired
    private AiPromptTemplateService aiPromptTemplateService;
    public void diagnoseStream(WcsDiagnosisRequest request, SseEmitter emitter) {
        List<ChatCompletionRequest.Message> messages = new ArrayList<>();
        AiPromptTemplate promptTemplate = aiPromptTemplateService.resolvePublished(AiPromptScene.DIAGNOSE_STREAM.getCode());
        ChatCompletionRequest.Message mcpSystem = new ChatCompletionRequest.Message();
        mcpSystem.setRole("system");
        mcpSystem.setContent(aiPromptUtils.getAiDiagnosePromptMcp());
        mcpSystem.setContent(promptTemplate.getContent());
        ChatCompletionRequest.Message mcpUser = new ChatCompletionRequest.Message();
        mcpUser.setRole("user");
        mcpUser.setContent(aiUtils.buildDiagnosisUserContentMcp(request));
        runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, 0.3, 2048, emitter, null);
        runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, promptTemplate, 0.3, 2048, emitter, null);
    }
    public void askStream(String prompt,
@@ -81,16 +84,17 @@
        }
        final String finalChatId = chatId;
        AiPromptTemplate promptTemplate = aiPromptTemplateService.resolvePublished(AiPromptScene.SENSOR_CHAT.getCode());
        ChatCompletionRequest.Message mcpSystem = new ChatCompletionRequest.Message();
        mcpSystem.setRole("system");
        mcpSystem.setContent(aiPromptUtils.getWcsSensorPromptMcp());
        mcpSystem.setContent(promptTemplate.getContent());
        ChatCompletionRequest.Message mcpUser = new ChatCompletionRequest.Message();
        mcpUser.setRole("user");
        mcpUser.setContent("【用户提问】\n" + (prompt == null ? "" : prompt));
        runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, 0.3, 2048, emitter, finalChatId);
        runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, promptTemplate, 0.3, 2048, emitter, finalChatId);
    }
    public List<Map<String, Object>> listChats() {
@@ -161,6 +165,7 @@
    private void runMcpStreamingDiagnosis(List<ChatCompletionRequest.Message> baseMessages,
                                          ChatCompletionRequest.Message systemPrompt,
                                          ChatCompletionRequest.Message userQuestion,
                                          AiPromptTemplate promptTemplate,
                                          Double temperature,
                                          Integer maxTokens,
                                          SseEmitter emitter,
@@ -272,6 +277,12 @@
                        Map<String, Object> meta = new java.util.HashMap<>();
                        meta.put("chatId", chatId);
                        meta.put("title", buildTitleFromPrompt(userQuestion.getContent()));
                        if (promptTemplate != null) {
                            meta.put("promptTemplateId", promptTemplate.getId());
                            meta.put("promptSceneCode", promptTemplate.getSceneCode());
                            meta.put("promptVersion", promptTemplate.getVersion());
                            meta.put("promptName", promptTemplate.getName());
                        }
                        meta.put("createdAt", createdAt);
                        meta.put("updatedAt", System.currentTimeMillis());
                        redisUtil.hmset(metaKey, meta, CHAT_TTL_SECONDS);
src/main/java/com/zy/ai/service/impl/AiPromptTemplateServiceImpl.java
New file
@@ -0,0 +1,262 @@
package com.zy.ai.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zy.ai.entity.AiPromptTemplate;
import com.zy.ai.enums.AiPromptScene;
import com.zy.ai.mapper.AiPromptTemplateMapper;
import com.zy.ai.service.AiPromptTemplateService;
import com.zy.ai.utils.AiPromptUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service("aiPromptTemplateService")
@RequiredArgsConstructor
public class AiPromptTemplateServiceImpl extends ServiceImpl<AiPromptTemplateMapper, AiPromptTemplate> implements AiPromptTemplateService {
    private final AiPromptUtils aiPromptUtils;
    @Override
    public AiPromptTemplate resolvePublished(String sceneCode) {
        AiPromptScene scene = requireScene(sceneCode);
        AiPromptTemplate prompt = findPublished(scene.getCode());
        if (prompt == null) {
            synchronized (("ai_prompt_scene_init_" + scene.getCode()).intern()) {
                prompt = findPublished(scene.getCode());
                if (prompt == null) {
                    prompt = ensurePublishedScene(scene);
                }
            }
        }
        if (prompt == null) {
            throw new IllegalStateException("未找到已发布的 Prompt,sceneCode=" + scene.getCode());
        }
        return prompt;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AiPromptTemplate savePrompt(AiPromptTemplate template, Long operatorUserId) {
        if (template == null) {
            throw new IllegalArgumentException("Prompt 不能为空");
        }
        AiPromptScene scene = requireScene(template.getSceneCode());
        String content = template.getContent();
        if (content == null || content.trim().isEmpty()) {
            throw new IllegalArgumentException("Prompt 内容不能为空");
        }
        if (template.getId() == null) {
            AiPromptTemplate entity = new AiPromptTemplate();
            int version = nextVersion(scene.getCode());
            entity.setName(defaultName(scene, version, template.getName()));
            entity.setSceneCode(scene.getCode());
            entity.setVersion(version);
            entity.setContent(content);
            entity.setStatus(normalizeStatus(template.getStatus()));
            entity.setPublished((short) 0);
            entity.setCreatedBy(operatorUserId);
            entity.setMemo(trim(template.getMemo()));
            this.save(entity);
            return entity;
        }
        AiPromptTemplate db = this.getById(template.getId());
        if (db == null) {
            throw new IllegalArgumentException("Prompt 不存在");
        }
        if (!scene.getCode().equals(db.getSceneCode())) {
            throw new IllegalArgumentException("不允许修改 Prompt 所属场景");
        }
        if (Short.valueOf((short) 1).equals(db.getPublished())) {
            throw new IllegalArgumentException("已发布 Prompt 不允许直接修改,请新建版本后再发布");
        }
        db.setName(defaultName(scene, db.getVersion() == null ? 1 : db.getVersion(), template.getName()));
        db.setContent(content);
        db.setStatus(normalizeStatus(template.getStatus()));
        db.setMemo(trim(template.getMemo()));
        this.updateById(db);
        return db;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AiPromptTemplate publishPrompt(Long id, Long operatorUserId) {
        if (id == null) {
            throw new IllegalArgumentException("id 不能为空");
        }
        AiPromptTemplate db = this.getById(id);
        if (db == null) {
            throw new IllegalArgumentException("Prompt 不存在");
        }
        if (db.getContent() == null || db.getContent().trim().isEmpty()) {
            throw new IllegalArgumentException("Prompt 内容不能为空");
        }
        UpdateWrapper<AiPromptTemplate> clearWrapper = new UpdateWrapper<>();
        clearWrapper.eq("scene_code", db.getSceneCode()).set("published", 0);
        this.update(clearWrapper);
        db.setPublished((short) 1);
        db.setStatus((short) 1);
        db.setPublishedBy(operatorUserId);
        db.setPublishedTime(new Date());
        if (db.getVersion() == null || db.getVersion() <= 0) {
            db.setVersion(nextVersion(db.getSceneCode()));
        }
        if (db.getName() == null || db.getName().trim().isEmpty()) {
            AiPromptScene scene = requireScene(db.getSceneCode());
            db.setName(defaultName(scene, db.getVersion(), null));
        }
        this.updateById(db);
        return db;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deletePrompt(Long id) {
        if (id == null) {
            return false;
        }
        AiPromptTemplate db = this.getById(id);
        if (db == null) {
            return false;
        }
        if (Short.valueOf((short) 1).equals(db.getPublished())) {
            throw new IllegalArgumentException("已发布 Prompt 不允许删除,请先发布其他版本");
        }
        return this.removeById(id);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int initDefaultsIfMissing() {
        int changed = 0;
        for (AiPromptScene scene : AiPromptScene.values()) {
            AiPromptTemplate prompt = findPublished(scene.getCode());
            if (prompt == null) {
                ensurePublishedScene(scene);
                changed++;
            }
        }
        return changed;
    }
    @Override
    public List<Map<String, Object>> listSupportedScenes() {
        List<Map<String, Object>> result = new ArrayList<>();
        for (AiPromptScene scene : AiPromptScene.values()) {
            HashMap<String, Object> item = new HashMap<>();
            item.put("code", scene.getCode());
            item.put("label", scene.getLabel());
            result.add(item);
        }
        return result;
    }
    private AiPromptTemplate ensurePublishedScene(AiPromptScene scene) {
        AiPromptTemplate latest = findLatest(scene.getCode());
        if (latest == null) {
            AiPromptTemplate seed = new AiPromptTemplate();
            seed.setName(defaultName(scene, 1, null));
            seed.setSceneCode(scene.getCode());
            seed.setVersion(1);
            seed.setContent(aiPromptUtils.getDefaultPrompt(scene.getCode()));
            seed.setStatus((short) 1);
            seed.setPublished((short) 1);
            seed.setPublishedTime(new Date());
            seed.setMemo("系统初始化默认 Prompt");
            this.save(seed);
            log.info("Initialized default AI prompt, sceneCode={}, version={}", scene.getCode(), seed.getVersion());
            return seed;
        }
        UpdateWrapper<AiPromptTemplate> clearWrapper = new UpdateWrapper<>();
        clearWrapper.eq("scene_code", scene.getCode()).set("published", 0);
        this.update(clearWrapper);
        latest.setStatus((short) 1);
        latest.setPublished((short) 1);
        if (latest.getPublishedTime() == null) {
            latest.setPublishedTime(new Date());
        }
        this.updateById(latest);
        return latest;
    }
    private AiPromptTemplate findPublished(String sceneCode) {
        QueryWrapper<AiPromptTemplate> wrapper = new QueryWrapper<>();
        wrapper.eq("scene_code", sceneCode)
                .eq("status", 1)
                .eq("published", 1)
                .orderByDesc("version")
                .orderByDesc("id")
                .last("limit 1");
        return this.getOne(wrapper, false);
    }
    private AiPromptTemplate findLatest(String sceneCode) {
        QueryWrapper<AiPromptTemplate> wrapper = new QueryWrapper<>();
        wrapper.eq("scene_code", sceneCode)
                .orderByDesc("version")
                .orderByDesc("id")
                .last("limit 1");
        return this.getOne(wrapper, false);
    }
    private int nextVersion(String sceneCode) {
        QueryWrapper<AiPromptTemplate> wrapper = new QueryWrapper<>();
        wrapper.eq("scene_code", sceneCode)
                .select("max(version) as version");
        Map<String, Object> row = this.getMap(wrapper);
        if (row == null || row.get("version") == null) {
            return 1;
        }
        Object value = row.get("version");
        if (value instanceof Number) {
            return ((Number) value).intValue() + 1;
        }
        return Integer.parseInt(String.valueOf(value)) + 1;
    }
    private Short normalizeStatus(Short status) {
        return status != null && status == 0 ? (short) 0 : (short) 1;
    }
    private String defaultName(AiPromptScene scene, Integer version, String name) {
        String value = trim(name);
        if (value != null && !value.isEmpty()) {
            return value;
        }
        return scene.getLabel() + " v" + version;
    }
    private AiPromptScene requireScene(String sceneCode) {
        String code = trim(sceneCode);
        AiPromptScene scene = AiPromptScene.ofCode(code);
        if (scene == null) {
            throw new IllegalArgumentException("不支持的 Prompt 场景: " + sceneCode);
        }
        return scene;
    }
    private String trim(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}
src/main/java/com/zy/ai/utils/AiPromptUtils.java
@@ -1,10 +1,29 @@
package com.zy.ai.utils;
import com.zy.ai.enums.AiPromptScene;
import org.springframework.stereotype.Component;
@Component
public class AiPromptUtils {
    public String getDefaultPrompt(String sceneCode) {
        AiPromptScene scene = AiPromptScene.ofCode(sceneCode);
        if (scene == null) {
            throw new IllegalArgumentException("不支持的 Prompt 场景: " + sceneCode);
        }
        return getDefaultPrompt(scene);
    }
    public String getDefaultPrompt(AiPromptScene scene) {
        if (scene == AiPromptScene.DIAGNOSE_STREAM) {
            return getAiDiagnosePromptMcp();
        }
        if (scene == AiPromptScene.SENSOR_CHAT) {
            return getWcsSensorPromptMcp();
        }
        throw new IllegalArgumentException("不支持的 Prompt 场景: " + scene.getCode());
    }
    //AI诊断系统Prompt
    public String getAiDiagnosePromptMcp() {
        String prompt = "你是一名资深 WCS(仓储控制系统)与自动化立库专家,熟悉:堆垛机、输送线、提升机、穿梭车等设备的任务分配和运行逻辑,也熟悉常见的系统卡死、任务不执行、设备空闲但无任务等问题模式。\n\n" +
src/main/resources/sql/20260312_create_sys_ai_prompt_template.sql
New file
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS `sys_ai_prompt_template` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` VARCHAR(128) NOT NULL COMMENT 'Prompt 名称',
  `scene_code` VARCHAR(64) NOT NULL COMMENT '场景编码',
  `version` INT NOT NULL COMMENT '版本号',
  `content` LONGTEXT NOT NULL COMMENT 'Prompt 内容',
  `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用0禁用',
  `published` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已发布:1是0否',
  `created_by` BIGINT DEFAULT NULL COMMENT '创建人',
  `published_by` BIGINT DEFAULT NULL COMMENT '发布人',
  `published_time` DATETIME DEFAULT NULL COMMENT '发布时间',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `memo` VARCHAR(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_sys_ai_prompt_scene_version` (`scene_code`, `version`),
  KEY `idx_sys_ai_prompt_scene_publish` (`scene_code`, `published`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI Prompt 模板版本表';