#AI
Junjie
5 小时以前 53fa1addcd22557f6541092a151a3064779d3f93
#AI
4个文件已修改
251 ■■■■■ 已修改文件
src/main/java/com/zy/ai/controller/WcsDiagnosisController.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/WcsDiagnosisService.java 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/enums/RedisKeyType.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/ai/diagnosis.html 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
@@ -3,10 +3,13 @@
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.zy.ai.entity.DeviceConfigsData;
import com.zy.ai.entity.DeviceRealTimeData;
import com.zy.ai.entity.ChatCompletionRequest;
import com.zy.ai.entity.WcsDiagnosisRequest;
import com.zy.ai.entity.WcsDiagnosisResponse;
import com.zy.ai.log.AiLogAppender;
import com.zy.ai.service.WcsDiagnosisService;
import com.core.annotations.ManagerAuth;
import com.zy.common.web.BaseController;
import com.zy.asrs.entity.BasCrnp;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.BasCrnpService;
@@ -33,7 +36,7 @@
@RestController
@RequestMapping("/ai/diagnose")
@RequiredArgsConstructor
public class WcsDiagnosisController {
public class WcsDiagnosisController extends BaseController {
    @Autowired
    private WcsDiagnosisService wcsDiagnosisService;
@@ -178,7 +181,9 @@
    }
    @GetMapping("/askStream")
    public SseEmitter askStream(@RequestParam("prompt") String prompt) {
    public SseEmitter askStream(@RequestParam("prompt") String prompt,
                                @RequestParam(value = "chatId", required = false) String chatId,
                                @RequestParam(value = "reset", required = false, defaultValue = "false") boolean reset) {
        SseEmitter emitter = new SseEmitter(0L);
        new Thread(() -> {
            try {
@@ -237,7 +242,7 @@
                request.setDeviceRealtimeData(deviceRealTimeDataList);
                request.setDeviceConfigs(deviceConfigsDataList);
                wcsDiagnosisService.askStream(request, prompt, emitter);
                wcsDiagnosisService.askStream(request, prompt, chatId, reset, emitter);
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
@@ -245,6 +250,21 @@
        return emitter;
    }
    @GetMapping("/chats")
    public List<Map<String, Object>> listChats() {
        return wcsDiagnosisService.listChats();
    }
    @DeleteMapping("/chats/{chatId}")
    public Boolean deleteChat(@PathVariable("chatId") String chatId) {
        return wcsDiagnosisService.deleteChat(chatId);
    }
    @GetMapping("/chats/{chatId}/history")
    public List<ChatCompletionRequest.Message> getChatHistory(@PathVariable("chatId") String chatId) {
        return wcsDiagnosisService.getChatHistory(chatId);
    }
    /**
     * POST /api/ai/diagnose/wcs
     */
src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -3,18 +3,23 @@
import com.alibaba.fastjson.JSON;
import com.zy.ai.entity.ChatCompletionRequest;
import com.zy.ai.entity.WcsDiagnosisRequest;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class WcsDiagnosisService {
    private final LlmChatService llmChatService;
    private final RedisUtil redisUtil;
    private static final long CHAT_TTL_SECONDS = 7L * 24 * 3600;
    /**
     * 针对“系统不执行任务 / 不知道哪个设备没在运行”的通用 AI 诊断
@@ -134,6 +139,162 @@
        });
    }
    public void askStream(WcsDiagnosisRequest request,
                          String prompt,
                          String chatId,
                          boolean reset,
                          SseEmitter emitter) {
        List<ChatCompletionRequest.Message> base = new ArrayList<>();
        ChatCompletionRequest.Message system = new ChatCompletionRequest.Message();
        system.setRole("system");
        system.setContent(
                "你是一名资深 WCS(仓储控制系统)与自动化立库专家,熟悉:堆垛机、输送线、提升机、穿梭车等设备的任务分配和运行逻辑,也熟悉常见的系统卡死、任务不执行、设备空闲但无任务等问题模式。\n\n" +
                "在回答用户问题时,需要结合下面给出的系统当前上下文信息(任务、设备实时状态、设备配置、系统日志等),以简洁、明确的中文作答,并在需要时给出可执行的排查建议。"
        );
        base.add(system);
        List<ChatCompletionRequest.Message> history = null;
        String historyKey = null;
        String metaKey = null;
        if (chatId != null && !chatId.isEmpty()) {
            historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId;
            metaKey = RedisKeyType.AI_CHAT_META.key + chatId;
            if (reset) {
                redisUtil.del(historyKey, metaKey);
            }
            List<Object> stored = redisUtil.lGet(historyKey, 0, -1);
            if (stored != null && !stored.isEmpty()) {
                history = new ArrayList<>(stored.size());
                for (Object o : stored) {
                    ChatCompletionRequest.Message m = convertToMessage(o);
                    if (m != null) history.add(m);
                }
                if (!history.isEmpty()) base.addAll(history);
            } else {
                history = new ArrayList<>();
            }
        }
        ChatCompletionRequest.Message contextMsg = new ChatCompletionRequest.Message();
        contextMsg.setRole("user");
        contextMsg.setContent(buildUserContent(request));
        base.add(contextMsg);
        ChatCompletionRequest.Message questionMsg = new ChatCompletionRequest.Message();
        questionMsg.setRole("user");
        questionMsg.setContent("【用户提问】\n" + (prompt == null ? "" : prompt));
        base.add(questionMsg);
        StringBuilder assistantBuffer = new StringBuilder();
        final String finalChatId = chatId;
        final String finalHistoryKey = historyKey;
        final String finalMetaKey = metaKey;
        final String finalPrompt = prompt;
        llmChatService.chatStream(base, 0.2, 2048, s -> {
            try {
                String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n");
                if (!safe.isEmpty()) {
                    emitter.send(SseEmitter.event().data(safe));
                    assistantBuffer.append(s);
                }
            } catch (Exception ignore) {}
        }, () -> {
            try {
                if (finalChatId != null && !finalChatId.isEmpty()) {
                    ChatCompletionRequest.Message q = new ChatCompletionRequest.Message();
                    q.setRole("user");
                    q.setContent(finalPrompt == null ? "" : finalPrompt);
                    ChatCompletionRequest.Message a = new ChatCompletionRequest.Message();
                    a.setRole("assistant");
                    a.setContent(assistantBuffer.toString());
                    redisUtil.lSet(finalHistoryKey, q);
                    redisUtil.lSet(finalHistoryKey, a);
                    redisUtil.expire(finalHistoryKey, CHAT_TTL_SECONDS);
                    Map<Object, Object> old = redisUtil.hmget(finalMetaKey);
                    Long createdAt = old != null && old.get("createdAt") != null ?
                            (old.get("createdAt") instanceof Number ? ((Number) old.get("createdAt")).longValue() : Long.valueOf(String.valueOf(old.get("createdAt"))))
                            : System.currentTimeMillis();
                    Map<String, Object> meta = new java.util.HashMap<>();
                    meta.put("chatId", finalChatId);
                    meta.put("title", buildTitleFromPrompt(finalPrompt));
                    meta.put("createdAt", createdAt);
                    meta.put("updatedAt", System.currentTimeMillis());
                    redisUtil.hmset(finalMetaKey, meta, CHAT_TTL_SECONDS);
                }
                emitter.complete();
            } catch (Exception ignore) {}
        }, e -> {
            try { emitter.completeWithError(e); } catch (Exception ignore) {}
        });
    }
    public List<Map<String, Object>> listChats() {
        java.util.Set<String> keys = redisUtil.scanKeys(RedisKeyType.AI_CHAT_META.key, 1000);
        List<Map<String, Object>> resp = new ArrayList<>();
        if (keys != null) {
            for (String key : keys) {
                Map<Object, Object> m = redisUtil.hmget(key);
                if (m != null && !m.isEmpty()) {
                    java.util.HashMap<String, Object> item = new java.util.HashMap<>();
                    for (Map.Entry<Object, Object> e : m.entrySet()) {
                        item.put(String.valueOf(e.getKey()), e.getValue());
                    }
                    String chatId = String.valueOf(item.get("chatId"));
                    String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId;
                    item.put("size", redisUtil.lGetListSize(historyKey));
                    resp.add(item);
                }
            }
        }
        return resp;
    }
    public boolean deleteChat(String chatId) {
        if (chatId == null || chatId.isEmpty()) return false;
        String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId;
        String metaKey = RedisKeyType.AI_CHAT_META.key + chatId;
        redisUtil.del(historyKey, metaKey);
        return true;
    }
    public List<ChatCompletionRequest.Message> getChatHistory(String chatId) {
        if (chatId == null || chatId.isEmpty()) return java.util.Collections.emptyList();
        String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId;
        List<Object> stored = redisUtil.lGet(historyKey, 0, -1);
        List<ChatCompletionRequest.Message> result = new ArrayList<>();
        if (stored != null) {
            for (Object o : stored) {
                ChatCompletionRequest.Message m = convertToMessage(o);
                if (m != null) result.add(m);
            }
        }
        return result;
    }
    private ChatCompletionRequest.Message convertToMessage(Object o) {
        if (o instanceof ChatCompletionRequest.Message) {
            return (ChatCompletionRequest.Message) o;
        }
        if (o instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) o;
            ChatCompletionRequest.Message m = new ChatCompletionRequest.Message();
            Object role = map.get("role");
            Object content = map.get("content");
            m.setRole(role == null ? null : String.valueOf(role));
            m.setContent(content == null ? null : String.valueOf(content));
            return m;
        }
        return null;
    }
    private String buildTitleFromPrompt(String prompt) {
        if (prompt == null || prompt.isEmpty()) return "未命名会话";
        String p = prompt.replaceAll("\n", " ").trim();
        return p.length() > 20 ? p.substring(0, 20) : p;
    }
    private String buildUserContent(WcsDiagnosisRequest request) {
        StringBuilder sb = new StringBuilder();
src/main/java/com/zy/core/enums/RedisKeyType.java
@@ -32,6 +32,8 @@
    CRN_IO_EXECUTE_FINISH_LIMIT("crn_io_execute_finish_limit_"),
    CURRENT_CIRCLE_TASK_CRN_NO("current_circle_task_crn_no_"),
    AI_CHAT_HISTORY("ai_chat_history_"),
    AI_CHAT_META("ai_chat_meta_"),
    ;
    public String key;
src/main/webapp/views/ai/diagnosis.html
@@ -9,7 +9,7 @@
    body { background: #f5f7fa; }
    .container { max-width: 1100px; margin: 24px auto; }
    .actions { display: flex; gap: 12px; align-items: center; }
    .output { height: 65vh; }
    .output { height: 60vh; }
    .markdown-body { font-size: 14px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; }
    .markdown-body p { margin: 4px 0; }
    .markdown-body ul, .markdown-body ol { margin: 4px 0 4px 16px; padding: 0; }
@@ -37,11 +37,16 @@
        <span>WCS AI 助手</span>
      </div>
      <div class="actions">
      <div class="actions" style="flex-wrap: wrap;">
        <el-button type="primary" :loading="loading" :disabled="streaming" @click="start">一键诊断系统</el-button>
        <el-button type="warning" :disabled="!streaming" @click="stop">停止</el-button>
        <el-button @click="clear">清空</el-button>
        <span class="status">{{ statusText }}</span>
        <el-button @click="clear">清空当前聊天</el-button>
        <span class="status" style="margin-right: 12px;">{{ statusText }}</span>
        <el-select v-model="currentChatId" placeholder="选择会话" style="min-width:240px;" @change="switchChat" :disabled="streaming">
          <el-option v-for="c in chats" :key="c.chatId" :label="(c.title||('会话 '+c.chatId)) + '('+(c.size||0)+')'" :value="c.chatId" />
        </el-select>
        <el-button type="success" plain icon="el-icon-plus" @click="newChat" :disabled="streaming">新会话</el-button>
        <el-button type="danger" plain icon="el-icon-delete" @click="deleteChat" :disabled="!currentChatId || streaming">删除会话</el-button>
      </div>
      <el-divider></el-divider>
@@ -102,7 +107,10 @@
          renderIntervalMs: 120,
          stepChars: 6,
          userInput: '',
          autoScrollThreshold: 80
          autoScrollThreshold: 80,
          chats: [],
          currentChatId: '',
          resetting: false
        };
      },
      computed: {
@@ -117,6 +125,42 @@
        }
      },
      methods: {
        loadChats: function() {
          var self = this;
          fetch(baseUrl + '/ai/diagnose/chats', { headers: { 'token': localStorage.getItem('token') } })
            .then(function(r){ return r.json(); })
            .then(function(arr){ if (Array.isArray(arr)) { self.chats = arr; } });
        },
        switchChat: function() {
          var self = this;
          if (!self.currentChatId) { self.clear(); return; }
          fetch(baseUrl + '/ai/diagnose/chats/' + encodeURIComponent(self.currentChatId) + '/history', { headers: { 'token': localStorage.getItem('token') } })
            .then(function(r){ return r.json(); })
            .then(function(arr){
              if (!Array.isArray(arr)) return;
              var msgs = [];
              for (var i=0;i<arr.length;i++) {
                var m = arr[i];
                if (m.role === 'assistant') msgs.push({ role: 'assistant', md: m.content || '', html: DOMPurify.sanitize(marked.parse((m.content||'').replace(/\\n/g,'\n'))), ts: self.nowStr() });
                else msgs.push({ role: 'user', text: m.content || '', ts: self.nowStr() });
              }
              self.messages = msgs;
              self.$nextTick(function(){ self.scrollToBottom(true); });
            });
        },
        newChat: function() {
          var id = Date.now() + '_' + Math.random().toString(36).substr(2,8);
          this.currentChatId = id;
          this.resetting = true;
          this.clear();
        },
        deleteChat: function() {
          var self = this;
          if (!self.currentChatId) return;
          fetch(baseUrl + '/ai/diagnose/chats/' + encodeURIComponent(self.currentChatId), { method: 'DELETE', headers: { 'token': localStorage.getItem('token') } })
            .then(function(r){ return r.json(); })
            .then(function(ok){ if (ok === true) { self.currentChatId = ''; self.clear(); self.loadChats(); self.newChat(); } });
        },
        shouldAutoScroll: function() {
          var el = this.$refs.chat;
          if (!el) return false;
@@ -150,6 +194,8 @@
          this.messages.push({ role: 'assistant', md: '', html: '', ts: this.nowStr() });
          this.scrollToBottom(true);
          var url = baseUrl + '/ai/diagnose/askStream?prompt=' + encodeURIComponent(msg);
          if (this.currentChatId) url += '&chatId=' + encodeURIComponent(this.currentChatId);
          if (this.resetting) url += '&reset=true';
          this.source = new EventSource(url);
          var self = this;
          this.source.onopen = function() {
@@ -169,6 +215,7 @@
            self.stop();
          };
          this.userInput = '';
          this.resetting = false;
        },
        start: function() {
          if (this.streaming) return;
@@ -246,12 +293,17 @@
            last.html = DOMPurify.sanitize(marked.parse(renderSource));
          }
          this.$nextTick(function() { this.scrollToBottom(true); }.bind(this));
          this.loadChats();
        },
        clear: function() {
          this.messages = [];
          this.pendingText = '';
        }
      }
      ,mounted: function() {
        this.loadChats();
        this.newChat();
      }
    });
  </script>
</body>