| | |
| | | package com.zy.ai.controller; |
| | | |
| | | 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.zy.asrs.entity.BasCrnp; |
| | | import com.zy.asrs.entity.WrkMast; |
| | | import com.zy.asrs.service.BasCrnpService; |
| | | import com.zy.asrs.service.WrkMastService; |
| | | import com.zy.core.cache.SlaveConnection; |
| | | import com.zy.core.enums.SlaveType; |
| | | import com.zy.core.model.StationObjModel; |
| | | import com.zy.core.model.protocol.CrnProtocol; |
| | | import com.zy.core.model.protocol.StationProtocol; |
| | | import com.zy.core.thread.CrnThread; |
| | | import com.zy.core.thread.StationThread; |
| | | import com.zy.ai.utils.AiUtils; |
| | | import com.zy.common.web.BaseController; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | |
| | | @RestController |
| | | @RequestMapping("/ai/diagnose") |
| | | @RequiredArgsConstructor |
| | | public class WcsDiagnosisController { |
| | | public class WcsDiagnosisController extends BaseController { |
| | | |
| | | @Autowired |
| | | private WcsDiagnosisService wcsDiagnosisService; |
| | | @Autowired |
| | | private WrkMastService wrkMastService; |
| | | @Autowired |
| | | private BasCrnpService basCrnpService; |
| | | private AiUtils aiUtils; |
| | | |
| | | @GetMapping("/runAi") |
| | | public WcsDiagnosisResponse runAi() { |
| | | WcsDiagnosisRequest request = new WcsDiagnosisRequest(); |
| | | |
| | | request.setAlarmMessage("系统不执行任务"); |
| | | |
| | | List<String> logs = AiLogAppender.getRecentLogs(100); |
| | | request.setLogs(logs); |
| | | |
| | | List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<>()); |
| | | request.setTasks(wrkMasts); |
| | | |
| | | List<DeviceRealTimeData> deviceRealTimeDataList = new ArrayList<>(); |
| | | List<DeviceConfigsData> deviceConfigsDataList = new ArrayList<>(); |
| | | |
| | | List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<>()); |
| | | for (BasCrnp basCrnp : basCrnps) { |
| | | CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo()); |
| | | if (crnThread == null) { |
| | | continue; |
| | | } |
| | | |
| | | CrnProtocol protocol = crnThread.getStatus(); |
| | | |
| | | for (StationObjModel stationObjModel : basCrnp.getInStationList$()) { |
| | | StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo()); |
| | | if (stationThread == null) { |
| | | continue; |
| | | } |
| | | |
| | | Map<Integer, StationProtocol> map = stationThread.getStatusMap(); |
| | | StationProtocol stationProtocol = map.get(stationObjModel.getStationId()); |
| | | if (stationProtocol == null) { |
| | | continue; |
| | | } |
| | | |
| | | DeviceRealTimeData stationData = new DeviceRealTimeData(); |
| | | stationData.setDeviceNo(stationObjModel.getDeviceNo()); |
| | | stationData.setDeviceType(String.valueOf(SlaveType.Devp)); |
| | | stationData.setDeviceData(stationProtocol); |
| | | deviceRealTimeDataList.add(stationData); |
| | | } |
| | | |
| | | |
| | | DeviceRealTimeData deviceRealTimeData = new DeviceRealTimeData(); |
| | | deviceRealTimeData.setDeviceNo(basCrnp.getCrnNo()); |
| | | deviceRealTimeData.setDeviceType(String.valueOf(SlaveType.Crn)); |
| | | deviceRealTimeData.setDeviceData(protocol); |
| | | deviceRealTimeDataList.add(deviceRealTimeData); |
| | | |
| | | DeviceConfigsData deviceConfigsData = new DeviceConfigsData(); |
| | | deviceConfigsData.setDeviceNo(basCrnp.getCrnNo()); |
| | | deviceConfigsData.setDeviceType(String.valueOf(SlaveType.Crn)); |
| | | deviceConfigsData.setDeviceData(basCrnp); |
| | | deviceConfigsDataList.add(deviceConfigsData); |
| | | } |
| | | |
| | | request.setDeviceRealtimeData(deviceRealTimeDataList); |
| | | request.setDeviceConfigs(deviceConfigsDataList); |
| | | WcsDiagnosisRequest request = aiUtils.makeAiRequest(1000, "系统当前不执行任务,但具体原因不明,请根据以下信息帮助判断。\n\n"); |
| | | WcsDiagnosisResponse response = diagnose(request); |
| | | return response; |
| | | } |
| | |
| | | |
| | | new Thread(() -> { |
| | | try { |
| | | WcsDiagnosisRequest request = new WcsDiagnosisRequest(); |
| | | request.setAlarmMessage("系统不执行任务"); |
| | | |
| | | List<String> logs = AiLogAppender.getRecentLogs(100); |
| | | request.setLogs(logs); |
| | | |
| | | List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<>()); |
| | | request.setTasks(wrkMasts); |
| | | |
| | | List<DeviceRealTimeData> deviceRealTimeDataList = new ArrayList<>(); |
| | | List<DeviceConfigsData> deviceConfigsDataList = new ArrayList<>(); |
| | | |
| | | List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<>()); |
| | | for (BasCrnp basCrnp : basCrnps) { |
| | | CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo()); |
| | | if (crnThread == null) { |
| | | continue; |
| | | } |
| | | |
| | | CrnProtocol protocol = crnThread.getStatus(); |
| | | |
| | | for (StationObjModel stationObjModel : basCrnp.getInStationList$()) { |
| | | StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo()); |
| | | if (stationThread == null) { |
| | | continue; |
| | | } |
| | | |
| | | Map<Integer, StationProtocol> map = stationThread.getStatusMap(); |
| | | StationProtocol stationProtocol = map.get(stationObjModel.getStationId()); |
| | | if (stationProtocol == null) { |
| | | continue; |
| | | } |
| | | |
| | | DeviceRealTimeData stationData = new DeviceRealTimeData(); |
| | | stationData.setDeviceNo(stationObjModel.getDeviceNo()); |
| | | stationData.setDeviceType(String.valueOf(SlaveType.Devp)); |
| | | stationData.setDeviceData(stationProtocol); |
| | | deviceRealTimeDataList.add(stationData); |
| | | } |
| | | |
| | | |
| | | DeviceRealTimeData deviceRealTimeData = new DeviceRealTimeData(); |
| | | deviceRealTimeData.setDeviceNo(basCrnp.getCrnNo()); |
| | | deviceRealTimeData.setDeviceType(String.valueOf(SlaveType.Crn)); |
| | | deviceRealTimeData.setDeviceData(protocol); |
| | | deviceRealTimeDataList.add(deviceRealTimeData); |
| | | |
| | | DeviceConfigsData deviceConfigsData = new DeviceConfigsData(); |
| | | deviceConfigsData.setDeviceNo(basCrnp.getCrnNo()); |
| | | deviceConfigsData.setDeviceType(String.valueOf(SlaveType.Crn)); |
| | | deviceConfigsData.setDeviceData(basCrnp); |
| | | deviceConfigsDataList.add(deviceConfigsData); |
| | | } |
| | | |
| | | request.setDeviceRealtimeData(deviceRealTimeDataList); |
| | | request.setDeviceConfigs(deviceConfigsDataList); |
| | | |
| | | WcsDiagnosisRequest request = aiUtils.makeAiRequest(1000, "系统当前不执行任务,但具体原因不明,请根据以下信息帮助判断。\n\n"); |
| | | wcsDiagnosisService.diagnoseStream(request, emitter); |
| | | } catch (Exception e) { |
| | | emitter.completeWithError(e); |
| | |
| | | return emitter; |
| | | } |
| | | |
| | | @GetMapping("/askStream") |
| | | 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 { |
| | | WcsDiagnosisRequest request = aiUtils.makeAiRequest(100, null); |
| | | wcsDiagnosisService.askStream(request, prompt, chatId, reset, emitter); |
| | | } catch (Exception e) { |
| | | emitter.completeWithError(e); |
| | | } |
| | | }).start(); |
| | | 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 |
| | | */ |
| | |
| | | package com.zy.ai.log; |
| | | |
| | | import ch.qos.logback.classic.spi.ILoggingEvent; |
| | | import ch.qos.logback.classic.spi.ThrowableProxyUtil; |
| | | import ch.qos.logback.core.AppenderBase; |
| | | |
| | | import java.time.Instant; |
| | |
| | | message |
| | | ); |
| | | |
| | | String throwable = event.getThrowableProxy() != null ? ThrowableProxyUtil.asString(event.getThrowableProxy()) : null; |
| | | if (throwable != null && !throwable.isEmpty()) { |
| | | logLine = logLine + System.lineSeparator() + throwable; |
| | | } |
| | | |
| | | // 放进环形缓冲区 |
| | | if (LOG_BUFFER.remainingCapacity() == 0) { |
| | | LOG_BUFFER.pollFirst(); // 移除最旧的 |
| | |
| | | .skip(skip) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | } |
| | | } |
| | |
| | | .doOnError(ex -> log.error("调用 LLM 流式失败", ex)); |
| | | |
| | | flux.subscribe(payload -> { |
| | | String s = payload == null ? null : payload.trim(); |
| | | String s = payload; |
| | | if (s == null || s.isEmpty()) return; |
| | | if (s.startsWith("data:")) s = s.substring(5).trim(); |
| | | if ("[DONE]".equals(s)) return; |
| | | if (s.startsWith("data:")) { |
| | | s = s.substring(5); |
| | | if (s.startsWith(" ")) s = s.substring(1); |
| | | } |
| | | // 保留模型输出中的换行,只在判断结束标记时忽略空白 |
| | | if ("[DONE]".equals(s.trim())) return; |
| | | try { |
| | | JSONObject obj = JSON.parseObject(s); |
| | | JSONArray choices = obj.getJSONArray("choices"); |
| | |
| | | JSONObject delta = c0.getJSONObject("delta"); |
| | | if (delta != null) { |
| | | String content = delta.getString("content"); |
| | | // log.info("chunk = [{}] len = {}", content, content.length()); |
| | | // for (char ch : content.toCharArray()) { |
| | | // log.info("char: {} ({})", (int) ch, ch == '\n' ? "\\n" : ch); |
| | | // } |
| | | if (content != null) onChunk.accept(content); |
| | | } |
| | | } |
| | |
| | | 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 诊断 |
| | |
| | | ); |
| | | messages.add(system); |
| | | |
| | | // 2. user:把具体的数据组织成文本(JSON 形式方便模型看结构) |
| | | ChatCompletionRequest.Message user = new ChatCompletionRequest.Message(); |
| | | user.setRole("user"); |
| | | user.setContent(buildUserContent(request)); |
| | | user.setContent(buildDiagnosisUserContent(request)); |
| | | messages.add(user); |
| | | |
| | | // 调用大模型 |
| | |
| | | |
| | | ChatCompletionRequest.Message user = new ChatCompletionRequest.Message(); |
| | | user.setRole("user"); |
| | | user.setContent(buildUserContent(request)); |
| | | user.setContent(buildDiagnosisUserContent(request)); |
| | | messages.add(user); |
| | | |
| | | llmChatService.chatStream(messages, 0.2, 2048, s -> { |
| | | try { emitter.send(SseEmitter.event().data(s)); } catch (Exception ignore) {} |
| | | try { |
| | | // SSE 协议不允许原样携带换行,先转为 \n 传输,前端再还原 |
| | | String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n"); |
| | | if (!safe.isEmpty()) { |
| | | emitter.send(SseEmitter.event().data(safe)); |
| | | } |
| | | } catch (Exception ignore) {} |
| | | }, () -> { |
| | | try { emitter.complete(); } catch (Exception ignore) {} |
| | | }, e -> { |
| | |
| | | }); |
| | | } |
| | | |
| | | private String buildUserContent(WcsDiagnosisRequest request) { |
| | | 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(buildAskUserContent(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 buildDiagnosisUserContent(WcsDiagnosisRequest request) { |
| | | StringBuilder sb = new StringBuilder(); |
| | | |
| | | sb.append("【问题描述】\n"); |
| | | if (request.getAlarmMessage() != null && !request.getAlarmMessage().isEmpty()) { |
| | | sb.append("【问题描述】\n"); |
| | | sb.append(request.getAlarmMessage()).append("\n\n"); |
| | | } else { |
| | | sb.append("系统当前不执行任务,但具体原因不明,请根据以下信息帮助判断。\n\n"); |
| | | } |
| | | |
| | | sb.append("【设备信息】\n"); |
| | | sb.append("关注设备(如果有指定): ") |
| | | .append(request.getCraneNo() != null ? request.getCraneNo() : "未指定,需整体分析") |
| | | .append("\n\n"); |
| | | |
| | | Object pseudo = redisUtil.get(com.zy.core.enums.RedisKeyType.MAIN_PROCESS_PSEUDOCODE.key); |
| | | if (pseudo != null) { |
| | | sb.append("【主流程伪代码 mainProcessPseudo】\n"); |
| | | sb.append(String.valueOf(pseudo)).append("\n\n"); |
| | | } |
| | | |
| | | if (request.getExtraContext() != null && !request.getExtraContext().isEmpty()) { |
| | | sb.append("【额外上下文 extraContext】\n"); |
| | |
| | | |
| | | return sb.toString(); |
| | | } |
| | | |
| | | private String buildAskUserContent(WcsDiagnosisRequest request) { |
| | | StringBuilder sb = new StringBuilder(); |
| | | |
| | | if (request.getExtraContext() != null && !request.getExtraContext().isEmpty()) { |
| | | sb.append("【额外上下文 extraContext】\n"); |
| | | sb.append(JSON.toJSONString(request.getExtraContext(), true)).append("\n\n"); |
| | | } |
| | | |
| | | if (request.getTasks() != null && !request.getTasks().isEmpty()) { |
| | | sb.append("【任务信息 tasks】\n"); |
| | | sb.append("下面是当前相关任务列表的 JSON 数据:\n"); |
| | | sb.append(JSON.toJSONString(request.getTasks(), true)).append("\n\n"); |
| | | } |
| | | |
| | | if (request.getDeviceRealtimeData() != null && !request.getDeviceRealtimeData().isEmpty()) { |
| | | sb.append("【设备实时数据 deviceRealtimeData】\n"); |
| | | sb.append("下面是各设备当前实时状态的 JSON 数据:\n"); |
| | | sb.append(JSON.toJSONString(request.getDeviceRealtimeData(), true)).append("\n\n"); |
| | | } |
| | | |
| | | if (request.getDeviceConfigs() != null && !request.getDeviceConfigs().isEmpty()) { |
| | | sb.append("【设备配置信息 deviceConfigs】\n"); |
| | | sb.append("下面是各设备配置的 JSON 数据:\n"); |
| | | sb.append(JSON.toJSONString(request.getDeviceConfigs(), true)).append("\n\n"); |
| | | } |
| | | |
| | | sb.append("【系统日志 logs(按时间顺序)】\n"); |
| | | if (request.getLogs() != null && !request.getLogs().isEmpty()) { |
| | | for (String logLine : request.getLogs()) { |
| | | sb.append(logLine).append("\n"); |
| | | } |
| | | } |
| | | |
| | | return sb.toString(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.ai.utils; |
| | | |
| | | import com.baomidou.mybatisplus.mapper.EntityWrapper; |
| | | import com.zy.ai.entity.DeviceConfigsData; |
| | | import com.zy.ai.entity.DeviceRealTimeData; |
| | | import com.zy.ai.entity.WcsDiagnosisRequest; |
| | | import com.zy.ai.log.AiLogAppender; |
| | | import com.zy.asrs.entity.BasCrnp; |
| | | import com.zy.asrs.entity.BasDevp; |
| | | import com.zy.asrs.entity.WrkMast; |
| | | import com.zy.asrs.service.BasCrnpService; |
| | | import com.zy.asrs.service.BasDevpService; |
| | | import com.zy.asrs.service.WrkMastService; |
| | | import com.zy.core.cache.SlaveConnection; |
| | | import com.zy.core.enums.SlaveType; |
| | | import com.zy.core.model.StationObjModel; |
| | | import com.zy.core.model.protocol.CrnProtocol; |
| | | import com.zy.core.model.protocol.StationProtocol; |
| | | import com.zy.core.thread.CrnThread; |
| | | import com.zy.core.thread.StationThread; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Component |
| | | public class AiUtils { |
| | | |
| | | @Autowired |
| | | private WrkMastService wrkMastService; |
| | | @Autowired |
| | | private BasCrnpService basCrnpService; |
| | | @Autowired |
| | | private BasDevpService basDevpService; |
| | | |
| | | public WcsDiagnosisRequest makeAiRequest(int logLimit, String alarmMessage) { |
| | | WcsDiagnosisRequest request = new WcsDiagnosisRequest(); |
| | | |
| | | request.setAlarmMessage(alarmMessage); |
| | | |
| | | List<String> logs = AiLogAppender.getRecentLogs(logLimit); |
| | | request.setLogs(logs); |
| | | |
| | | List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<>()); |
| | | request.setTasks(wrkMasts); |
| | | |
| | | List<DeviceRealTimeData> deviceRealTimeDataList = new ArrayList<>(); |
| | | List<DeviceConfigsData> deviceConfigsDataList = new ArrayList<>(); |
| | | |
| | | List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<>()); |
| | | for (BasCrnp basCrnp : basCrnps) { |
| | | CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo()); |
| | | if (crnThread == null) { |
| | | continue; |
| | | } |
| | | |
| | | CrnProtocol protocol = crnThread.getStatus(); |
| | | |
| | | for (StationObjModel stationObjModel : basCrnp.getInStationList$()) { |
| | | StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo()); |
| | | if (stationThread == null) { |
| | | continue; |
| | | } |
| | | |
| | | Map<Integer, StationProtocol> map = stationThread.getStatusMap(); |
| | | StationProtocol stationProtocol = map.get(stationObjModel.getStationId()); |
| | | if (stationProtocol == null) { |
| | | continue; |
| | | } |
| | | |
| | | DeviceRealTimeData stationData = new DeviceRealTimeData(); |
| | | stationData.setDeviceNo(stationObjModel.getDeviceNo()); |
| | | stationData.setDeviceType(String.valueOf(SlaveType.Devp)); |
| | | stationData.setDeviceData(stationProtocol); |
| | | deviceRealTimeDataList.add(stationData); |
| | | } |
| | | |
| | | DeviceRealTimeData deviceRealTimeData = new DeviceRealTimeData(); |
| | | deviceRealTimeData.setDeviceNo(basCrnp.getCrnNo()); |
| | | deviceRealTimeData.setDeviceType(String.valueOf(SlaveType.Crn)); |
| | | deviceRealTimeData.setDeviceData(protocol); |
| | | deviceRealTimeDataList.add(deviceRealTimeData); |
| | | |
| | | DeviceConfigsData deviceConfigsData = new DeviceConfigsData(); |
| | | deviceConfigsData.setDeviceNo(basCrnp.getCrnNo()); |
| | | deviceConfigsData.setDeviceType(String.valueOf(SlaveType.Crn)); |
| | | deviceConfigsData.setDeviceData(basCrnp); |
| | | deviceConfigsDataList.add(deviceConfigsData); |
| | | } |
| | | |
| | | List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<>()); |
| | | for (BasDevp basDevp : basDevps) { |
| | | StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo()); |
| | | if (stationThread == null) { |
| | | continue; |
| | | } |
| | | Map<Integer, StationProtocol> map = stationThread.getStatusMap(); |
| | | |
| | | for (StationObjModel stationObjModel : basDevp.getInStationList$()) { |
| | | StationProtocol stationProtocol = map.get(stationObjModel.getStationId()); |
| | | if (stationProtocol == null) { |
| | | continue; |
| | | } |
| | | |
| | | DeviceRealTimeData stationData = new DeviceRealTimeData(); |
| | | stationData.setDeviceNo(stationObjModel.getDeviceNo()); |
| | | stationData.setDeviceType(String.valueOf(SlaveType.Devp)); |
| | | stationData.setDeviceData(stationProtocol); |
| | | deviceRealTimeDataList.add(stationData); |
| | | } |
| | | |
| | | for (StationObjModel stationObjModel : basDevp.getOutStationList$()) { |
| | | StationProtocol stationProtocol = map.get(stationObjModel.getStationId()); |
| | | if (stationProtocol == null) { |
| | | continue; |
| | | } |
| | | |
| | | DeviceRealTimeData stationData = new DeviceRealTimeData(); |
| | | stationData.setDeviceNo(stationObjModel.getDeviceNo()); |
| | | stationData.setDeviceType(String.valueOf(SlaveType.Devp)); |
| | | stationData.setDeviceData(stationProtocol); |
| | | deviceRealTimeDataList.add(stationData); |
| | | } |
| | | |
| | | DeviceConfigsData deviceConfigsData = new DeviceConfigsData(); |
| | | deviceConfigsData.setDeviceNo(basDevp.getDevpNo()); |
| | | deviceConfigsData.setDeviceType(String.valueOf(SlaveType.Devp)); |
| | | deviceConfigsData.setDeviceData(basDevp); |
| | | deviceConfigsDataList.add(deviceConfigsData); |
| | | } |
| | | |
| | | request.setDeviceRealtimeData(deviceRealTimeDataList); |
| | | request.setDeviceConfigs(deviceConfigsDataList); |
| | | return request; |
| | | } |
| | | |
| | | } |
| | |
| | | import com.zy.asrs.service.BasCrnpService; |
| | | import com.zy.asrs.service.WrkMastService; |
| | | import com.zy.common.utils.RedisUtil; |
| | | import com.zy.core.News; |
| | | import com.zy.core.cache.SlaveConnection; |
| | | import com.zy.core.enums.CrnModeType; |
| | | import com.zy.core.enums.RedisKeyType; |
| | | import com.zy.core.enums.SlaveType; |
| | | import com.zy.core.enums.WrkIoType; |
| | | import com.zy.core.model.protocol.CrnProtocol; |
| | | import com.zy.core.thread.CrnThread; |
| | | |
| | |
| | | if (crnProtocol.getMode() != CrnModeType.AUTO.id) { |
| | | continue; |
| | | } |
| | | |
| | | List<WrkMast> inWrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>() |
| | | .eq("crn_no", basCrnp.getCrnNo()) |
| | | .eq("io_type", WrkIoType.IN.id) |
| | | ); |
| | | // 检查是否超过最大入库任务数 |
| | | if(inWrkMasts.size() >= basCrnp.getMaxInTask()){ |
| | | News.info("堆垛机:{} 已达最大入库任务数,当前任务数:{}", basCrnp.getCrnNo(), inWrkMasts.size()); |
| | | continue; |
| | | } |
| | | enabledCrnps.add(basCrnp); |
| | | } |
| | | |
| | |
| | | throw new CoolException("未找到输送目标站点可走行路径"); |
| | | } |
| | | |
| | | BasCrnp basCrnp = basCrnpService.selectOne(new EntityWrapper<BasCrnp>().eq("crn_no", crnNo)); |
| | | if(basCrnp == null) { |
| | | throw new CoolException("未找到对应堆垛机数据"); |
| | | } |
| | | List<WrkMast> outWrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>() |
| | | .eq("crn_no", crnNo) |
| | | .eq("io_type", WrkIoType.OUT.id) |
| | | ); |
| | | // 检查是否超过最大出库任务数 |
| | | if(outWrkMasts.size() >= basCrnp.getMaxOutTask()){ |
| | | News.info("堆垛机:{} 已达最大出库任务数,当前任务数:{}", basCrnp.getCrnNo(), outWrkMasts.size()); |
| | | throw new CoolException("堆垛机:" + basCrnp.getCrnNo() + "已达最大出库任务数,当前任务数:" + outWrkMasts.size()); |
| | | } |
| | | |
| | | // 获取工作号 |
| | | int workNo = getWorkNo(WrkIoType.OUT.id); |
| | | // 保存工作档 |
| | |
| | | locMast.setLocSts("R"); |
| | | locMast.setModiTime(new Date()); |
| | | locMastService.updateById(locMast); |
| | | |
| | | return true; |
| | | } |
| | | |
| | |
| | | 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_"), |
| | | MAIN_PROCESS_PSEUDOCODE("main_process_pseudocode"), |
| | | ; |
| | | |
| | | public String key; |
| | |
| | | News.error("请求WMS接口失败!!!url:{};request:{};response:{}", wmsUrl + wmsSystemInUrl, JSON.toJSONString(requestParam), response); |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | News.error("请求WMS接口异常!!!url:{};request:{};response:{}", wmsUrl + wmsSystemInUrl, JSON.toJSONString(requestParam), response, e); |
| | | } finally { |
| | | HttpRequestLog httpRequestLog = new HttpRequestLog(); |
| | | httpRequestLog.setName(wmsUrl + wmsSystemInUrl); |
| New file |
| | |
| | | package com.zy.core.task; |
| | | |
| | | import com.zy.ai.entity.ChatCompletionRequest; |
| | | import com.zy.ai.service.LlmChatService; |
| | | import com.zy.common.utils.RedisUtil; |
| | | import com.zy.core.News; |
| | | import com.zy.core.enums.RedisKeyType; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Paths; |
| | | import java.util.List; |
| | | |
| | | @Component |
| | | public class MakeMainProcessPseudocodeScheduler { |
| | | |
| | | @Value("${mainProcessPlugin}") |
| | | private String mainProcessPlugin; |
| | | @Autowired |
| | | private LlmChatService llmChatService; |
| | | @Autowired |
| | | private RedisUtil redisUtil; |
| | | |
| | | @Scheduled(cron = "1 * * * * ? ") |
| | | public void refreshPseudocodeDaily() { |
| | | try { |
| | | initMainProcessPseudocode(); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | |
| | | private void initMainProcessPseudocode(){ |
| | | Object object = redisUtil.get(RedisKeyType.MAIN_PROCESS_PSEUDOCODE.key); |
| | | if (object != null) { |
| | | return; |
| | | } |
| | | |
| | | String plugin = mainProcessPlugin; |
| | | if (plugin == null) plugin = "NormalProcess"; |
| | | String className = plugin.contains(".") ? plugin : "com.zy.core.plugin." + plugin; |
| | | String code = null; |
| | | try { |
| | | String rel = className.replace('.', '/') + ".java"; |
| | | java.nio.file.Path p = Paths.get(System.getProperty("user.dir"), "src", "main", "java", rel); |
| | | if (Files.exists(p)) { |
| | | code = new String(Files.readAllBytes(p), StandardCharsets.UTF_8); |
| | | } |
| | | } catch (Exception ignore) {} |
| | | String result = null; |
| | | if (code != null && !code.isEmpty()) { |
| | | List<ChatCompletionRequest.Message> messages = new java.util.ArrayList<>(); |
| | | ChatCompletionRequest.Message system = new ChatCompletionRequest.Message(); |
| | | system.setRole("system"); |
| | | system.setContent("你现在是一名高级 Java 架构师兼伪代码转换专家,专门负责把复杂的 Java 代码转换成结构清晰、适合大模型阅读与推理的伪代码。\n" + |
| | | "\n" + |
| | | "请严格遵守以下要求工作:\n" + |
| | | "\n" + |
| | | "核心目标\n" + |
| | | "\n" + |
| | | "输入是一段或多段 Java 代码。\n" + |
| | | "\n" + |
| | | "输出是一段人类可读、逻辑清晰、尽量语言中立的伪代码。\n" + |
| | | "\n" + |
| | | "这份伪代码将被用作后续大模型提问的“参考描述”,所以要:\n" + |
| | | "\n" + |
| | | "保留关键业务逻辑和判断条件;\n" + |
| | | "\n" + |
| | | "弱化语言细节(如具体库、注解、框架细节);\n" + |
| | | "\n" + |
| | | "用自然语言 + 简洁流程结构,帮助大模型快速理解代码意图。\n" + |
| | | "\n" + |
| | | "风格要求\n" + |
| | | "\n" + |
| | | "使用中文描述逻辑,但可以保留少量关键英文标识(例如类名、方法名、状态枚举)以便跟代码对应。\n" + |
| | | "\n" + |
| | | "伪代码要分层分块,尽量按:\n" + |
| | | "\n" + |
| | | "类职责说明\n" + |
| | | "\n" + |
| | | "重要字段 / 全局变量说明\n" + |
| | | "\n" + |
| | | "每个公开方法 / 核心私有方法的伪代码\n" + |
| | | "\n" + |
| | | "逻辑上使用类似:\n" + |
| | | "\n" + |
| | | "如果 ... 则 ...\n" + |
| | | "\n" + |
| | | "否则如果 ...\n" + |
| | | "\n" + |
| | | "循环遍历列表 ...\n" + |
| | | "\n" + |
| | | "调用服务/方法: ...\n" + |
| | | "\n" + |
| | | "返回 ...\n" + |
| | | "\n" + |
| | | "不追求严格语法,只追求易懂和准确。\n" + |
| | | "\n" + |
| | | "保留信息 & 抽象信息\n" + |
| | | "\n" + |
| | | "必须保留:\n" + |
| | | "\n" + |
| | | "关键业务含义(例如“生成入库任务”、“检查堆垛机任务是否完成”)\n" + |
| | | "\n" + |
| | | "关键条件判断(状态字段、枚举、重要配置开关)\n" + |
| | | "\n" + |
| | | "重要数据流向(从哪里读数据、写到哪里、调用了哪些服务)\n" + |
| | | "\n" + |
| | | "与外部系统交互(如 HTTP 调用 WMS、写 Redis 锁、写数据库)\n" + |
| | | "\n" + |
| | | "可以抽象或省略:\n" + |
| | | "\n" + |
| | | "日志打印的具体格式,只保留“记录日志:xxx”即可;\n" + |
| | | "\n" + |
| | | "具体框架注解(如 @Component, @Autowired 等);\n" + |
| | | "\n" + |
| | | "泛型、异常栈细节、工具类内部实现;\n" + |
| | | "\n" + |
| | | "结构模板(优先遵循)\n" + |
| | | "\n" + |
| | | "对于一段较大的 Java 类,请按以下结构输出伪代码:\n" + |
| | | "\n" + |
| | | "类整体说明\n" + |
| | | "\n" + |
| | | "简要说明这个类的用途和在系统中的角色。\n" + |
| | | "\n" + |
| | | "重要字段 / 配置说明\n" + |
| | | "\n" + |
| | | "列出关键静态变量 / 配置项 / 状态缓存,并用一行解释它们的含义。\n" + |
| | | "\n" + |
| | | "主流程方法(例如 run())\n" + |
| | | "\n" + |
| | | "用有序列表或伪代码,按调用顺序描述主要步骤。\n" + |
| | | "\n" + |
| | | "每个核心私有方法\n" + |
| | | "\n" + |
| | | "对于每个关键方法:\n" + |
| | | "\n" + |
| | | "先用一行中文总结功能;\n" + |
| | | "\n" + |
| | | "再给出伪代码流程(条件、循环、关键调用);\n" + |
| | | "\n" + |
| | | "与外部系统交互的说明\n" + |
| | | "\n" + |
| | | "单独强调有哪些地方调用了外部服务(HTTP、消息队列、数据库、Redis 等)。\n" + |
| | | "\n" + |
| | | "输出格式要求\n" + |
| | | "\n" + |
| | | "使用 Markdown 结构,方便复制给其他大模型:\n" + |
| | | "\n" + |
| | | "用 ## 标题区分“类说明”、“主流程伪代码”、“方法伪代码”等部分;\n" + |
| | | "\n" + |
| | | "伪代码块可以使用缩进和项目符号,或用 pseudo 代码块 包裹;\n" + |
| | | "\n" + |
| | | "不要直接逐行翻译代码,而是做抽象和整理;\n" + |
| | | "\n" + |
| | | "不要输出无关文本,例如道歉、寒暄或与任务无关的解释。\n" + |
| | | "\n" + |
| | | "伪代码示例风格(示意)\n" + |
| | | "\n" + |
| | | "例如当输入一个 run() 方法时,期望你的输出风格类似:\n" + |
| | | "\n" + |
| | | "函数 run():\n" + |
| | | " 读取配置 enableFake, fakeRealTaskRequestWms\n" + |
| | | " 如果 enableFake == \"Y\":\n" + |
| | | " 调用 checkInStationHasTask() 检测入库站并生成仿真站点数据\n" + |
| | | " 如果 fakeRealTaskRequestWms == \"N\":\n" + |
| | | " 调用 generateFakeInTask() 生成本地仿真入库任务\n" + |
| | | " 调用 generateFakeOutTask() 生成本地仿真出库任务\n" + |
| | | " 计算所有站点的停留时间 calcAllStationStayTime()\n" + |
| | | " 检查出库站点是否超时并重置 checkOutStationStayTimeOut()\n" + |
| | | " 检查入库站点货物是否已被堆垛机取走 checkInStationCrnTake()\n" + |
| | | " 如果 fakeRealTaskRequestWms == \"Y\":\n" + |
| | | " 调用 generateStoreWrkFile() 请求 WMS 生成真实任务\n" + |
| | | " 调用 crnOperateUtils.crnIoExecute() 执行堆垛机任务\n" + |
| | | " 调用 crnIoExecuteFinish() 处理堆垛机任务完成后的状态更新和仿真站点生成\n" + |
| | | " 调用 stationOperateProcessUtils.stationInExecute() 执行输送站入库任务\n" + |
| | | " 调用 stationOperateProcessUtils.stationOutExecute() 执行输送站出库任务\n" + |
| | | " 调用 stationOperateProcessUtils.stationOutExecuteFinish() 检查输送站出库任务完成\n" + |
| | | "\n" + |
| | | "\n" + |
| | | "对输入的要求\n" + |
| | | "\n" + |
| | | "如果用户给出的是多段代码或只给出片段:\n" + |
| | | "\n" + |
| | | "先推断这段代码的职责;\n" + |
| | | "\n" + |
| | | "再按你能理解到的范围进行伪代码转换;\n" + |
| | | "\n" + |
| | | "如果存在明显缺失的类/方法,只需在伪代码中用“调用 XXX(具体逻辑略)”标记即可。\n" + |
| | | "\n" + |
| | | "请始终以「让后续大模型能看懂这段代码逻辑并基于伪代码进行推理和提问」为最高优先级来组织你的输出。"); |
| | | messages.add(system); |
| | | ChatCompletionRequest.Message user = new ChatCompletionRequest.Message(); |
| | | user.setRole("user"); |
| | | user.setContent("主流程插件类源代码:\n\n" + code); |
| | | messages.add(user); |
| | | try { |
| | | result = llmChatService.chat(messages, 0.2, 2048); |
| | | } catch (Exception ignore) {} |
| | | } |
| | | redisUtil.set(RedisKeyType.MAIN_PROCESS_PSEUDOCODE.key, result, 60 * 60 * 24); |
| | | News.info("主流程伪代码已刷新"); |
| | | } |
| | | |
| | | } |
| | |
| | | return; |
| | | } |
| | | |
| | | List<WrkMast> inWrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>() |
| | | .eq("crn_no", basCrnp.getCrnNo()) |
| | | .eq("io_type", WrkIoType.IN.id) |
| | | ); |
| | | // 检查是否超过最大入库任务数 |
| | | if(inWrkMasts.size() >= basCrnp.getMaxInTask()){ |
| | | News.info("堆垛机:{} 已达最大入库任务数,当前任务数:{}", basCrnp.getCrnNo(), inWrkMasts.size()); |
| | | return; |
| | | } |
| | | |
| | | Integer crnNo = basCrnp.getCrnNo(); |
| | | |
| | | for (StationObjModel stationObjModel : inStationList) { |
| | |
| | | List<StationObjModel> outStationList = basCrnp.getOutStationList$(); |
| | | if(outStationList.isEmpty()){ |
| | | News.info("堆垛机:{} 出库站点未设置", basCrnp.getCrnNo()); |
| | | return; |
| | | } |
| | | |
| | | List<WrkMast> outWrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>() |
| | | .eq("crn_no", basCrnp.getCrnNo()) |
| | | .eq("io_type", WrkIoType.OUT.id) |
| | | ); |
| | | // 检查是否超过最大出库任务数 |
| | | if(outWrkMasts.size() >= basCrnp.getMaxOutTask()){ |
| | | News.info("堆垛机:{} 已达最大出库任务数,当前任务数:{}", basCrnp.getCrnNo(), outWrkMasts.size()); |
| | | return; |
| | | } |
| | | |
| | |
| | | expireDays: 7 |
| | | |
| | | llm: |
| | | base-url: https://api.siliconflow.cn/v1 |
| | | api-key: sk-sxdtebtquwrugzrmaqqqkzdzmrgzhzmplwwuowysdasccent |
| | | model: Qwen/Qwen3-VL-32B-Instruct |
| | | # base-url: https://api.siliconflow.cn/v1 |
| | | # api-key: sk-sxdtebtquwrugzrmaqqqkzdzmrgzhzmplwwuowysdasccent |
| | | # model: deepseek-ai/DeepSeek-V3.2 |
| | | base-url: http://47.76.147.249:9998/e/7g7kqxxt1ei2un71 |
| | | api-key: app-mP0O6aY5WpbfaHs7BNnjVkli |
| | | model: deepseek-ai/DeepSeek-V3.2 |
| | |
| | | if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); |
| | | return fmt; |
| | | } |
| | | |
| | | /** |
| | | * 获取AI助手SVG图标HTML |
| | | * @param {number} width 宽度,默认48 |
| | | * @param {number} height 高度,默认48 |
| | | * @returns {string} SVG HTML字符串 |
| | | */ |
| | | function getAiIconHtml(width, height) { |
| | | width = width || 48; |
| | | height = height || 48; |
| | | // 生成唯一ID防止冲突 |
| | | var uniqueId = 'ai_icon_' + Math.random().toString(36).substr(2, 9); |
| | | var textGradientId = 'textGradient_' + uniqueId; |
| | | var glowId = 'glow_' + uniqueId; |
| | | var spinName = 'spin_' + uniqueId; |
| | | |
| | | return '<svg width="' + width + '" height="' + height + '" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" fill="none">' + |
| | | '<defs>' + |
| | | '<linearGradient id="' + textGradientId + '" gradientUnits="userSpaceOnUse" x1="25" y1="50" x2="75" y2="50">' + |
| | | '<stop offset="0%" stop-color="#8b5cf6"/>' + |
| | | '<stop offset="20%" stop-color="#f472b6"/>' + |
| | | '<stop offset="40%" stop-color="#fb923c"/>' + |
| | | '<stop offset="60%" stop-color="#fbbf24"/>' + |
| | | '<stop offset="80%" stop-color="#22d3ee"/>' + |
| | | '<stop offset="100%" stop-color="#3b82f6"/>' + |
| | | '<animateTransform attributeName="gradientTransform" type="rotate" from="0 50 50" to="360 50 50" dur="5s" repeatCount="indefinite" />' + |
| | | '</linearGradient>' + |
| | | '<filter id="' + glowId + '" x="-50%" y="-50%" width="200%" height="200%">' + |
| | | '<feGaussianBlur stdDeviation="1.6" result="blur"/>' + |
| | | '<feMerge>' + |
| | | '<feMergeNode in="blur"/>' + |
| | | '<feMergeNode in="SourceGraphic"/>' + |
| | | '</feMerge>' + |
| | | '</filter>' + |
| | | '<style>' + |
| | | '.' + spinName + ' { animation: ' + spinName + ' 5s linear infinite; transform-origin: 50px 50px; }' + |
| | | '@keyframes ' + spinName + ' { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }' + |
| | | '.geo-text-' + uniqueId + ' { fill: url(#' + textGradientId + '); stroke: rgba(0,0,0,0.40); stroke-width: 0.45; paint-order: stroke fill; }' + |
| | | '</style>' + |
| | | '</defs>' + |
| | | '<g class="' + spinName + '" filter="url(#' + glowId + ')">' + |
| | | '<g transform="rotate(0 50 50)"><rect x="48" y="18" width="4.5" height="10" rx="2" fill="#8b5cf6"/></g>' + |
| | | '<g transform="rotate(60 50 50)"><rect x="48" y="18" width="4.5" height="10" rx="2" fill="#f472b6"/></g>' + |
| | | '<g transform="rotate(120 50 50)"><rect x="48" y="18" width="4.5" height="10" rx="2" fill="#fb923c"/></g>' + |
| | | '<g transform="rotate(180 50 50)"><rect x="48" y="18" width="4.5" height="10" rx="2" fill="#fbbf24"/></g>' + |
| | | '<g transform="rotate(240 50 50)"><rect x="48" y="18" width="4.5" height="10" rx="2" fill="#22d3ee"/></g>' + |
| | | '<g transform="rotate(300 50 50)"><rect x="48" y="18" width="4.5" height="10" rx="2" fill="#3b82f6"/></g>' + |
| | | '</g>' + |
| | | '<g transform="translate(50 50) scale(0.35) translate(-32.5 -20)" class="geo-text-' + uniqueId + '">' + |
| | | '<path d="M0 40 L20 0 L40 40 Z"/><rect x="12" y="22" width="16" height="4" rx="1"/>' + |
| | | '<rect x="50" y="0" width="15" height="4" rx="1"/><rect x="55" y="4" width="5" height="32" rx="1.5"/><rect x="50" y="36" width="15" height="4" rx="1"/>' + |
| | | '</g>' + |
| | | '</svg>'; |
| | | } |
| New file |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="UTF-8" /> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | | <title>WCS AI 助手</title> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css" /> |
| | | <style> |
| | | body { background: #f5f7fa; } |
| | | .container { max-width: 1100px; margin: 24px auto; } |
| | | .actions { display: flex; gap: 12px; align-items: center; } |
| | | .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; } |
| | | .markdown-body h1, .markdown-body h2, .markdown-body h3 { margin-top: 8px; } |
| | | .markdown-body pre { background: #f6f8fa; padding: 12px; border-radius: 6px; overflow: auto; } |
| | | .status { color: #909399; } |
| | | .chat { display: flex; flex-direction: column; gap: 10px; height: 100%; overflow-y: auto; padding-right: 8px; } |
| | | .msg { display: flex; align-items: flex-start; } |
| | | .msg.user { justify-content: flex-end; } |
| | | .msg.assistant { justify-content: flex-start; } |
| | | .bubble { max-width: 72%; padding: 10px 12px; border-radius: 16px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; } |
| | | .assistant .bubble { background: #ffffff; border: 1px solid #ebeef5; color: #303133; } |
| | | .user .bubble { background: #409EFF; color: #ffffff; } |
| | | .composer { display: flex; gap: 10px; align-items: center; margin-top: 12px; } |
| | | .avatar { width: 24px; height: 24px; display: flex; align-items: center; margin-right: 8px; } |
| | | .time { font-size: 12px; color: #909399; text-align: right; margin-top: 6px; } |
| | | .output .el-card__body { height: 100%; padding: 0; } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div id="app" class="container"> |
| | | <el-card shadow="hover"> |
| | | <div slot="header" class="clearfix" style="display: flex; align-items: center;"> |
| | | <div v-html="headerIcon" style="margin-right: 10px; display: flex;"></div> |
| | | <span>WCS AI 助手</span> |
| | | </div> |
| | | |
| | | <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" 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> |
| | | |
| | | <el-card class="output" shadow="never"> |
| | | <div ref="chat" class="chat"> |
| | | <div v-for="(m,i) in messages" :key="i" class="msg" :class="m.role"> |
| | | <div class="avatar" v-html="m.role === 'assistant' ? assistantIcon : userIcon"></div> |
| | | <div class="bubble"> |
| | | <div v-if="m.role === 'assistant'" class="markdown-body" v-html="m.html"></div> |
| | | <div v-else v-text="m.text"></div> |
| | | <div class="time">{{ m.ts }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <div class="composer"> |
| | | <el-input v-model="userInput" placeholder="向 AI 助手提问" clearable :disabled="streaming" @keyup.enter.native="ask"></el-input> |
| | | <el-button type="success" :disabled="sendDisabled" @click="ask">发送</el-button> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
| | | <script src="https://cdn.jsdelivr.net/npm/dompurify@2.4.3/dist/purify.min.js"></script> |
| | | <script> |
| | | marked.setOptions({ |
| | | gfm: true, |
| | | breaks: true |
| | | }); |
| | | |
| | | function getUserIconHtml(width, height) { |
| | | width = width || 24; height = height || 24; |
| | | return '<svg width="'+width+'" height="'+height+'" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">\n' |
| | | + '<circle cx="12" cy="7" r="4" fill="#909399"/>\n' |
| | | + '<path d="M4 20c0-4 4-6 8-6s8 2 8 6" fill="#909399" opacity="0.35"/>\n' |
| | | + '</svg>'; |
| | | } |
| | | |
| | | new Vue({ |
| | | el: '#app', |
| | | data: function() { |
| | | return { |
| | | headerIcon: getAiIconHtml(50, 50), |
| | | assistantIcon: getAiIconHtml(24, 24), |
| | | userIcon: getUserIconHtml(24, 24), |
| | | loading: false, |
| | | streaming: false, |
| | | source: null, |
| | | messages: [], |
| | | pendingText: '', |
| | | typingTimer: null, |
| | | lastRenderTs: 0, |
| | | renderIntervalMs: 120, |
| | | stepChars: 6, |
| | | userInput: '', |
| | | autoScrollThreshold: 80, |
| | | chats: [], |
| | | currentChatId: '', |
| | | resetting: false |
| | | }; |
| | | }, |
| | | computed: { |
| | | statusText: function() { |
| | | if (this.streaming) return '诊断进行中'; |
| | | if (this.loading) return '连接中'; |
| | | return '空闲'; |
| | | }, |
| | | sendDisabled: function() { |
| | | var t = (this.userInput || '').trim(); |
| | | return this.streaming || t.length === 0; |
| | | } |
| | | }, |
| | | 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; |
| | | return (el.scrollHeight - el.scrollTop - el.clientHeight) <= this.autoScrollThreshold; |
| | | }, |
| | | scrollToBottom: function(force) { |
| | | var el = this.$refs.chat; |
| | | if (!el) return; |
| | | if (force || this.streaming || this.shouldAutoScroll()) { |
| | | el.scrollTop = el.scrollHeight; |
| | | } |
| | | }, |
| | | nowStr: function() { |
| | | var d = new Date(); |
| | | function pad(n) { return (n<10?'0':'') + n; } |
| | | var y = d.getFullYear(); |
| | | var m = pad(d.getMonth() + 1); |
| | | var day = pad(d.getDate()); |
| | | var hh = pad(d.getHours()); |
| | | var mm = pad(d.getMinutes()); |
| | | var ss = pad(d.getSeconds()); |
| | | return y + '-' + m + '-' + day + ' ' + hh + ':' + mm + ':' + ss; |
| | | }, |
| | | ask: function() { |
| | | if (this.streaming) return; |
| | | var msg = (this.userInput || '').trim(); |
| | | if (!msg) return; |
| | | this.loading = true; |
| | | this.streaming = true; |
| | | this.messages.push({ role: 'user', text: msg, ts: this.nowStr() }); |
| | | 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() { |
| | | self.loading = false; |
| | | }; |
| | | this.source.onmessage = function(e) { |
| | | if (!e || !e.data) return; |
| | | var chunk = (e.data || '').replace(/\\n/g, '\n'); |
| | | if (!chunk) return; |
| | | var normalized = chunk.replace(/\r/g, ''); |
| | | if (/^\n+$/.test(normalized)) return; |
| | | self.pendingText += chunk; |
| | | self.ensureTyping(); |
| | | self.scrollToBottom(true); |
| | | }; |
| | | this.source.onerror = function() { |
| | | self.stop(); |
| | | }; |
| | | this.userInput = ''; |
| | | this.resetting = false; |
| | | }, |
| | | start: function() { |
| | | if (this.streaming) return; |
| | | this.clear(); |
| | | this.loading = true; |
| | | this.streaming = true; |
| | | var url = baseUrl + '/ai/diagnose/runAiStream'; |
| | | this.source = new EventSource(url); |
| | | this.messages.push({ role: 'assistant', md: '', html: '', ts: this.nowStr() }); |
| | | this.scrollToBottom(true); |
| | | var self = this; |
| | | this.source.onopen = function() { |
| | | self.loading = false; |
| | | }; |
| | | this.source.onmessage = function(e) { |
| | | if (!e || !e.data) return; |
| | | // 后端把换行转义成 \n,这里还原为真正的换行 |
| | | var chunk = (e.data || '').replace(/\\n/g, '\n'); |
| | | if (!chunk) return; |
| | | // 如果仅包含换行符(如单独 \n 或 \n\n),丢弃避免空白行 |
| | | var normalized = chunk.replace(/\r/g, ''); |
| | | if (/^\n+$/.test(normalized)) return; |
| | | self.pendingText += chunk; |
| | | self.ensureTyping(); |
| | | self.scrollToBottom(true); |
| | | }; |
| | | this.source.onerror = function() { |
| | | self.stop(); |
| | | }; |
| | | }, |
| | | ensureTyping: function() { |
| | | if (this.typingTimer) return; |
| | | var self = this; |
| | | this.typingTimer = setInterval(function() { |
| | | if (!self.streaming && self.pendingText.length === 0) { |
| | | clearInterval(self.typingTimer); |
| | | self.typingTimer = null; |
| | | return; |
| | | } |
| | | if (self.pendingText.length > 0) { |
| | | var n = Math.min(self.stepChars, self.pendingText.length); |
| | | var part = self.pendingText.slice(0, n); |
| | | self.pendingText = self.pendingText.slice(n); |
| | | var last = self.messages.length > 0 ? self.messages[self.messages.length - 1] : null; |
| | | if (last && last.role === 'assistant') { |
| | | last.md = (last.md || '') + part; |
| | | } |
| | | } |
| | | var now = Date.now(); |
| | | if (now - self.lastRenderTs > self.renderIntervalMs) { |
| | | self.lastRenderTs = now; |
| | | var last = self.messages.length > 0 ? self.messages[self.messages.length - 1] : null; |
| | | if (last && last.role === 'assistant') { |
| | | var renderSource = (last.md || '').replace(/\\n/g, '\n'); |
| | | last.html = DOMPurify.sanitize(marked.parse(renderSource)); |
| | | self.$nextTick(function() { self.scrollToBottom(true); }); |
| | | } |
| | | } |
| | | }, 50); |
| | | }, |
| | | stop: function() { |
| | | if (this.source) { |
| | | this.source.close(); |
| | | this.source = null; |
| | | } |
| | | this.streaming = false; |
| | | this.loading = false; |
| | | if (this.typingTimer) { |
| | | clearInterval(this.typingTimer); |
| | | this.typingTimer = null; |
| | | } |
| | | var last = this.messages.length > 0 ? this.messages[this.messages.length - 1] : null; |
| | | if (last && last.role === 'assistant') { |
| | | var renderSource = (last.md || '').replace(/\\n/g, '\n'); |
| | | 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> |
| | | </html> |
| | |
| | | box-shadow: 0px 0px 20px rgba(0,0,0,0.3); |
| | | text-align: center; |
| | | } |
| | | |
| | | /* AI助手抽屉动画 */ |
| | | @keyframes slideInRight { |
| | | from { transform: translate3d(100%, 0, 0); opacity: 0; } |
| | | to { transform: translate3d(0, 0, 0); opacity: 1; } |
| | | } |
| | | |
| | | @keyframes slideOutRight { |
| | | from { transform: translate3d(0, 0, 0); opacity: 1; } |
| | | to { transform: translate3d(100%, 0, 0); opacity: 0; } |
| | | } |
| | | |
| | | .ai-drawer-layer { |
| | | box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15) !important; |
| | | border-radius: 8px 0 0 8px !important; |
| | | overflow: hidden; |
| | | animation: slideInRight 0.5s cubic-bezier(0.16, 1, 0.3, 1); |
| | | } |
| | | |
| | | .ai-drawer-layer-close { |
| | | animation: slideOutRight 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards !important; |
| | | } |
| | | </style> |
| | | </head> |
| | | <body class="layui-layout-body"> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 右下角SVG动画 --> |
| | | <div id="ai-assistant-btn" style="position: fixed; bottom: 40px; right: 20px; z-index: 9999; cursor: pointer;"> |
| | | </div> |
| | | |
| | | <script type="text/javascript" src="../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../static/layui/layui.js"></script> |
| | | <script type="text/javascript" src="../static/js/handlebars/handlebars-v4.5.3.js"></script> |
| | |
| | | <script> |
| | | console.log('%c 中扬立库平台 %c 1.0.0','background-color:rgb(53,73,94);color: #fff;border-radius:2px 0 0 2px;padding:2px 4px;','background-color:rgb(25,190,107);color: #fff;border-radius:0 2px 2px 0;padding:2px 4px;font: 9pt "Apercu Regular", Georgia, "Times New Roman", Times, serif;'); |
| | | $(function () { |
| | | // 注入AI助手图标 |
| | | $('#ai-assistant-btn').html(getAiIconHtml(60, 60)); |
| | | |
| | | if ("" === localStorage.getItem('token')) { |
| | | top.location.href = baseUrl + "/login"; |
| | | } |
| | |
| | | var url = logout.getAttribute('href'); |
| | | logout.setAttribute('href', baseUrl + "/login"); |
| | | |
| | | // AI助手图标悬浮提示 |
| | | $('#ai-assistant-btn').on('mouseenter', function(){ |
| | | this.index = layer.tips('AI助手', this, { |
| | | tips: [1, '#333'], // 上方显示,深色背景 |
| | | time: -1 // 不自动关闭 |
| | | }); |
| | | }).on('mouseleave', function(){ |
| | | layer.close(this.index); |
| | | }).on('click', function () { |
| | | layer.open({ |
| | | type: 2, |
| | | title: false, // 隐藏默认标题栏,更简洁 |
| | | closeBtn: 0, // 隐藏关闭按钮,点击遮罩关闭 |
| | | shadeClose: false, // 改为手动控制关闭,以便播放动画 |
| | | shade: 0.1, |
| | | area: ['600px', '100%'], |
| | | offset: 'r', // 右侧悬浮 |
| | | anim: -1, // 禁用默认动画,使用CSS动画 |
| | | isOutAnim: false, |
| | | skin: 'ai-drawer-layer', // 自定义皮肤 |
| | | content: 'ai/diagnosis.html', |
| | | success: function(layero, index){ |
| | | // 背景模糊效果 |
| | | var shadeId = layero.attr('id').replace('layui-layer', 'layui-layer-shade'); |
| | | var $shade = $('#' + shadeId); |
| | | $shade.css({ |
| | | 'backdrop-filter': 'blur(3px)', |
| | | 'transition': 'opacity 0.8s' |
| | | }); |
| | | |
| | | // 点击遮罩关闭(带动画) |
| | | $shade.on('click', function() { |
| | | layero.addClass('ai-drawer-layer-close'); |
| | | $shade.css('opacity', 0); |
| | | setTimeout(function(){ |
| | | layer.close(index); |
| | | }, 400); |
| | | }); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | }); |
| | | </script> |
| | | <script type="text/html" id="menuTpl"> |