src/main/java/com/zy/ai/config/AiPromptTemplateInitializer.java
@@ -25,8 +25,8 @@ @PostConstruct public void init() { try (Connection connection = dataSource.getConnection()) { if (!hasTable(connection, "sys_ai_prompt_template")) { log.warn("Skip AI prompt initialization because table sys_ai_prompt_template does not exist"); if (!hasTable(connection, "sys_ai_prompt_template") || !hasTable(connection, "sys_ai_prompt_block")) { log.warn("Skip AI prompt initialization because prompt tables do not exist"); return; } int changed = aiPromptTemplateService.initDefaultsIfMissing(); src/main/java/com/zy/ai/controller/AiPromptTemplateController.java
@@ -45,7 +45,7 @@ wrapper.eq("status", status); } wrapper.orderByAsc("scene_code").orderByDesc("version").orderByDesc("id"); List<AiPromptTemplate> list = aiPromptTemplateService.list(wrapper); List<AiPromptTemplate> list = aiPromptTemplateService.enrichTemplates(aiPromptTemplateService.list(wrapper)); return R.ok(list); } @@ -53,7 +53,7 @@ @ManagerAuth public R active(@RequestParam("sceneCode") String sceneCode) { try { return R.ok(aiPromptTemplateService.resolvePublished(sceneCode)); return R.ok(aiPromptTemplateService.enrichTemplate(aiPromptTemplateService.resolvePublished(sceneCode))); } catch (IllegalArgumentException | IllegalStateException e) { return R.error(e.getMessage()); } @@ -63,7 +63,7 @@ @ManagerAuth public R save(@RequestBody AiPromptTemplate template) { try { return R.ok(aiPromptTemplateService.savePrompt(template, getUserId())); return R.ok(aiPromptTemplateService.enrichTemplate(aiPromptTemplateService.savePrompt(template, getUserId()))); } catch (IllegalArgumentException e) { return R.error(e.getMessage()); } @@ -73,7 +73,7 @@ @ManagerAuth public R publish(@RequestParam("id") Long id) { try { return R.ok(aiPromptTemplateService.publishPrompt(id, getUserId())); return R.ok(aiPromptTemplateService.enrichTemplate(aiPromptTemplateService.publishPrompt(id, getUserId()))); } catch (IllegalArgumentException e) { return R.error(e.getMessage()); } @@ -83,7 +83,7 @@ @ManagerAuth public R cancelPublish(@RequestParam("id") Long id) { try { return R.ok(aiPromptTemplateService.cancelPublish(id, getUserId())); return R.ok(aiPromptTemplateService.enrichTemplate(aiPromptTemplateService.cancelPublish(id, getUserId()))); } catch (IllegalArgumentException e) { return R.error(e.getMessage()); } src/main/java/com/zy/ai/entity/AiPromptBlock.java
New file @@ -0,0 +1,42 @@ 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_block") public class AiPromptBlock implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; @TableField("template_id") private Long templateId; @TableField("block_type") private String blockType; private String content; @TableField("sort_no") private Integer sortNo; /** * 1 启用 0 禁用 */ private Short status; @TableField("create_time") private Date createTime; @TableField("update_time") private Date updateTime; } src/main/java/com/zy/ai/entity/AiPromptTemplate.java
@@ -8,6 +8,7 @@ import java.io.Serializable; import java.util.Date; import java.util.List; @Data @TableName("sys_ai_prompt_template") @@ -53,4 +54,19 @@ private Date updateTime; private String memo; @TableField(exist = false) private String basePolicy; @TableField(exist = false) private String toolPolicy; @TableField(exist = false) private String outputContract; @TableField(exist = false) private String scenePlaybook; @TableField(exist = false) private List<AiPromptBlock> blocks; } src/main/java/com/zy/ai/enums/AiPromptBlockType.java
New file @@ -0,0 +1,49 @@ package com.zy.ai.enums; public enum AiPromptBlockType { BASE_POLICY("base_policy", "Base Policy", "基础策略", 10), TOOL_POLICY("tool_policy", "Tool Policy", "工具策略", 20), OUTPUT_CONTRACT("output_contract", "Output Contract", "输出约定", 30), SCENE_PLAYBOOK("scene_playbook", "Scene Playbook", "场景策略", 40); private final String code; private final String title; private final String label; private final int sort; AiPromptBlockType(String code, String title, String label, int sort) { this.code = code; this.title = title; this.label = label; this.sort = sort; } public String getCode() { return code; } public String getTitle() { return title; } public String getLabel() { return label; } public int getSort() { return sort; } public static AiPromptBlockType ofCode(String code) { if (code == null) { return null; } for (AiPromptBlockType item : values()) { if (item.code.equals(code)) { return item; } } return null; } } src/main/java/com/zy/ai/mapper/AiPromptBlockMapper.java
New file @@ -0,0 +1,11 @@ package com.zy.ai.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.zy.ai.entity.AiPromptBlock; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface AiPromptBlockMapper extends BaseMapper<AiPromptBlock> { } src/main/java/com/zy/ai/service/AiPromptComposerService.java
New file @@ -0,0 +1,8 @@ package com.zy.ai.service; import com.zy.ai.entity.AiPromptTemplate; public interface AiPromptComposerService { String compose(AiPromptTemplate template); } src/main/java/com/zy/ai/service/AiPromptTemplateService.java
@@ -16,6 +16,10 @@ AiPromptTemplate cancelPublish(Long id, Long operatorUserId); AiPromptTemplate enrichTemplate(AiPromptTemplate template); List<AiPromptTemplate> enrichTemplates(List<AiPromptTemplate> templates); boolean deletePrompt(Long id); int initDefaultsIfMissing(); src/main/java/com/zy/ai/service/impl/AiPromptComposerServiceImpl.java
New file @@ -0,0 +1,42 @@ package com.zy.ai.service.impl; import com.zy.ai.entity.AiPromptTemplate; import com.zy.ai.enums.AiPromptBlockType; import com.zy.ai.service.AiPromptComposerService; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service("aiPromptComposerService") public class AiPromptComposerServiceImpl implements AiPromptComposerService { @Override public String compose(AiPromptTemplate template) { if (template == null) { return ""; } List<String> sections = new ArrayList<>(); appendSection(sections, AiPromptBlockType.BASE_POLICY, template.getBasePolicy()); appendSection(sections, AiPromptBlockType.TOOL_POLICY, template.getToolPolicy()); appendSection(sections, AiPromptBlockType.OUTPUT_CONTRACT, template.getOutputContract()); appendSection(sections, AiPromptBlockType.SCENE_PLAYBOOK, template.getScenePlaybook()); return String.join("\n\n", sections).trim(); } private void appendSection(List<String> sections, AiPromptBlockType type, String content) { String value = trim(content); if (value == null) { return; } sections.add("【" + type.getLabel() + "】\n" + value); } 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/service/impl/AiPromptTemplateServiceImpl.java
@@ -3,9 +3,13 @@ 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.AiPromptBlock; import com.zy.ai.entity.AiPromptTemplate; import com.zy.ai.enums.AiPromptBlockType; import com.zy.ai.enums.AiPromptScene; import com.zy.ai.mapper.AiPromptBlockMapper; import com.zy.ai.mapper.AiPromptTemplateMapper; import com.zy.ai.service.AiPromptComposerService; import com.zy.ai.service.AiPromptTemplateService; import com.zy.ai.utils.AiPromptUtils; import lombok.RequiredArgsConstructor; @@ -14,8 +18,10 @@ import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Date; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -24,7 +30,20 @@ @RequiredArgsConstructor public class AiPromptTemplateServiceImpl extends ServiceImpl<AiPromptTemplateMapper, AiPromptTemplate> implements AiPromptTemplateService { private static final Comparator<AiPromptBlock> BLOCK_SORT = (a, b) -> { int sa = a != null && a.getSortNo() != null ? a.getSortNo() : Integer.MAX_VALUE; int sb = b != null && b.getSortNo() != null ? b.getSortNo() : Integer.MAX_VALUE; if (sa != sb) { return sa - sb; } long ia = a != null && a.getId() != null ? a.getId() : Long.MAX_VALUE; long ib = b != null && b.getId() != null ? b.getId() : Long.MAX_VALUE; return Long.compare(ia, ib); }; private final AiPromptUtils aiPromptUtils; private final AiPromptBlockMapper aiPromptBlockMapper; private final AiPromptComposerService aiPromptComposerService; @Override public AiPromptTemplate resolvePublished(String sceneCode) { @@ -33,18 +52,15 @@ if (prompt == null) { synchronized (("ai_prompt_scene_init_" + scene.getCode()).intern()) { prompt = findPublished(scene.getCode()); if (prompt == null) { if (findLatest(scene.getCode()) == null) { prompt = ensurePublishedScene(scene); } if (prompt == null && findLatest(scene.getCode()) == null) { prompt = ensurePublishedScene(scene); } } } if (prompt == null) { throw new IllegalStateException("当前场景没有已发布 Prompt,sceneCode=" + scene.getCode()); } return prompt; return enrichTemplate(prompt); } @Override @@ -54,9 +70,9 @@ throw new IllegalArgumentException("Prompt 不能为空"); } AiPromptScene scene = requireScene(template.getSceneCode()); String content = template.getContent(); if (content == null || content.trim().isEmpty()) { throw new IllegalArgumentException("Prompt 内容不能为空"); String compiled = buildCompiledPrompt(template); if (compiled.isEmpty()) { throw new IllegalArgumentException("Prompt 分段内容不能为空"); } if (template.getId() == null) { @@ -65,12 +81,13 @@ entity.setName(defaultName(scene, version, template.getName())); entity.setSceneCode(scene.getCode()); entity.setVersion(version); entity.setContent(content); entity.setContent(compiled); entity.setStatus(normalizeStatus(template.getStatus())); entity.setPublished((short) 0); entity.setCreatedBy(operatorUserId); entity.setMemo(trim(template.getMemo())); this.save(entity); upsertBlocks(entity.getId(), extractBlockContentMap(template)); return entity; } @@ -82,14 +99,15 @@ throw new IllegalArgumentException("不允许修改 Prompt 所属场景"); } if (Short.valueOf((short) 1).equals(db.getPublished())) { throw new IllegalArgumentException("已发布 Prompt 不允许直接修改,请新建版本后再发布"); throw new IllegalArgumentException("已发布 Prompt 不允许直接修改,请先取消发布后再保存"); } db.setName(defaultName(scene, db.getVersion() == null ? 1 : db.getVersion(), template.getName())); db.setContent(content); db.setContent(compiled); db.setStatus(normalizeStatus(template.getStatus())); db.setMemo(trim(template.getMemo())); this.updateById(db); upsertBlocks(db.getId(), extractBlockContentMap(template)); return db; } @@ -103,7 +121,9 @@ if (db == null) { throw new IllegalArgumentException("Prompt 不存在"); } if (db.getContent() == null || db.getContent().trim().isEmpty()) { db = enrichTemplate(db); String compiled = buildCompiledPrompt(db); if (compiled.isEmpty()) { throw new IllegalArgumentException("Prompt 内容不能为空"); } @@ -114,10 +134,8 @@ 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())); } db.setPublishedTime(new java.util.Date()); db.setContent(compiled); if (db.getName() == null || db.getName().trim().isEmpty()) { AiPromptScene scene = requireScene(db.getSceneCode()); db.setName(defaultName(scene, db.getVersion(), null)); @@ -147,6 +165,65 @@ } @Override public AiPromptTemplate enrichTemplate(AiPromptTemplate template) { if (template == null) { return null; } if (template.getId() == null) { template.setContent(buildCompiledPrompt(template)); return template; } List<AiPromptBlock> blocks = loadBlocks(template.getId()); if (blocks.isEmpty()) { migrateLegacyTemplateBlocks(template); blocks = loadBlocks(template.getId()); } applyBlocks(template, blocks); return template; } @Override public List<AiPromptTemplate> enrichTemplates(List<AiPromptTemplate> templates) { if (templates == null || templates.isEmpty()) { return templates == null ? Collections.emptyList() : templates; } List<Long> templateIds = new ArrayList<>(); for (AiPromptTemplate template : templates) { if (template != null && template.getId() != null) { templateIds.add(template.getId()); } } Map<Long, List<AiPromptBlock>> blockMap = groupBlocks(loadBlocks(templateIds)); boolean migrated = false; for (AiPromptTemplate template : templates) { if (template == null || template.getId() == null) { continue; } List<AiPromptBlock> blocks = blockMap.get(template.getId()); if (blocks == null || blocks.isEmpty()) { migrateLegacyTemplateBlocks(template); migrated = true; } } if (migrated) { blockMap = groupBlocks(loadBlocks(templateIds)); } for (AiPromptTemplate template : templates) { if (template == null) { continue; } if (template.getId() == null) { template.setContent(buildCompiledPrompt(template)); continue; } applyBlocks(template, blockMap.get(template.getId())); } return templates; } @Override @Transactional(rollbackFor = Exception.class) public boolean deletePrompt(Long id) { if (id == null) { @@ -157,8 +234,9 @@ return false; } if (Short.valueOf((short) 1).equals(db.getPublished())) { throw new IllegalArgumentException("已发布 Prompt 不允许删除,请先发布其他版本"); throw new IllegalArgumentException("已发布 Prompt 不允许删除,请先取消发布"); } aiPromptBlockMapper.delete(new QueryWrapper<AiPromptBlock>().eq("template_id", id)); return this.removeById(id); } @@ -170,6 +248,12 @@ AiPromptTemplate latest = findLatest(scene.getCode()); if (latest == null) { ensurePublishedScene(scene); changed++; continue; } List<AiPromptBlock> blocks = loadBlocks(latest.getId()); if (blocks.isEmpty()) { migrateLegacyTemplateBlocks(latest); changed++; } } @@ -189,33 +273,145 @@ } 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; LinkedHashMap<AiPromptBlockType, String> blocks = aiPromptUtils.getDefaultPromptBlocks(scene); AiPromptTemplate seed = new AiPromptTemplate(); seed.setName(defaultName(scene, 1, null)); seed.setSceneCode(scene.getCode()); seed.setVersion(1); seed.setStatus((short) 1); seed.setPublished((short) 1); seed.setPublishedTime(new java.util.Date()); seed.setMemo("系统初始化默认 Prompt"); applyBlockFields(seed, blocks); seed.setContent(buildCompiledPrompt(seed)); this.save(seed); upsertBlocks(seed.getId(), blocks); log.info("Initialized default AI prompt blocks, sceneCode={}, version={}", scene.getCode(), seed.getVersion()); return seed; } private void migrateLegacyTemplateBlocks(AiPromptTemplate template) { if (template == null || template.getId() == null) { return; } AiPromptScene scene = requireScene(template.getSceneCode()); LinkedHashMap<AiPromptBlockType, String> blocks = aiPromptUtils.resolveStoredOrDefaultBlocks(scene, template.getContent()); upsertBlocks(template.getId(), blocks); applyBlockFields(template, blocks); template.setContent(buildCompiledPrompt(template)); this.updateById(template); } private void applyBlocks(AiPromptTemplate template, List<AiPromptBlock> blocks) { List<AiPromptBlock> ordered = blocks == null ? new ArrayList<>() : new ArrayList<>(blocks); ordered.sort(BLOCK_SORT); template.setBlocks(ordered); LinkedHashMap<AiPromptBlockType, String> blockContent = new LinkedHashMap<>(); for (AiPromptBlockType type : AiPromptBlockType.values()) { blockContent.put(type, ""); } for (AiPromptBlock block : ordered) { AiPromptBlockType type = AiPromptBlockType.ofCode(block.getBlockType()); if (type == null) { continue; } blockContent.put(type, block.getContent()); } applyBlockFields(template, blockContent); template.setContent(buildCompiledPrompt(template)); } private void applyBlockFields(AiPromptTemplate template, LinkedHashMap<AiPromptBlockType, String> blockContent) { template.setBasePolicy(valueOf(blockContent, AiPromptBlockType.BASE_POLICY)); template.setToolPolicy(valueOf(blockContent, AiPromptBlockType.TOOL_POLICY)); template.setOutputContract(valueOf(blockContent, AiPromptBlockType.OUTPUT_CONTRACT)); template.setScenePlaybook(valueOf(blockContent, AiPromptBlockType.SCENE_PLAYBOOK)); } private String valueOf(LinkedHashMap<AiPromptBlockType, String> blockContent, AiPromptBlockType type) { if (blockContent == null) { return ""; } String value = blockContent.get(type); return value == null ? "" : value; } private String buildCompiledPrompt(AiPromptTemplate template) { String compiled = aiPromptComposerService.compose(template); return compiled == null ? "" : compiled.trim(); } private LinkedHashMap<AiPromptBlockType, String> extractBlockContentMap(AiPromptTemplate template) { LinkedHashMap<AiPromptBlockType, String> blocks = new LinkedHashMap<>(); blocks.put(AiPromptBlockType.BASE_POLICY, defaultString(template.getBasePolicy())); blocks.put(AiPromptBlockType.TOOL_POLICY, defaultString(template.getToolPolicy())); blocks.put(AiPromptBlockType.OUTPUT_CONTRACT, defaultString(template.getOutputContract())); blocks.put(AiPromptBlockType.SCENE_PLAYBOOK, defaultString(template.getScenePlaybook())); return blocks; } private void upsertBlocks(Long templateId, LinkedHashMap<AiPromptBlockType, String> blockContent) { List<AiPromptBlock> existingBlocks = loadBlocks(templateId); HashMap<String, AiPromptBlock> existingMap = new HashMap<>(); for (AiPromptBlock block : existingBlocks) { existingMap.put(block.getBlockType(), block); } 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()); for (AiPromptBlockType type : AiPromptBlockType.values()) { AiPromptBlock block = existingMap.get(type.getCode()); if (block == null) { block = new AiPromptBlock(); block.setTemplateId(templateId); block.setBlockType(type.getCode()); block.setSortNo(type.getSort()); block.setStatus((short) 1); block.setContent(defaultString(blockContent.get(type))); aiPromptBlockMapper.insert(block); continue; } block.setSortNo(type.getSort()); block.setStatus((short) 1); block.setContent(defaultString(blockContent.get(type))); aiPromptBlockMapper.updateById(block); } this.updateById(latest); return latest; } private List<AiPromptBlock> loadBlocks(Long templateId) { if (templateId == null) { return Collections.emptyList(); } return aiPromptBlockMapper.selectList(new QueryWrapper<AiPromptBlock>() .eq("template_id", templateId) .orderByAsc("sort_no") .orderByAsc("id")); } private List<AiPromptBlock> loadBlocks(List<Long> templateIds) { if (templateIds == null || templateIds.isEmpty()) { return Collections.emptyList(); } return aiPromptBlockMapper.selectList(new QueryWrapper<AiPromptBlock>() .in("template_id", templateIds) .orderByAsc("sort_no") .orderByAsc("id")); } private Map<Long, List<AiPromptBlock>> groupBlocks(List<AiPromptBlock> blocks) { HashMap<Long, List<AiPromptBlock>> result = new HashMap<>(); if (blocks == null) { return result; } for (AiPromptBlock block : blocks) { if (block == null || block.getTemplateId() == null) { continue; } result.computeIfAbsent(block.getTemplateId(), k -> new ArrayList<>()).add(block); } for (List<AiPromptBlock> list : result.values()) { list.sort(BLOCK_SORT); } return result; } private AiPromptTemplate findPublished(String sceneCode) { @@ -281,4 +477,8 @@ String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } private String defaultString(String value) { return value == null ? "" : value; } } src/main/java/com/zy/ai/utils/AiPromptUtils.java
@@ -1,7 +1,10 @@ package com.zy.ai.utils; import com.zy.ai.enums.AiPromptBlockType; import com.zy.ai.enums.AiPromptScene; import org.springframework.stereotype.Component; import java.util.LinkedHashMap; @Component public class AiPromptUtils { @@ -24,6 +27,122 @@ throw new IllegalArgumentException("不支持的 Prompt 场景: " + scene.getCode()); } public LinkedHashMap<AiPromptBlockType, String> getDefaultPromptBlocks(String sceneCode) { AiPromptScene scene = AiPromptScene.ofCode(sceneCode); if (scene == null) { throw new IllegalArgumentException("不支持的 Prompt 场景: " + sceneCode); } return getDefaultPromptBlocks(scene); } public LinkedHashMap<AiPromptBlockType, String> getDefaultPromptBlocks(AiPromptScene scene) { LinkedHashMap<AiPromptBlockType, String> blocks = new LinkedHashMap<>(); if (scene == AiPromptScene.DIAGNOSE_STREAM) { blocks.put(AiPromptBlockType.BASE_POLICY, "你是一名资深 WCS(仓储控制系统)与自动化立库专家,熟悉:堆垛机、输送线、提升机、穿梭车等设备的任务分配和运行逻辑,也熟悉常见的系统卡死、任务不执行、设备空闲但无任务等问题模式。"); blocks.put(AiPromptBlockType.TOOL_POLICY, "你可以按需调用系统提供的工具以获取实时数据与上下文(工具返回 JSON):\n" + "- 任务:task_query\n" + "- 设备实时状态:device_get_crn_status / device_get_station_status / device_get_rgv_status\n" + "- 日志:log_query\n" + "- 设备配置:config_get_device_config\n" + "- 系统配置:config_get_system_config\n\n" + "使用策略:\n" + "1)避免臆测。如信息不足,先调用相应工具收集必要数据;可多轮调用。\n" + "2)对工具返回的 JSON 先进行结构化归纳,提炼关键字段,再做推理。\n" + "3)优先顺序:任务→设备状态→日志→配置;按需调整。\n\n" + "如需要额外数据,请先调用合适的工具再继续回答。"); blocks.put(AiPromptBlockType.OUTPUT_CONTRACT, "请按以下结构输出诊断结果(使用简体中文):\n" + "1. 问题概述(1-3 句话,概括当前系统状态)\n" + "2. 可疑设备列表(列出 1-N 个设备编号,并说明每个设备为什么可疑,例如:配置禁用/长时间空闲/状态异常/任务分配不到它等)\n" + "3. 可能原因(从任务分配、设备状态、配置错误、接口/通信异常等角度,列出 3-7 条)\n" + "4. 建议排查步骤(步骤 1、2、3...,每步要尽量具体、可操作,例如:在某页面查看某字段、检查某个开关、对比某个状态位等)\n" + "5. 风险评估(说明当前问题对业务影响程度:高/中/低,以及是否需要立即人工干预)"); blocks.put(AiPromptBlockType.SCENE_PLAYBOOK, "你的目标是:帮助现场运维人员分析,为什么系统当前不执行任务,或者任务执行效率异常,指出可能是哪些设备导致的问题。"); return blocks; } if (scene == AiPromptScene.SENSOR_CHAT) { blocks.put(AiPromptBlockType.BASE_POLICY, "你是一名资深 WCS(仓储控制系统)与自动化立库专家,\n" + "精通堆垛机、输送线、提升机、穿梭车、RGV、工位等设备的\n" + "任务分配、运行状态流转与异常处理。\n\n" + "你的职责是:**基于实时数据进行工程级诊断,而不是凭经验猜测。**"); blocks.put(AiPromptBlockType.TOOL_POLICY, "==================== 工作规则(非常重要) ====================\n\n" + "1. **禁止在未获取实时数据的情况下直接下结论。**\n" + " - 若问题涉及“当前状态 / 是否卡死 / 是否有任务 / 是否异常”,\n" + " 你必须先调用工具获取数据,再进行分析。\n\n" + "2. **优先使用最少且最相关的工具调用。**\n" + " - 整体诊断时,先查任务与关键设备状态。\n" + " - 需要补证据时,再查日志或配置。\n\n" + "3. **当信息不足以判断时,不得猜测原因。**\n" + " - 必须明确指出“缺少哪些数据”,并调用对应工具获取。\n\n" + "4. **工具返回的数据是事实依据,必须引用其关键信息进行推理。**\n\n" + "==================== 可用工具(返回 JSON) ====================\n\n" + "【任务相关】\n" + "- task_query:按任务号、状态、设备、条码、库位等条件查询任务\n" + "\n【设备实时状态】\n" + "- device_get_crn_status:堆垛机实时状态\n" + "- device_get_station_status:工位实时状态\n" + "- device_get_rgv_status:RGV / 穿梭车实时状态\n" + "\n【日志】\n" + "- log_query:查询系统/设备日志\n" + "\n【配置】\n" + "- config_get_device_config:设备配置\n" + "- config_get_system_config:系统级配置"); blocks.put(AiPromptBlockType.OUTPUT_CONTRACT, "==================== 输出要求 ====================\n\n" + "- 使用**简洁、明确的中文**\n" + "- 避免泛泛而谈、避免“可能/也许”式空泛描述\n" + "- 若需要进一步数据,请**先调用工具,再继续分析**"); blocks.put(AiPromptBlockType.SCENE_PLAYBOOK, "==================== 推荐诊断流程 ====================\n\n" + "当接到诊断请求时,请遵循以下步骤:\n\n" + "Step 1 明确诊断目标\n" + "- 当前要判断的是:设备是否异常?任务是否卡死?调度是否阻塞?\n\n" + "Step 2 调用必要工具获取事实数据\n" + "- 设备状态 → 是否在线 / 是否空闲 / 当前任务\n" + "- 任务状态 → 是否存在待执行/挂起任务\n" + "- 日志 → 是否存在关键异常、等待确认、命令未响应等信息\n\n" + "Step 3 基于数据进行逻辑分析\n" + "- 使用 WCS 专业知识进行因果判断(而非猜测)\n\n" + "Step 4 输出结构化结论\n" + "- 【现象总结】\n" + "- 【关键证据(来自工具返回)】\n" + "- 【可能原因(按优先级)】\n" + "- 【可执行的排查 / 处理建议】"); return blocks; } throw new IllegalArgumentException("不支持的 Prompt 场景: " + scene.getCode()); } public LinkedHashMap<AiPromptBlockType, String> resolveStoredOrDefaultBlocks(AiPromptScene scene, String legacyContent) { String content = trim(legacyContent); if (content == null) { return getDefaultPromptBlocks(scene); } if ((scene == AiPromptScene.DIAGNOSE_STREAM && content.equals(getAiDiagnosePromptMcp())) || (scene == AiPromptScene.SENSOR_CHAT && content.equals(getWcsSensorPromptMcp()))) { return getDefaultPromptBlocks(scene); } LinkedHashMap<AiPromptBlockType, String> blocks = new LinkedHashMap<>(); blocks.put(AiPromptBlockType.BASE_POLICY, ""); blocks.put(AiPromptBlockType.TOOL_POLICY, ""); blocks.put(AiPromptBlockType.OUTPUT_CONTRACT, ""); blocks.put(AiPromptBlockType.SCENE_PLAYBOOK, content); return blocks; } private String trim(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } //AI诊断系统Prompt public String getAiDiagnosePromptMcp() { String prompt = "你是一名资深 WCS(仓储控制系统)与自动化立库专家,熟悉:堆垛机、输送线、提升机、穿梭车等设备的任务分配和运行逻辑,也熟悉常见的系统卡死、任务不执行、设备空闲但无任务等问题模式。\n\n" + src/main/resources/sql/20260312_create_sys_ai_prompt_block.sql
New file @@ -0,0 +1,13 @@ CREATE TABLE IF NOT EXISTS `sys_ai_prompt_block` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `template_id` BIGINT NOT NULL COMMENT '模板ID', `block_type` VARCHAR(64) NOT NULL COMMENT '分段类型', `content` LONGTEXT NOT NULL COMMENT '分段内容', `sort_no` INT NOT NULL DEFAULT 0 COMMENT '排序号', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用0禁用', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_sys_ai_prompt_block_template_type` (`template_id`, `block_type`), KEY `idx_sys_ai_prompt_block_template_sort` (`template_id`, `sort_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI Prompt 模板分段表'; src/main/webapp/views/ai/prompt_config.html
@@ -265,14 +265,33 @@ font-size: 12px; color: #6f859d; } .editor-textarea .el-textarea__inner { min-height: 430px !important; .block-textarea .el-textarea__inner { min-height: 190px !important; font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 12px; line-height: 1.7; color: #243447; background: #fbfdff; border-color: #dbe7f3; } .preview-textarea .el-textarea__inner { min-height: 260px !important; font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 12px; line-height: 1.75; color: #203246; background: linear-gradient(180deg, #fcfdff 0%, #f7fbff 100%); border-color: #d5e3f1; } .dynamic-context-card { margin-top: 10px; padding: 10px 12px; border-radius: 12px; border: 1px solid #e2ebf5; background: linear-gradient(180deg, #fdfefe 0%, #f5fbfa 100%); color: #5f7288; font-size: 12px; line-height: 1.75; } .editor-hint { margin-top: 10px; @@ -512,14 +531,56 @@ <div class="field-label">备注</div> <el-input v-model="editor.memo" size="mini" placeholder="记录这次优化目的或测试结论"></el-input> </div> <div class="editor-full"> <div class="field-label">Prompt内容</div> <div> <div class="field-label">Base Policy</div> <el-input class="editor-textarea" class="block-textarea" type="textarea" v-model="editor.content" placeholder="在这里编辑 Prompt 内容" :autosize="{ minRows: 20, maxRows: 28 }"></el-input> v-model="editor.basePolicy" placeholder="角色、底线规则、稳定约束" :autosize="{ minRows: 8, maxRows: 14 }"></el-input> </div> <div> <div class="field-label">Tool Policy</div> <el-input class="block-textarea" type="textarea" v-model="editor.toolPolicy" placeholder="什么时候必须查 MCP、怎么使用工具和引用证据" :autosize="{ minRows: 8, maxRows: 14 }"></el-input> </div> <div class="editor-full"> <div class="field-label">Output Contract</div> <el-input class="block-textarea" type="textarea" v-model="editor.outputContract" placeholder="输出格式、结构、禁止事项、工程化要求" :autosize="{ minRows: 7, maxRows: 12 }"></el-input> </div> <div class="editor-full"> <div class="field-label">Scene Playbook</div> <el-input class="block-textarea" type="textarea" v-model="editor.scenePlaybook" placeholder="场景策略,比如任务不执行、设备异常、人工问答等" :autosize="{ minRows: 10, maxRows: 16 }"></el-input> </div> <div class="editor-full"> <div class="field-label">组装预览</div> <el-input class="preview-textarea" type="textarea" :value="assembledPromptPreview" readonly :autosize="{ minRows: 12, maxRows: 18 }"></el-input> </div> <div class="editor-full"> <div class="dynamic-context-card"> <strong>Dynamic Context</strong><br /> 这一层不在这里持久化维护。运行时仍由请求实时注入,比如:用户问题、告警描述、重点设备、日志范围、extraContext,以及 Agent 后续通过 MCP 拉到的实时事实数据。 </div> </div> </div> @@ -540,7 +601,7 @@ 当前正在编辑草稿 v{{ editor.version || '-' }}。保存只会更新这份草稿,发布后它会替换当前场景的线上版本。 </div> <div v-else> 当前是新草稿。你一新建它就会出现在左侧版本列表里;可以先保存,再单独发布。 当前是新草稿。你现在编辑的是 4 个持久化层:Base Policy、Tool Policy、Output Contract、Scene Playbook;Dynamic Context 继续由运行时注入。 </div> </div> </div> @@ -570,6 +631,10 @@ sceneCode: '', version: null, content: '', basePolicy: '', toolPolicy: '', outputContract: '', scenePlaybook: '', status: 1, published: 0, memo: '', @@ -622,19 +687,22 @@ return '先从左侧选择场景,或者直接新建草稿。'; } if (this.editor.published === 1) { return '已发布版本只作为查看和复制来源,不直接覆盖修改。'; return '当前是已发布版本,4 层内容只能查看;若要修改,先取消发布。'; } if (this.editor.id) { return '正在编辑已有草稿,保存后不会影响线上版本,只有发布才会切换。'; return '正在编辑已有草稿。修改的是 Prompt 的 4 个持久化层,保存不会影响线上版本。'; } return '当前是新草稿,可以自由编辑并保存。'; return '当前是新草稿。你可以分层编辑 Prompt,再通过右侧预览检查最终拼接效果。'; }, assembledPromptPreview: function() { return this.composePromptPreview(this.editor); }, contentCharCount: function() { return this.editor.content ? this.editor.content.length : 0; return this.assembledPromptPreview ? this.assembledPromptPreview.length : 0; }, contentLineCount: function() { if (!this.editor.content) return 0; return this.editor.content.split(/\r?\n/).length; if (!this.assembledPromptPreview) return 0; return this.assembledPromptPreview.split(/\r?\n/).length; } }, methods: { @@ -645,6 +713,10 @@ sceneCode: '', version: null, content: '', basePolicy: '', toolPolicy: '', outputContract: '', scenePlaybook: '', status: 1, published: 0, memo: '', @@ -686,6 +758,10 @@ sceneCode: item && item.sceneCode ? item.sceneCode : '', version: item && item.version != null ? item.version : null, content: item && item.content ? item.content : '', basePolicy: item && item.basePolicy ? item.basePolicy : '', toolPolicy: item && item.toolPolicy ? item.toolPolicy : '', outputContract: item && item.outputContract ? item.outputContract : '', scenePlaybook: item && item.scenePlaybook ? item.scenePlaybook : '', status: item && item.status != null ? item.status : 1, published: item && item.published != null ? item.published : 0, memo: item && item.memo ? item.memo : '', @@ -722,6 +798,19 @@ }, buildLocalDraftKey: function() { return 'draft_' + Date.now() + '_' + Math.floor(Math.random() * 100000); }, composePromptPreview: function(editor) { var sections = []; var append = function(label, content) { var value = content == null ? '' : String(content).trim(); if (!value) return; sections.push('【' + label + '】\n' + value); }; append('基础策略', editor && editor.basePolicy); append('工具策略', editor && editor.toolPolicy); append('输出约定', editor && editor.outputContract); append('场景策略', editor && editor.scenePlaybook); return sections.join('\n\n'); }, hasUnsavedEditorForScene: function(sceneCode) { return !!(this.editor @@ -885,15 +974,14 @@ this.cloneFromTemplate(this.currentPublishedTemplate); }, buildSavePayload: function() { var payloadId = this.editor.id; if (this.editor.published === 1) { payloadId = null; } return { id: payloadId, id: this.editor.id, name: this.editor.name, sceneCode: this.editor.sceneCode, content: this.editor.content, basePolicy: this.editor.basePolicy, toolPolicy: this.editor.toolPolicy, outputContract: this.editor.outputContract, scenePlaybook: this.editor.scenePlaybook, status: this.editor.status, memo: this.editor.memo }; @@ -911,8 +999,8 @@ self.$message.warning('请选择场景'); return; } if (!self.editor.content || !self.editor.content.trim()) { self.$message.warning('Prompt内容不能为空'); if (!self.assembledPromptPreview || !self.assembledPromptPreview.trim()) { self.$message.warning('至少填写一个 Prompt 分段'); return; } self.saving = true;