| src/main/java/com/zy/ai/controller/AiPromptTemplateController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/entity/AiPromptTemplate.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/enums/AiPromptScene.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/mapper/AiPromptTemplateMapper.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/AiPromptTemplateService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/WcsDiagnosisService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/impl/AiPromptTemplateServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/utils/AiPromptUtils.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/sql/20260312_create_sys_ai_prompt_template.sql | ●●●●● 补丁 | 查看 | 原始文档 | 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 模板版本表';