src/main/java/com/zy/ai/controller/LlmCallLogController.java
New file @@ -0,0 +1,65 @@ package com.zy.ai.controller; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.baomidou.mybatisplus.plugins.Page; import com.core.annotations.ManagerAuth; import com.core.common.R; import com.zy.ai.entity.LlmCallLog; import com.zy.ai.service.LlmCallLogService; import com.zy.common.web.BaseController; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/ai/llm/log") @RequiredArgsConstructor public class LlmCallLogController extends BaseController { private final LlmCallLogService llmCallLogService; @GetMapping("/list/auth") @ManagerAuth public R list(@RequestParam(defaultValue = "1") Integer curr, @RequestParam(defaultValue = "20") Integer limit, @RequestParam(required = false) String scene, @RequestParam(required = false) Integer success, @RequestParam(required = false) Long routeId, @RequestParam(required = false) String traceId) { EntityWrapper<LlmCallLog> wrapper = new EntityWrapper<>(); if (!isBlank(scene)) { wrapper.eq("scene", scene.trim()); } if (success != null) { wrapper.eq("success", success); } if (routeId != null) { wrapper.eq("route_id", routeId); } if (!isBlank(traceId)) { wrapper.eq("trace_id", traceId.trim()); } wrapper.orderBy("id", false); return R.ok(llmCallLogService.selectPage(new Page<>(curr, limit), wrapper)); } @PostMapping("/delete/auth") @ManagerAuth public R delete(@RequestParam("id") Long id) { if (id == null) { return R.error("idä¸è½ä¸ºç©º"); } llmCallLogService.deleteById(id); return R.ok(); } @PostMapping("/clear/auth") @ManagerAuth public R clear() { llmCallLogService.delete(new EntityWrapper<LlmCallLog>()); return R.ok(); } private boolean isBlank(String s) { return s == null || s.trim().isEmpty(); } } src/main/java/com/zy/ai/controller/LlmRouteConfigController.java
New file @@ -0,0 +1,330 @@ package com.zy.ai.controller; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.core.annotations.ManagerAuth; import com.core.common.R; import com.fasterxml.jackson.databind.ObjectMapper; import com.zy.ai.entity.LlmRouteConfig; import com.zy.ai.service.LlmRouteConfigService; import com.zy.ai.service.LlmRoutingService; import com.zy.common.web.BaseController; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @Slf4j @RestController @RequestMapping("/ai/llm/config") @RequiredArgsConstructor public class LlmRouteConfigController extends BaseController { private final LlmRouteConfigService llmRouteConfigService; private final LlmRoutingService llmRoutingService; private final ObjectMapper objectMapper; @GetMapping("/list/auth") @ManagerAuth public R list() { EntityWrapper<LlmRouteConfig> wrapper = new EntityWrapper<>(); wrapper.orderBy("priority", true).orderBy("id", true); List<LlmRouteConfig> list = llmRouteConfigService.selectList(wrapper); return R.ok(list); } @PostMapping("/save/auth") @ManagerAuth public R save(@RequestBody LlmRouteConfig config) { if (config == null) { return R.error("åæ°ä¸è½ä¸ºç©º"); } if (isBlank(config.getBaseUrl()) || isBlank(config.getApiKey()) || isBlank(config.getModel())) { return R.error("å¿ é¡»å¡«å baseUrl/apiKey/model"); } if (config.getId() == null) { llmRoutingService.fillAndNormalize(config, true); llmRouteConfigService.insert(config); } else { LlmRouteConfig db = llmRouteConfigService.selectById(config.getId()); if (db == null) { return R.error("é ç½®ä¸åå¨"); } // ä¿çç»è®¡å段ï¼é¿å å端误è¦ç Integer failCount = db.getFailCount(); Integer successCount = db.getSuccessCount(); Integer consecutiveFailCount = db.getConsecutiveFailCount(); Date lastFailTime = db.getLastFailTime(); Date lastUsedTime = db.getLastUsedTime(); String lastError = db.getLastError(); llmRoutingService.fillAndNormalize(config, false); config.setFailCount(failCount); config.setSuccessCount(successCount); config.setConsecutiveFailCount(consecutiveFailCount); config.setLastFailTime(lastFailTime); config.setLastUsedTime(lastUsedTime); config.setLastError(lastError); config.setCreateTime(db.getCreateTime()); llmRouteConfigService.updateById(config); } llmRoutingService.evictCache(); return R.ok(config); } @PostMapping("/delete/auth") @ManagerAuth public R delete(@RequestParam("id") Long id) { if (id == null) { return R.error("idä¸è½ä¸ºç©º"); } llmRouteConfigService.deleteById(id); llmRoutingService.evictCache(); return R.ok(); } @PostMapping("/clearCooldown/auth") @ManagerAuth public R clearCooldown(@RequestParam("id") Long id) { if (id == null) { return R.error("idä¸è½ä¸ºç©º"); } LlmRouteConfig cfg = llmRouteConfigService.selectById(id); if (cfg == null) { return R.error("é ç½®ä¸åå¨"); } cfg.setCooldownUntil(null); cfg.setConsecutiveFailCount(0); cfg.setUpdateTime(new Date()); llmRouteConfigService.updateById(cfg); llmRoutingService.evictCache(); return R.ok(); } @PostMapping("/test/auth") @ManagerAuth public R test(@RequestBody LlmRouteConfig config) { if (config == null) { return R.error("åæ°ä¸è½ä¸ºç©º"); } if (isBlank(config.getBaseUrl()) || isBlank(config.getApiKey()) || isBlank(config.getModel())) { return R.error("æµè¯å¤±è´¥ï¼å¿ 须填å baseUrl/apiKey/model"); } Map<String, Object> data = llmRoutingService.testRoute(config); if (Boolean.TRUE.equals(data.get("ok")) && config.getId() != null) { LlmRouteConfig db = llmRouteConfigService.selectById(config.getId()); if (db != null) { db.setCooldownUntil(null); db.setConsecutiveFailCount(0); db.setUpdateTime(new Date()); llmRouteConfigService.updateById(db); llmRoutingService.evictCache(); } } return R.ok(data); } @GetMapping("/export/auth") @ManagerAuth public R exportConfig() { EntityWrapper<LlmRouteConfig> wrapper = new EntityWrapper<>(); wrapper.orderBy("priority", true).orderBy("id", true); List<LlmRouteConfig> list = llmRouteConfigService.selectList(wrapper); List<Map<String, Object>> routes = new ArrayList<>(); if (list != null) { for (LlmRouteConfig cfg : list) { routes.add(exportRow(cfg)); } } HashMap<String, Object> result = new HashMap<>(); result.put("version", "1.0"); result.put("exportTime", new Date()); result.put("count", routes.size()); result.put("routes", routes); return R.ok(result); } @PostMapping("/import/auth") @ManagerAuth @Transactional(rollbackFor = Exception.class) public R importConfig(@RequestBody Object body) { boolean replace = false; List<?> rawRoutes = null; if (body instanceof Map) { Map<?, ?> map = (Map<?, ?>) body; replace = parseBoolean(map.get("replace")); Object routesObj = map.get("routes"); if (routesObj instanceof List) { rawRoutes = (List<?>) routesObj; } } else if (body instanceof List) { rawRoutes = (List<?>) body; } if (rawRoutes == null || rawRoutes.isEmpty()) { return R.error("å¯¼å ¥æ°æ®ä¸ºç©ºææ ¼å¼ä¸æ£ç¡®ï¼å¿ é¡»å å« routes æ°ç»"); } int inserted = 0; int updated = 0; int skipped = 0; List<String> errors = new ArrayList<>(); List<LlmRouteConfig> validRoutes = new ArrayList<>(); for (int i = 0; i < rawRoutes.size(); i++) { Object row = rawRoutes.get(i); LlmRouteConfig cfg; try { cfg = objectMapper.convertValue(row, LlmRouteConfig.class); } catch (Exception e) { skipped++; errors.add("第" + (i + 1) + "æ¡è§£æå¤±è´¥: " + safeMsg(e.getMessage())); continue; } if (cfg == null) { skipped++; errors.add("第" + (i + 1) + "æ¡ä¸ºç©º"); continue; } cfg.setName(trim(cfg.getName())); cfg.setBaseUrl(trim(cfg.getBaseUrl())); cfg.setApiKey(trim(cfg.getApiKey())); cfg.setModel(trim(cfg.getModel())); cfg.setMemo(trim(cfg.getMemo())); if (isBlank(cfg.getBaseUrl()) || isBlank(cfg.getApiKey()) || isBlank(cfg.getModel())) { skipped++; errors.add("第" + (i + 1) + "æ¡ç¼ºå°å¿ å¡«åæ®µ baseUrl/apiKey/model"); continue; } validRoutes.add(cfg); } if (validRoutes.isEmpty()) { String firstError = errors.isEmpty() ? "" : ("ï¼é¦æ¡åå ï¼" + errors.get(0)); return R.error("å¯¼å ¥å¤±è´¥ï¼æ²¡æå¯ç¨é ç½®" + firstError); } if (replace) { llmRouteConfigService.delete(new EntityWrapper<LlmRouteConfig>()); } HashMap<Long, LlmRouteConfig> dbById = new HashMap<>(); if (!replace) { List<LlmRouteConfig> current = llmRouteConfigService.selectList(new EntityWrapper<>()); if (current != null) { for (LlmRouteConfig item : current) { if (item != null && item.getId() != null) { dbById.put(item.getId(), item); } } } } for (LlmRouteConfig cfg : validRoutes) { if (!replace && cfg.getId() != null && dbById.containsKey(cfg.getId())) { LlmRouteConfig db = dbById.get(cfg.getId()); Long keepId = db.getId(); Date createTime = db.getCreateTime(); Integer failCount = db.getFailCount(); Integer successCount = db.getSuccessCount(); Integer consecutiveFailCount = db.getConsecutiveFailCount(); Date lastFailTime = db.getLastFailTime(); Date lastUsedTime = db.getLastUsedTime(); String lastError = db.getLastError(); Date cooldownUntil = db.getCooldownUntil(); llmRoutingService.fillAndNormalize(cfg, false); cfg.setId(keepId); cfg.setCreateTime(createTime); cfg.setFailCount(failCount); cfg.setSuccessCount(successCount); cfg.setConsecutiveFailCount(consecutiveFailCount); cfg.setLastFailTime(lastFailTime); cfg.setLastUsedTime(lastUsedTime); cfg.setLastError(lastError); cfg.setCooldownUntil(cooldownUntil); llmRouteConfigService.updateById(cfg); updated++; continue; } cfg.setId(null); cfg.setCooldownUntil(null); cfg.setFailCount(0); cfg.setSuccessCount(0); cfg.setConsecutiveFailCount(0); cfg.setLastFailTime(null); cfg.setLastUsedTime(null); cfg.setLastError(null); llmRoutingService.fillAndNormalize(cfg, true); llmRouteConfigService.insert(cfg); inserted++; } llmRoutingService.evictCache(); HashMap<String, Object> result = new HashMap<>(); result.put("replace", replace); result.put("total", rawRoutes.size()); result.put("inserted", inserted); result.put("updated", updated); result.put("skipped", skipped); result.put("errorCount", errors.size()); if (!errors.isEmpty()) { int max = Math.min(errors.size(), 20); result.put("errors", errors.subList(0, max)); } log.info("LLMè·¯ç±å¯¼å ¥å®æ, replace={}, total={}, inserted={}, updated={}, skipped={}", replace, rawRoutes.size(), inserted, updated, skipped); return R.ok(result); } private Map<String, Object> exportRow(LlmRouteConfig cfg) { LinkedHashMap<String, Object> row = new LinkedHashMap<>(); row.put("id", cfg.getId()); row.put("name", cfg.getName()); row.put("baseUrl", cfg.getBaseUrl()); row.put("apiKey", cfg.getApiKey()); row.put("model", cfg.getModel()); row.put("thinking", cfg.getThinking()); row.put("priority", cfg.getPriority()); row.put("status", cfg.getStatus()); row.put("switchOnQuota", cfg.getSwitchOnQuota()); row.put("switchOnError", cfg.getSwitchOnError()); row.put("cooldownSeconds", cfg.getCooldownSeconds()); row.put("memo", cfg.getMemo()); return row; } private boolean parseBoolean(Object x) { if (x instanceof Boolean) return (Boolean) x; if (x == null) return false; String s = String.valueOf(x).trim(); return "1".equals(s) || "true".equalsIgnoreCase(s) || "yes".equalsIgnoreCase(s); } private String trim(String s) { return s == null ? null : s.trim(); } private String safeMsg(String s) { if (s == null) return ""; return s.length() > 200 ? s.substring(0, 200) : s; } private boolean isBlank(String s) { return s == null || s.trim().isEmpty(); } } src/main/java/com/zy/ai/entity/LlmCallLog.java
New file @@ -0,0 +1,219 @@ package com.zy.ai.entity; import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName; import com.baomidou.mybatisplus.enums.IdType; import java.io.Serializable; import java.util.Date; @TableName("sys_llm_call_log") public class LlmCallLog implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; @TableField("trace_id") private String traceId; private String scene; private Short stream; @TableField("attempt_no") private Integer attemptNo; @TableField("route_id") private Long routeId; @TableField("route_name") private String routeName; @TableField("base_url") private String baseUrl; private String model; private Short success; @TableField("http_status") private Integer httpStatus; @TableField("latency_ms") private Long latencyMs; @TableField("switch_mode") private String switchMode; @TableField("request_content") private String requestContent; @TableField("response_content") private String responseContent; @TableField("error_type") private String errorType; @TableField("error_message") private String errorMessage; private String extra; @TableField("create_time") private Date createTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTraceId() { return traceId; } public void setTraceId(String traceId) { this.traceId = traceId; } public String getScene() { return scene; } public void setScene(String scene) { this.scene = scene; } public Short getStream() { return stream; } public void setStream(Short stream) { this.stream = stream; } public Integer getAttemptNo() { return attemptNo; } public void setAttemptNo(Integer attemptNo) { this.attemptNo = attemptNo; } public Long getRouteId() { return routeId; } public void setRouteId(Long routeId) { this.routeId = routeId; } public String getRouteName() { return routeName; } public void setRouteName(String routeName) { this.routeName = routeName; } public String getBaseUrl() { return baseUrl; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } public String getModel() { return model; } public void setModel(String model) { this.model = model; } public Short getSuccess() { return success; } public void setSuccess(Short success) { this.success = success; } public Integer getHttpStatus() { return httpStatus; } public void setHttpStatus(Integer httpStatus) { this.httpStatus = httpStatus; } public Long getLatencyMs() { return latencyMs; } public void setLatencyMs(Long latencyMs) { this.latencyMs = latencyMs; } public String getSwitchMode() { return switchMode; } public void setSwitchMode(String switchMode) { this.switchMode = switchMode; } public String getRequestContent() { return requestContent; } public void setRequestContent(String requestContent) { this.requestContent = requestContent; } public String getResponseContent() { return responseContent; } public void setResponseContent(String responseContent) { this.responseContent = responseContent; } public String getErrorType() { return errorType; } public void setErrorType(String errorType) { this.errorType = errorType; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } public String getExtra() { return extra; } public void setExtra(String extra) { this.extra = extra; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } } src/main/java/com/zy/ai/entity/LlmRouteConfig.java
New file @@ -0,0 +1,249 @@ package com.zy.ai.entity; import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName; import com.baomidou.mybatisplus.enums.IdType; import java.io.Serializable; import java.util.Date; @TableName("sys_llm_route") public class LlmRouteConfig implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; private String name; @TableField("base_url") private String baseUrl; @TableField("api_key") private String apiKey; private String model; /** * 1 å¼å¯æ·±åº¦æè 0 å ³é */ private Short thinking; /** * æ°åè¶å°ä¼å 级è¶é« */ private Integer priority; /** * 1 å¯ç¨ 0 ç¦ç¨ */ private Short status; @TableField("switch_on_quota") private Short switchOnQuota; @TableField("switch_on_error") private Short switchOnError; @TableField("cooldown_seconds") private Integer cooldownSeconds; @TableField("cooldown_until") private Date cooldownUntil; @TableField("fail_count") private Integer failCount; @TableField("success_count") private Integer successCount; @TableField("consecutive_fail_count") private Integer consecutiveFailCount; @TableField("last_error") private String lastError; @TableField("last_used_time") private Date lastUsedTime; @TableField("last_fail_time") private Date lastFailTime; @TableField("create_time") private Date createTime; @TableField("update_time") private Date updateTime; private String memo; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getBaseUrl() { return baseUrl; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getModel() { return model; } public void setModel(String model) { this.model = model; } public Short getThinking() { return thinking; } public void setThinking(Short thinking) { this.thinking = thinking; } public Integer getPriority() { return priority; } public void setPriority(Integer priority) { this.priority = priority; } public Short getStatus() { return status; } public void setStatus(Short status) { this.status = status; } public Short getSwitchOnQuota() { return switchOnQuota; } public void setSwitchOnQuota(Short switchOnQuota) { this.switchOnQuota = switchOnQuota; } public Short getSwitchOnError() { return switchOnError; } public void setSwitchOnError(Short switchOnError) { this.switchOnError = switchOnError; } public Integer getCooldownSeconds() { return cooldownSeconds; } public void setCooldownSeconds(Integer cooldownSeconds) { this.cooldownSeconds = cooldownSeconds; } public Date getCooldownUntil() { return cooldownUntil; } public void setCooldownUntil(Date cooldownUntil) { this.cooldownUntil = cooldownUntil; } public Integer getFailCount() { return failCount; } public void setFailCount(Integer failCount) { this.failCount = failCount; } public Integer getSuccessCount() { return successCount; } public void setSuccessCount(Integer successCount) { this.successCount = successCount; } public Integer getConsecutiveFailCount() { return consecutiveFailCount; } public void setConsecutiveFailCount(Integer consecutiveFailCount) { this.consecutiveFailCount = consecutiveFailCount; } public String getLastError() { return lastError; } public void setLastError(String lastError) { this.lastError = lastError; } public Date getLastUsedTime() { return lastUsedTime; } public void setLastUsedTime(Date lastUsedTime) { this.lastUsedTime = lastUsedTime; } public Date getLastFailTime() { return lastFailTime; } public void setLastFailTime(Date lastFailTime) { this.lastFailTime = lastFailTime; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } public String getMemo() { return memo; } public void setMemo(String memo) { this.memo = memo; } } src/main/java/com/zy/ai/mapper/LlmCallLogMapper.java
New file @@ -0,0 +1,11 @@ package com.zy.ai.mapper; import com.baomidou.mybatisplus.mapper.BaseMapper; import com.zy.ai.entity.LlmCallLog; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface LlmCallLogMapper extends BaseMapper<LlmCallLog> { } src/main/java/com/zy/ai/mapper/LlmRouteConfigMapper.java
New file @@ -0,0 +1,11 @@ package com.zy.ai.mapper; import com.baomidou.mybatisplus.mapper.BaseMapper; import com.zy.ai.entity.LlmRouteConfig; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface LlmRouteConfigMapper extends BaseMapper<LlmRouteConfig> { } src/main/java/com/zy/ai/service/LlmCallLogService.java
New file @@ -0,0 +1,8 @@ package com.zy.ai.service; import com.baomidou.mybatisplus.service.IService; import com.zy.ai.entity.LlmCallLog; public interface LlmCallLogService extends IService<LlmCallLog> { void saveIgnoreError(LlmCallLog log); } src/main/java/com/zy/ai/service/LlmRouteConfigService.java
New file @@ -0,0 +1,7 @@ package com.zy.ai.service; import com.baomidou.mybatisplus.service.IService; import com.zy.ai.entity.LlmRouteConfig; public interface LlmRouteConfigService extends IService<LlmRouteConfig> { } src/main/java/com/zy/ai/service/LlmRoutingService.java
New file @@ -0,0 +1,286 @@ package com.zy.ai.service; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.zy.ai.entity.LlmRouteConfig; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j @Service @RequiredArgsConstructor public class LlmRoutingService { private static final long CACHE_TTL_MS = 3000L; private final LlmRouteConfigService llmRouteConfigService; private volatile List<LlmRouteConfig> allRouteCache = Collections.emptyList(); private volatile long cacheExpireAt = 0L; private static final Comparator<LlmRouteConfig> ROUTE_ORDER = (a, b) -> { int pa = a == null || a.getPriority() == null ? Integer.MAX_VALUE : a.getPriority(); int pb = b == null || b.getPriority() == null ? Integer.MAX_VALUE : b.getPriority(); if (pa != pb) return Integer.compare(pa, pb); long ia = a == null || a.getId() == null ? Long.MAX_VALUE : a.getId(); long ib = b == null || b.getId() == null ? Long.MAX_VALUE : b.getId(); return Long.compare(ia, ib); }; public void evictCache() { cacheExpireAt = 0L; } public List<LlmRouteConfig> listAllOrdered() { return new ArrayList<>(loadAllRoutes()); } public List<LlmRouteConfig> listAvailableRoutes() { Date now = new Date(); List<LlmRouteConfig> result = new ArrayList<>(); List<LlmRouteConfig> coolingRoutes = new ArrayList<>(); int total = 0; int disabled = 0; int invalid = 0; for (LlmRouteConfig c : loadAllRoutes()) { total++; if (!isEnabled(c)) { disabled++; continue; } if (isBlank(c.getBaseUrl()) || isBlank(c.getApiKey()) || isBlank(c.getModel())) { invalid++; continue; } if (isCooling(c, now)) { coolingRoutes.add(c); continue; } result.add(c); } if (result.isEmpty() && !coolingRoutes.isEmpty()) { // é¿å ææè·¯ç±é½å¤äºå·å´æ¶ç³»ç»å®å ¨ä¸å¯ç¨ï¼é级å 许使ç¨å·å´è·¯ç± coolingRoutes.sort(ROUTE_ORDER); log.warn("LLM è·¯ç±åå¤äºå·å´ï¼é级å¯ç¨å·å´è·¯ç±ãcooling={}, total={}", coolingRoutes.size(), total); return coolingRoutes; } result.sort(ROUTE_ORDER); if (result.isEmpty()) { log.warn("æªæ¾å°å¯ç¨ LLM è·¯ç±ãtotal={}, disabled={}, invalid={}", total, disabled, invalid); } return result; } public void markSuccess(Long routeId) { if (routeId == null) return; try { LlmRouteConfig db = llmRouteConfigService.selectById(routeId); if (db == null) return; db.setSuccessCount(nvl(db.getSuccessCount()) + 1); db.setConsecutiveFailCount(0); db.setLastUsedTime(new Date()); db.setUpdateTime(new Date()); llmRouteConfigService.updateById(db); evictCache(); } catch (Exception e) { log.warn("æ´æ°è·¯ç±æåç¶æå¤±è´¥, routeId={}", routeId, e); } } public void markFailure(Long routeId, String errorText, boolean enterCooldown, Integer cooldownSeconds) { if (routeId == null) return; try { LlmRouteConfig db = llmRouteConfigService.selectById(routeId); if (db == null) return; Date now = new Date(); db.setFailCount(nvl(db.getFailCount()) + 1); db.setConsecutiveFailCount(nvl(db.getConsecutiveFailCount()) + 1); db.setLastFailTime(now); db.setLastError(trimError(errorText)); if (enterCooldown) { int sec = cooldownSeconds != null && cooldownSeconds > 0 ? cooldownSeconds : defaultCooldown(db.getCooldownSeconds()); db.setCooldownUntil(new Date(now.getTime() + sec * 1000L)); } db.setUpdateTime(now); llmRouteConfigService.updateById(db); evictCache(); } catch (Exception e) { log.warn("æ´æ°è·¯ç±å¤±è´¥ç¶æå¤±è´¥, routeId={}", routeId, e); } } private int defaultCooldown(Integer sec) { return sec == null || sec <= 0 ? 300 : sec; } private String trimError(String err) { if (err == null) return null; String x = err.replace("\n", " ").replace("\r", " "); return x.length() > 500 ? x.substring(0, 500) : x; } private Integer nvl(Integer x) { return x == null ? 0 : x; } private boolean isEnabled(LlmRouteConfig c) { return c != null && c.getStatus() != null && c.getStatus() == 1; } private boolean isCooling(LlmRouteConfig c, Date now) { return c != null && c.getCooldownUntil() != null && c.getCooldownUntil().after(now); } private List<LlmRouteConfig> loadAllRoutes() { long now = System.currentTimeMillis(); if (now < cacheExpireAt && allRouteCache != null) { return allRouteCache; } synchronized (this) { now = System.currentTimeMillis(); if (now < cacheExpireAt && allRouteCache != null) { return allRouteCache; } EntityWrapper<LlmRouteConfig> wrapper = new EntityWrapper<>(); wrapper.orderBy("priority", true).orderBy("id", true); List<LlmRouteConfig> list = llmRouteConfigService.selectList(wrapper); if (list == null) { allRouteCache = Collections.emptyList(); } else { list.sort(ROUTE_ORDER); allRouteCache = list; } cacheExpireAt = System.currentTimeMillis() + CACHE_TTL_MS; return allRouteCache; } } private String safe(String s) { return s == null ? "" : s.trim(); } private boolean isBlank(String s) { return s == null || s.trim().isEmpty(); } public LlmRouteConfig fillAndNormalize(LlmRouteConfig cfg, boolean isCreate) { Date now = new Date(); if (isBlank(cfg.getName())) { cfg.setName("LLM_ROUTE_" + now.getTime()); } if (cfg.getThinking() == null) { cfg.setThinking((short) 0); } if (cfg.getPriority() == null) { cfg.setPriority(100); } if (cfg.getStatus() == null) { cfg.setStatus((short) 1); } if (cfg.getSwitchOnQuota() == null) { cfg.setSwitchOnQuota((short) 1); } if (cfg.getSwitchOnError() == null) { cfg.setSwitchOnError((short) 1); } if (cfg.getCooldownSeconds() == null || cfg.getCooldownSeconds() < 0) { cfg.setCooldownSeconds(300); } if (cfg.getFailCount() == null) { cfg.setFailCount(0); } if (cfg.getSuccessCount() == null) { cfg.setSuccessCount(0); } if (cfg.getConsecutiveFailCount() == null) { cfg.setConsecutiveFailCount(0); } if (isCreate) { cfg.setCreateTime(now); } cfg.setUpdateTime(now); return cfg; } public Map<String, Object> testRoute(LlmRouteConfig cfg) { HashMap<String, Object> result = new HashMap<>(); long start = System.currentTimeMillis(); try { TestHttpResult raw = testJavaRoute(cfg); fillTestResult(result, raw, start); } catch (Exception e) { result.put("ok", false); result.put("statusCode", -1); result.put("latencyMs", System.currentTimeMillis() - start); result.put("message", "æµè¯å¼å¸¸: " + safe(e.getMessage())); result.put("responseSnippet", ""); } return result; } private void fillTestResult(HashMap<String, Object> result, TestHttpResult raw, long start) { boolean ok = raw.statusCode >= 200 && raw.statusCode < 300; result.put("ok", ok); result.put("statusCode", raw.statusCode); result.put("latencyMs", System.currentTimeMillis() - start); result.put("message", ok ? "æµè¯æå" : "æµè¯å¤±è´¥"); result.put("responseSnippet", trimBody(raw.body)); } private TestHttpResult testJavaRoute(LlmRouteConfig cfg) { HashMap<String, Object> req = new HashMap<>(); req.put("model", cfg.getModel()); List<Map<String, String>> messages = new ArrayList<>(); HashMap<String, String> msg = new HashMap<>(); msg.put("role", "user"); msg.put("content", "ping"); messages.add(msg); req.put("messages", messages); req.put("stream", false); req.put("max_tokens", 8); req.put("temperature", 0); WebClient client = WebClient.builder().baseUrl(cfg.getBaseUrl()).build(); return client.post() .uri("/chat/completions") .header(HttpHeaders.AUTHORIZATION, "Bearer " + cfg.getApiKey()) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM) .bodyValue(req) .exchangeToMono(resp -> resp.bodyToMono(String.class) .defaultIfEmpty("") .map(body -> new TestHttpResult(resp.rawStatusCode(), body))) .timeout(Duration.ofSeconds(12)) .onErrorResume(ex -> Mono.just(new TestHttpResult(-1, safe(ex.getMessage())))) .block(); } private String trimBody(String body) { String x = safe(body).replace("\r", " ").replace("\n", " "); return x.length() > 300 ? x.substring(0, 300) : x; } private static class TestHttpResult { private final int statusCode; private final String body; private TestHttpResult(int statusCode, String body) { this.statusCode = statusCode; this.body = body; } } } src/main/java/com/zy/ai/service/impl/LlmCallLogServiceImpl.java
New file @@ -0,0 +1,33 @@ package com.zy.ai.service.impl; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.zy.ai.entity.LlmCallLog; import com.zy.ai.mapper.LlmCallLogMapper; import com.zy.ai.service.LlmCallLogService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Service("llmCallLogService") @Slf4j public class LlmCallLogServiceImpl extends ServiceImpl<LlmCallLogMapper, LlmCallLog> implements LlmCallLogService { private volatile boolean disabled = false; @Override public void saveIgnoreError(LlmCallLog logItem) { if (logItem == null || disabled) { return; } try { insert(logItem); } catch (Exception e) { String msg = e.getMessage() == null ? "" : e.getMessage(); if (msg.contains("doesn't exist") || msg.contains("ä¸åå¨")) { disabled = true; log.warn("LLMè°ç¨æ¥å¿è¡¨ä¸åå¨ï¼æ¥å¿è®°å½å·²èªå¨å ³éï¼è¯·å æ§è¡å»ºè¡¨SQL"); return; } log.warn("åå ¥LLMè°ç¨æ¥å¿å¤±è´¥: {}", msg); } } } src/main/java/com/zy/ai/service/impl/LlmRouteConfigServiceImpl.java
New file @@ -0,0 +1,11 @@ package com.zy.ai.service.impl; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.zy.ai.entity.LlmRouteConfig; import com.zy.ai.mapper.LlmRouteConfigMapper; import com.zy.ai.service.LlmRouteConfigService; import org.springframework.stereotype.Service; @Service("llmRouteConfigService") public class LlmRouteConfigServiceImpl extends ServiceImpl<LlmRouteConfigMapper, LlmRouteConfig> implements LlmRouteConfigService { } src/main/java/com/zy/asrs/domain/param/DualCrnUpdateTaskNoParam.java
New file @@ -0,0 +1,10 @@ package com.zy.asrs.domain.param; import lombok.Data; @Data public class DualCrnUpdateTaskNoParam { private Integer crnNo; private Integer station; private Integer taskNo; } src/main/java/com/zy/asrs/domain/param/StationCommandBarcodeParam.java
New file @@ -0,0 +1,14 @@ package com.zy.asrs.domain.param; import lombok.Data; @Data public class StationCommandBarcodeParam { // ç«ç¹ç¼å· private Integer stationId; // æ¡ç å¼ï¼å 许为空åç¬¦ä¸²ï¼ private String barcode; } src/main/java/com/zy/asrs/domain/vo/StationCycleCapacityVo.java
New file @@ -0,0 +1,29 @@ package com.zy.asrs.domain.vo; import lombok.Data; import java.util.ArrayList; import java.util.Date; import java.util.List; @Data public class StationCycleCapacityVo { // 循ç¯åæç» private List<StationCycleLoopVo> loopList = new ArrayList<>(); // 循ç¯åæ°é private Integer loopCount = 0; // 循ç¯åç«ç¹æ»æ° private Integer totalStationCount = 0; // 循ç¯å䏿任å¡ç«ç¹æ»æ° private Integer taskStationCount = 0; // å½åæ¿è½½éï¼0-1ï¼ï¼å½å任塿° / 循ç¯åæ»ç«ç¹æ° private Double currentLoad = 0.0; // ææ°å·æ°æ¶é´ private Date refreshTime; } src/main/java/com/zy/asrs/domain/vo/StationCycleLoopVo.java
New file @@ -0,0 +1,28 @@ package com.zy.asrs.domain.vo; import lombok.Data; import java.util.ArrayList; import java.util.List; @Data public class StationCycleLoopVo { // 循ç¯ååºå·ï¼ä»1å¼å§ï¼ private Integer loopNo; // 循ç¯åå ç«ç¹ç¼å· private List<Integer> stationIdList = new ArrayList<>(); // 循ç¯åå åå¨çå·¥ä½å· private List<Integer> workNoList = new ArrayList<>(); // 循ç¯åç«ç¹æ»æ° private Integer stationCount = 0; // 循ç¯åå æä»»å¡ç«ç¹æ° private Integer taskCount = 0; // å½åæ¿è½½éï¼0-1ï¼ï¼å½å任塿° / å½å循ç¯åæ»ç«ç¹æ° private Double currentLoad = 0.0; } src/main/java/com/zy/asrs/service/StationCycleCapacityService.java
New file @@ -0,0 +1,13 @@ package com.zy.asrs.service; import com.zy.asrs.domain.vo.StationCycleCapacityVo; public interface StationCycleCapacityService { // ç«å³å·æ°å¾ªç¯å䏿¿è½½éå¿«ç § void refreshSnapshot(); // è·åææ°å¾ªç¯å䏿¿è½½éå¿«ç § StationCycleCapacityVo getLatestSnapshot(); } src/main/java/com/zy/asrs/service/impl/StationCycleCapacityServiceImpl.java
New file @@ -0,0 +1,623 @@ package com.zy.asrs.service.impl; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.zy.asrs.domain.vo.StationCycleCapacityVo; import com.zy.asrs.domain.vo.StationCycleLoopVo; import com.zy.asrs.entity.BasDevp; import com.zy.asrs.entity.DeviceConfig; import com.zy.asrs.service.BasMapService; import com.zy.asrs.service.BasDevpService; import com.zy.asrs.service.DeviceConfigService; import com.zy.asrs.service.StationCycleCapacityService; import com.zy.common.model.NavigateNode; import com.zy.common.utils.RedisUtil; import com.zy.common.utils.NavigateSolution; import com.zy.core.cache.SlaveConnection; import com.zy.core.enums.RedisKeyType; import com.zy.core.enums.SlaveType; import com.zy.core.model.StationObjModel; import com.zy.core.model.protocol.StationProtocol; import com.zy.core.thread.StationThread; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @Service("stationCycleCapacityService") @Slf4j public class StationCycleCapacityServiceImpl implements StationCycleCapacityService { private static final long LOOP_LOAD_RESERVE_EXPIRE_MILLIS = 120_000L; @Autowired private BasMapService basMapService; @Autowired private DeviceConfigService deviceConfigService; @Autowired private BasDevpService basDevpService; @Autowired private RedisUtil redisUtil; private final AtomicReference<StationCycleCapacityVo> snapshotRef = new AtomicReference<>(new StationCycleCapacityVo()); @Override public synchronized void refreshSnapshot() { try { StationCycleCapacityVo snapshot = buildSnapshot(); snapshotRef.set(snapshot); } catch (Exception e) { log.error("å·æ°å¾ªç¯åæ¿è½½é失败", e); } } @Override public StationCycleCapacityVo getLatestSnapshot() { StationCycleCapacityVo snapshot = snapshotRef.get(); if (snapshot == null || snapshot.getRefreshTime() == null) { refreshSnapshot(); snapshot = snapshotRef.get(); } return snapshot == null ? new StationCycleCapacityVo() : snapshot; } private StationCycleCapacityVo buildSnapshot() { GraphContext context = buildStationGraph(); Map<Integer, Integer> workNoMap = buildStationWorkNoMap(); Set<Integer> availableStationSet = new HashSet<>(context.graph.keySet()); availableStationSet.removeAll(context.excludeStationSet); Map<Integer, Set<Integer>> filteredGraph = new HashMap<>(); for (Integer stationId : availableStationSet) { Set<Integer> nextSet = context.graph.getOrDefault(stationId, Collections.emptySet()); Set<Integer> filteredNext = new HashSet<>(); for (Integer nextId : nextSet) { if (availableStationSet.contains(nextId)) { filteredNext.add(nextId); } } filteredGraph.put(stationId, filteredNext); } List<Set<Integer>> sccList = findStrongConnectedComponents(filteredGraph); List<StationCycleLoopVo> loopList = new ArrayList<>(); int loopNo = 1; int totalStationCount = 0; int taskStationCount = 0; Set<Integer> actualWorkNoSet = new HashSet<>(); for (Set<Integer> scc : sccList) { if (!isCycleScc(scc, filteredGraph)) { continue; } // 对 SCC åå䏿¬¡âç¯æ ¸å¿âå¥ç¦»ï¼å餿æ/æ»è¡åèç¹ List<Set<Integer>> coreLoopList = extractCoreLoopComponents(scc, filteredGraph); for (Set<Integer> coreLoop : coreLoopList) { List<Integer> stationIdList = new ArrayList<>(coreLoop); Collections.sort(stationIdList); List<Integer> workNoList = new ArrayList<>(); int currentLoopTaskCount = 0; for (Integer stationId : stationIdList) { Integer workNo = workNoMap.get(stationId); if (workNo != null && workNo > 0) { workNoList.add(workNo); currentLoopTaskCount++; actualWorkNoSet.add(workNo); } } StationCycleLoopVo loopVo = new StationCycleLoopVo(); loopVo.setLoopNo(loopNo++); loopVo.setStationIdList(stationIdList); loopVo.setWorkNoList(workNoList); loopVo.setStationCount(stationIdList.size()); loopVo.setTaskCount(currentLoopTaskCount); loopVo.setCurrentLoad(calcCurrentLoad(currentLoopTaskCount, stationIdList.size())); loopList.add(loopVo); totalStationCount += stationIdList.size(); taskStationCount += currentLoopTaskCount; } } int reserveTaskCount = mergeReserveTaskCount(loopList, actualWorkNoSet); taskStationCount += reserveTaskCount; StationCycleCapacityVo vo = new StationCycleCapacityVo(); vo.setLoopList(loopList); vo.setLoopCount(loopList.size()); vo.setTotalStationCount(totalStationCount); vo.setTaskStationCount(taskStationCount); vo.setCurrentLoad(calcCurrentLoad(taskStationCount, totalStationCount)); vo.setRefreshTime(new Date()); return vo; } private int mergeReserveTaskCount(List<StationCycleLoopVo> loopList, Set<Integer> actualWorkNoSet) { if (loopList == null || loopList.isEmpty()) { return 0; } Map<Object, Object> reserveMap = redisUtil.hmget(RedisKeyType.STATION_CYCLE_LOAD_RESERVE.key); if (reserveMap == null || reserveMap.isEmpty()) { return 0; } Map<Integer, StationCycleLoopVo> loopMap = new HashMap<>(); Map<Integer, StationCycleLoopVo> stationLoopMap = new HashMap<>(); for (StationCycleLoopVo loopVo : loopList) { if (loopVo != null && loopVo.getLoopNo() != null) { loopMap.put(loopVo.getLoopNo(), loopVo); } if (loopVo == null || loopVo.getStationIdList() == null) { continue; } for (Integer stationId : loopVo.getStationIdList()) { if (stationId != null) { stationLoopMap.put(stationId, loopVo); } } } long now = System.currentTimeMillis(); int mergedCount = 0; List<Object> removeFieldList = new ArrayList<>(); for (Map.Entry<Object, Object> entry : reserveMap.entrySet()) { ReserveRecord record = parseReserveRecord(entry.getKey(), entry.getValue()); if (record == null) { removeFieldList.add(entry.getKey()); continue; } if (actualWorkNoSet.contains(record.wrkNo)) { removeFieldList.add(entry.getKey()); continue; } if (record.createTime <= 0 || now - record.createTime > LOOP_LOAD_RESERVE_EXPIRE_MILLIS) { removeFieldList.add(entry.getKey()); continue; } StationCycleLoopVo loopVo = loopMap.get(record.loopNo); if (loopVo == null && record.hitStationId != null) { loopVo = stationLoopMap.get(record.hitStationId); } if (loopVo == null) { removeFieldList.add(entry.getKey()); continue; } List<Integer> workNoList = loopVo.getWorkNoList(); if (workNoList == null) { workNoList = new ArrayList<>(); loopVo.setWorkNoList(workNoList); } if (workNoList.contains(record.wrkNo)) { continue; } workNoList.add(record.wrkNo); Collections.sort(workNoList); int mergedTaskCount = toNonNegative(loopVo.getTaskCount()) + 1; loopVo.setTaskCount(mergedTaskCount); loopVo.setCurrentLoad(calcCurrentLoad(mergedTaskCount, toNonNegative(loopVo.getStationCount()))); mergedCount++; } if (!removeFieldList.isEmpty()) { redisUtil.hdel(RedisKeyType.STATION_CYCLE_LOAD_RESERVE.key, removeFieldList.toArray()); } return mergedCount; } private ReserveRecord parseReserveRecord(Object fieldObj, Object valueObj) { if (fieldObj == null || valueObj == null) { return null; } Integer fieldWrkNo = parseInteger(String.valueOf(fieldObj)); if (fieldWrkNo == null || fieldWrkNo <= 0) { return null; } JSONObject jsonObject; try { jsonObject = JSON.parseObject(String.valueOf(valueObj)); } catch (Exception e) { return null; } if (jsonObject == null) { return null; } Integer wrkNo = jsonObject.getInteger("wrkNo"); Integer loopNo = jsonObject.getInteger("loopNo"); Integer hitStationId = jsonObject.getInteger("hitStationId"); Long createTime = jsonObject.getLong("createTime"); if (wrkNo == null || wrkNo <= 0) { wrkNo = fieldWrkNo; } if ((loopNo == null || loopNo <= 0) && (hitStationId == null || hitStationId <= 0)) { return null; } if (createTime == null || createTime <= 0) { return null; } ReserveRecord record = new ReserveRecord(); record.wrkNo = wrkNo; record.loopNo = loopNo; record.hitStationId = hitStationId; record.createTime = createTime; return record; } private Integer parseInteger(String value) { if (value == null || value.trim().isEmpty()) { return null; } try { return Integer.parseInt(value.trim()); } catch (Exception e) { return null; } } private int toNonNegative(Integer value) { if (value == null || value < 0) { return 0; } return value; } private static class ReserveRecord { private Integer wrkNo; private Integer loopNo; private Integer hitStationId; private Long createTime; } private double calcCurrentLoad(int taskCount, int stationCount) { if (stationCount <= 0 || taskCount <= 0) { return 0.0; } double value = (double) taskCount / (double) stationCount; if (value < 0.0) { return 0.0; } if (value > 1.0) { return 1.0; } return value; } private GraphContext buildStationGraph() { GraphContext context = new GraphContext(); List<Integer> levList = basMapService.getLevList(); if (levList == null || levList.isEmpty()) { return context; } NavigateSolution navigateSolution = new NavigateSolution(); List<Integer> sortedLevList = new ArrayList<>(levList); sortedLevList = new ArrayList<>(new HashSet<>(sortedLevList)); Collections.sort(sortedLevList); // for (Integer lev : sortedLevList) { // List<List<NavigateNode>> stationMap; // try { // stationMap = navigateSolution.getStationMap(lev); // } catch (Exception e) { // log.warn("å 载楼å±{}å°å¾å¤±è´¥ï¼è·³è¿å¾ªç¯å计ç®", lev); // continue; // } // if (stationMap == null || stationMap.isEmpty()) { // continue; // } // // for (List<NavigateNode> row : stationMap) { // for (NavigateNode node : row) { // JSONObject valueObj = parseNodeValue(node.getNodeValue()); // if (valueObj == null) { // continue; // } // Integer stationId = valueObj.getInteger("stationId"); // if (stationId == null) { // continue; // } // // context.graph.computeIfAbsent(stationId, k -> new HashSet<>()); // if (isExcludeStation(valueObj)) { // context.excludeStationSet.add(stationId); // } // // List<NavigateNode> nextNodeList = navigateSolution.extend_current_node(stationMap, node); // if (nextNodeList == null || nextNodeList.isEmpty()) { // continue; // } // for (NavigateNode nextNode : nextNodeList) { // JSONObject nextValueObj = parseNodeValue(nextNode.getNodeValue()); // if (nextValueObj == null) { // continue; // } // Integer nextStationId = nextValueObj.getInteger("stationId"); // if (nextStationId == null || stationId.equals(nextStationId)) { // continue; // } // // context.graph.computeIfAbsent(nextStationId, k -> new HashSet<>()); // context.graph.get(stationId).add(nextStationId); // } // } // } // } appendExcludeStationsFromDeviceConfig(context.excludeStationSet); return context; } private void appendExcludeStationsFromDeviceConfig(Set<Integer> excludeStationSet) { List<BasDevp> basDevpList = basDevpService.selectList(new EntityWrapper<>()); if (basDevpList == null || basDevpList.isEmpty()) { return; } for (BasDevp basDevp : basDevpList) { List<StationObjModel> inStationList = basDevp.getInStationList$(); for (StationObjModel stationObjModel : inStationList) { if (stationObjModel != null && stationObjModel.getStationId() != null) { excludeStationSet.add(stationObjModel.getStationId()); } } List<StationObjModel> barcodeStationList = basDevp.getBarcodeStationList$(); for (StationObjModel stationObjModel : barcodeStationList) { if (stationObjModel != null && stationObjModel.getStationId() != null) { excludeStationSet.add(stationObjModel.getStationId()); } } } } private JSONObject parseNodeValue(String nodeValue) { if (nodeValue == null || nodeValue.trim().isEmpty()) { return null; } try { return JSON.parseObject(nodeValue); } catch (Exception ignore) { return null; } } private boolean isExcludeStation(JSONObject valueObj) { Integer isInStation = valueObj.getInteger("isInStation"); Integer isBarcodeStation = valueObj.getInteger("isBarcodeStation"); return (isInStation != null && isInStation == 1) || (isBarcodeStation != null && isBarcodeStation == 1); } private Map<Integer, Integer> buildStationWorkNoMap() { Map<Integer, Integer> workNoMap = new HashMap<>(); List<DeviceConfig> devpList = deviceConfigService.selectList(new EntityWrapper<DeviceConfig>() .eq("device_type", String.valueOf(SlaveType.Devp))); if (devpList == null || devpList.isEmpty()) { return workNoMap; } for (DeviceConfig deviceConfig : devpList) { StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, deviceConfig.getDeviceNo()); if (stationThread == null) { continue; } Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap(); if (statusMap == null || statusMap.isEmpty()) { continue; } for (StationProtocol protocol : statusMap.values()) { if (protocol == null || protocol.getStationId() == null) { continue; } Integer taskNo = protocol.getTaskNo(); if (taskNo != null && taskNo > 0) { workNoMap.put(protocol.getStationId(), taskNo); } } } return workNoMap; } private List<Set<Integer>> findStrongConnectedComponents(Map<Integer, Set<Integer>> graph) { List<Set<Integer>> result = new ArrayList<>(); if (graph == null || graph.isEmpty()) { return result; } Map<Integer, Integer> indexMap = new HashMap<>(); Map<Integer, Integer> lowLinkMap = new HashMap<>(); Deque<Integer> stack = new ArrayDeque<>(); Set<Integer> inStack = new HashSet<>(); int[] index = new int[]{0}; List<Integer> nodeList = new ArrayList<>(graph.keySet()); Collections.sort(nodeList); for (Integer node : nodeList) { if (!indexMap.containsKey(node)) { strongConnect(node, graph, indexMap, lowLinkMap, stack, inStack, index, result); } } return result; } private void strongConnect(Integer node, Map<Integer, Set<Integer>> graph, Map<Integer, Integer> indexMap, Map<Integer, Integer> lowLinkMap, Deque<Integer> stack, Set<Integer> inStack, int[] index, List<Set<Integer>> result) { indexMap.put(node, index[0]); lowLinkMap.put(node, index[0]); index[0]++; stack.push(node); inStack.add(node); List<Integer> nextList = new ArrayList<>(graph.getOrDefault(node, Collections.emptySet())); Collections.sort(nextList); for (Integer next : nextList) { if (!indexMap.containsKey(next)) { strongConnect(next, graph, indexMap, lowLinkMap, stack, inStack, index, result); lowLinkMap.put(node, Math.min(lowLinkMap.get(node), lowLinkMap.get(next))); } else if (inStack.contains(next)) { lowLinkMap.put(node, Math.min(lowLinkMap.get(node), indexMap.get(next))); } } if (!lowLinkMap.get(node).equals(indexMap.get(node))) { return; } Set<Integer> scc = new HashSet<>(); while (!stack.isEmpty()) { Integer top = stack.pop(); inStack.remove(top); scc.add(top); if (top.equals(node)) { break; } } result.add(scc); } private boolean isCycleScc(Set<Integer> scc, Map<Integer, Set<Integer>> graph) { if (scc == null || scc.isEmpty()) { return false; } if (scc.size() > 1) { return true; } Integer onlyNode = scc.iterator().next(); Set<Integer> nextSet = graph.getOrDefault(onlyNode, Collections.emptySet()); return nextSet.contains(onlyNode); } /** * ä» SCC 䏿åå¾ªç¯æ ¸å¿ï¼ * 1) 转æ åå¾ * 2) éå½å¥ç¦»åº¦æ°<2çèç¹ï¼2-coreï¼ * 3) å°å©ä½èç¹ææè¿éåéï¼æ¯ä¸ªåé>=3æè®¤å®ä¸ºå¾ªç¯å */ private List<Set<Integer>> extractCoreLoopComponents(Set<Integer> scc, Map<Integer, Set<Integer>> graph) { List<Set<Integer>> result = new ArrayList<>(); if (scc == null || scc.isEmpty()) { return result; } // æå»º SCC å æ å黿¥ Map<Integer, Set<Integer>> undirectedMap = new HashMap<>(); for (Integer node : scc) { undirectedMap.put(node, new HashSet<>()); } for (Integer from : scc) { Set<Integer> nextSet = graph.getOrDefault(from, Collections.emptySet()); for (Integer to : nextSet) { if (!scc.contains(to) || from.equals(to)) { continue; } undirectedMap.get(from).add(to); undirectedMap.get(to).add(from); } } // 2-core å¥ç¦» Set<Integer> alive = new HashSet<>(scc); Map<Integer, Integer> degreeMap = new HashMap<>(); ArrayDeque<Integer> queue = new ArrayDeque<>(); for (Integer node : scc) { int degree = undirectedMap.getOrDefault(node, Collections.emptySet()).size(); degreeMap.put(node, degree); if (degree < 2) { queue.offer(node); } } while (!queue.isEmpty()) { Integer node = queue.poll(); if (!alive.remove(node)) { continue; } for (Integer next : undirectedMap.getOrDefault(node, Collections.emptySet())) { if (!alive.contains(next)) { continue; } int newDegree = degreeMap.getOrDefault(next, 0) - 1; degreeMap.put(next, newDegree); if (newDegree < 2) { queue.offer(next); } } } if (alive.size() < 3) { return result; } // æåè¿éåé Set<Integer> visited = new HashSet<>(); List<Integer> sortedAlive = new ArrayList<>(alive); Collections.sort(sortedAlive); for (Integer start : sortedAlive) { if (!visited.add(start)) { continue; } Set<Integer> component = new HashSet<>(); ArrayDeque<Integer> bfs = new ArrayDeque<>(); bfs.offer(start); component.add(start); while (!bfs.isEmpty()) { Integer node = bfs.poll(); for (Integer next : undirectedMap.getOrDefault(node, Collections.emptySet())) { if (!alive.contains(next) || !visited.add(next)) { continue; } component.add(next); bfs.offer(next); } } // è³å°3ä¸ªç¹æè®¤ä¸ºæ¯çæ£âåâ if (component.size() >= 3) { result.add(component); } } return result; } private static class GraphContext { private final Map<Integer, Set<Integer>> graph = new HashMap<>(); private final Set<Integer> excludeStationSet = new HashSet<>(); } } src/main/java/com/zy/asrs/task/StationCycleCapacityScheduler.java
New file @@ -0,0 +1,20 @@ package com.zy.asrs.task; import com.zy.asrs.service.StationCycleCapacityService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class StationCycleCapacityScheduler { @Autowired private StationCycleCapacityService stationCycleCapacityService; // // æ¯ç§å·æ°ä¸æ¬¡å¾ªç¯åæ¿è½½é // @Scheduled(cron = "0/1 * * * * ? ") // public void refreshStationCycleCapacity() { // stationCycleCapacityService.refreshSnapshot(); // } } src/main/java/com/zy/core/network/fake/ZyStationV4FakeSegConnect.java
New file @@ -0,0 +1,8 @@ package com.zy.core.network.fake; /** * è¾éç« V4 仿çåæ®µè¿æ¥å®ç°ã * å½åå¤ç¨ V3 åæ®µä»¿çé»è¾ï¼ä¿çç¬ç«ç±»ç¨äºåç» V4 仿ççç¥æ¼è¿ã */ public class ZyStationV4FakeSegConnect extends ZyStationFakeSegConnect { } src/main/java/com/zy/core/network/real/ZyStationV4RealConnect.java
New file @@ -0,0 +1,335 @@ package com.zy.core.network.real; import HslCommunication.Core.Types.OperateResult; import HslCommunication.Core.Types.OperateResultExOne; import HslCommunication.Profinet.Siemens.SiemensPLCS; import HslCommunication.Profinet.Siemens.SiemensS7Net; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.core.common.DateUtils; import com.core.common.SpringUtils; import com.zy.asrs.entity.BasDevp; import com.zy.asrs.entity.DeviceConfig; import com.zy.asrs.service.BasDevpService; import com.zy.common.utils.RedisUtil; import com.zy.core.News; import com.zy.core.cache.OutputQueue; import com.zy.core.model.CommandResponse; import com.zy.core.model.StationObjModel; import com.zy.core.model.command.StationCommand; import com.zy.core.network.api.ZyStationConnectApi; import com.zy.core.network.entity.ZyStationStatusEntity; import lombok.extern.slf4j.Slf4j; import java.text.MessageFormat; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; /** * è¾éç«çå®è¿æ¥ï¼PLCï¼ */ @Slf4j public class ZyStationV4RealConnect implements ZyStationConnectApi { private List<ZyStationStatusEntity> statusList; private List<StationObjModel> barcodeOriginList; private SiemensS7Net siemensNet; private DeviceConfig deviceConfig; private RedisUtil redisUtil; public ZyStationV4RealConnect(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; } @Override public boolean connect() { boolean connected = false; siemensNet = new SiemensS7Net(SiemensPLCS.S1200, deviceConfig.getIp()); OperateResult connect = siemensNet.ConnectServer(); if (connect.IsSuccess) { connected = true; OutputQueue.DEVP.offer(MessageFormat.format("ã{0}ãè¾éç«plcè¿æ¥æå ===>> [id:{1}] [ip:{2}] [port:{3}]", DateUtils.convert(new Date()), deviceConfig.getDeviceNo(), deviceConfig.getIp(), deviceConfig.getPort())); News.info("è¾éç«plcè¿æ¥æå ===>> [id:{}] [ip:{}] [port:{}]", deviceConfig.getDeviceNo(), deviceConfig.getIp(), deviceConfig.getPort()); } else { OutputQueue.DEVP.offer(MessageFormat.format("ã{0}ãè¾éç«plcè¿æ¥å¤±è´¥ï¼ï¼ï¼ ===>> [id:{1}] [ip:{2}] [port:{3}]", DateUtils.convert(new Date()), deviceConfig.getDeviceNo(), deviceConfig.getIp(), deviceConfig.getPort())); News.error("è¾éç«plcè¿æ¥å¤±è´¥ï¼ï¼ï¼ ===>> [id:{}] [ip:{}] [port:{}]", deviceConfig.getDeviceNo(), deviceConfig.getIp(), deviceConfig.getPort()); } // siemensNet.ConnectClose(); return connected; } @Override public boolean disconnect() { siemensNet.ConnectClose(); return true; } @Override public List<ZyStationStatusEntity> getStatus(Integer deviceNo) { if (statusList == null) { BasDevpService basDevpService = SpringUtils.getBean(BasDevpService.class); if (basDevpService == null) { return Collections.emptyList(); } BasDevp basDevp = basDevpService .selectOne(new EntityWrapper<BasDevp>().eq("devp_no", deviceConfig.getDeviceNo())); if (basDevp == null) { return Collections.emptyList(); } statusList = JSONObject.parseArray(basDevp.getStationList(), ZyStationStatusEntity.class); if (statusList != null) { statusList.sort(Comparator.comparing(ZyStationStatusEntity::getStationId)); } barcodeOriginList = basDevp.getBarcodeStationList$(); } if (siemensNet == null) { return statusList; } OperateResultExOne<byte[]> result = siemensNet.Read("DB100.0", (short) (statusList.size() * 10)); if (result.IsSuccess) { for (int i = 0; i < statusList.size(); i++) { ZyStationStatusEntity statusEntity = statusList.get(i); // ç«ç¹ç¼å· statusEntity.setTaskNo(siemensNet.getByteTransform().TransInt32(result.Content, i * 10)); // å·¥ä½å· statusEntity.setTargetStaNo((int) siemensNet.getByteTransform().TransInt16(result.Content, i * 10 + 4)); // ç®æ ç« boolean[] status = siemensNet.getByteTransform().TransBool(result.Content, i * 10 + 6, 1); statusEntity.setAutoing(status[0]); // èªå¨ statusEntity.setLoading(status[1]); // æç© statusEntity.setInEnable(status[2]); // å¯å ¥ statusEntity.setOutEnable(status[3]);// å¯åº statusEntity.setEmptyMk(status[4]); // 空æç statusEntity.setFullPlt(status[5]); // 满æç boolean[] status2 = siemensNet.getByteTransform().TransBool(result.Content, i * 10 + 7, 1); statusEntity.setEnableIn(status2[1]);//å¯å¨å ¥åº Integer palletHeight = null; if (status[7]) { palletHeight = 1;//ä½ } if (status2[0]) { palletHeight = 2;//ä¸ } if (status[6]) { palletHeight = 3;//é« } statusEntity.setPalletHeight(palletHeight);//é«ä½ä¿¡å· statusEntity.setError(0);//é»è®¤æ æ¥è¦ statusEntity.setTaskWriteIdx((int) siemensNet.getByteTransform().TransInt16(result.Content, i * 10 + 8));//ä»»å¡å¯ååº } } // æ¡ç æ«æå¨ OperateResultExOne<byte[]> result2 = siemensNet.Read("DB101.16", (short) (barcodeOriginList.size() * 16)); if (result2.IsSuccess) { for (int i = 0; i < barcodeOriginList.size(); i++) { ZyStationStatusEntity barcodeEntity = findStatusEntityByBarcodeIdx(i + 1); if (barcodeEntity == null) { continue; } String barcode = siemensNet.getByteTransform().TransString(result2.Content, i * 16 + 2, 14, "UTF-8"); barcode = barcode.trim(); barcodeEntity.setBarcode(barcode); } } // ç§°é OperateResultExOne<byte[]> result3 = siemensNet.Read("DB102.4", (short) (barcodeOriginList.size() * 4)); if (result3.IsSuccess) { for (int i = 0; i < barcodeOriginList.size(); i++) { ZyStationStatusEntity barcodeEntity = findStatusEntityByBarcodeIdx(i + 1); if (barcodeEntity == null) { continue; } double weight = (double) siemensNet.getByteTransform().TransSingle(result3.Content, i * 4); barcodeEntity.setWeight(weight); } } // æ¥è¦ä¿¡æ¯ OperateResultExOne<byte[]> result4 = siemensNet.Read("DB103.2", (short) (barcodeOriginList.size() * 2)); if (result4.IsSuccess) { for (int i = 0; i < barcodeOriginList.size(); i++) { ZyStationStatusEntity barcodeEntity = findStatusEntityByBarcodeIdx(i + 1); if (barcodeEntity == null) { continue; } StringBuilder sb = new StringBuilder(); boolean[] status1 = siemensNet.getByteTransform().TransBool(result4.Content, i * 2, 1); boolean[] status2 = siemensNet.getByteTransform().TransBool(result4.Content, i * 2 + 1, 1); if(status1[0]){ sb.append("å·¦è¶ å®½æ¥è¦;"); } if(status1[1]) { sb.append("å³è¶ 宽æ¥è¦;"); } if(status1[2]) { sb.append("åè¶ é¿æ¥è¦;"); } if(status1[3]) { sb.append("åè¶ é¿æ¥è¦;"); } if(status1[4]) { sb.append("è¶ é«æ¥è¦;"); } if(status1[5]) { sb.append("æè´§æ¥è¦ï¼ç©ºæå ¥åºæ¶æ£æµæç䏿æ è´§ç©;"); } if(status1[6]) { sb.append("ééå¼å¸¸æ¥è¦;"); } if(status1[7]) { sb.append("æ«ç å¼å¸¸;"); } if(sb.length() > 0) { barcodeEntity.setError(1); }else { barcodeEntity.setError(0); } barcodeEntity.setErrorMsg(sb.toString()); } } return statusList; } @Override public CommandResponse sendCommand(Integer deviceNo, StationCommand command) { CommandResponse commandResponse = new CommandResponse(false); if (null == command) { commandResponse.setMessage("å½ä»¤ä¸ºç©º"); return commandResponse; } int taskWriteIdx = getTaskWriteIdx(command.getStationId()); if (taskWriteIdx == -1) { commandResponse.setMessage("å½ä»¤ä¸åè¶ æ¶ï¼æ æ³æ¾å°å¯ç¨ä¸ååºå"); return commandResponse; } int stationIdx = findIndex(command.getStationId()); short[] data = new short[2]; data[0] = (short) 0; data[1] = command.getTargetStaNo().shortValue(); OperateResult writeTaskNo = siemensNet.Write("DB13." + (stationIdx * 48 + (taskWriteIdx * 12)), command.getTaskNo()); if (!writeTaskNo.IsSuccess) { log.error("åå ¥è¾é线å½ä»¤å¤±è´¥ãç«ç¹ç¼å·={}ï¼ç«ç¹æ°æ®={}", command.getTaskNo(), JSON.toJSON(command)); commandResponse.setResult(false); commandResponse.setMessage("å½ä»¤ä¸å失败ï¼åå ¥å·¥ä½å·å¤±è´¥"); return commandResponse; } OperateResult writeData = siemensNet.Write("DB13." + (stationIdx * 48 + (taskWriteIdx * 12 + 4)), data); if (!writeData.IsSuccess) { log.error("åå ¥è¾é线å½ä»¤å¤±è´¥ãç«ç¹ç¼å·={}ï¼ç«ç¹æ°æ®={}", command.getTaskNo(), JSON.toJSON(command)); commandResponse.setResult(false); commandResponse.setMessage("å½ä»¤ä¸å失败ï¼åå ¥æ°æ®åºå失败"); return commandResponse; } log.info("åå ¥è¾é线å½ä»¤æåãä»»å¡å·={}ï¼ç«ç¹æ°æ®={}", command.getTaskNo(), JSON.toJSON(command)); commandResponse.setResult(true); return commandResponse; } @Override public synchronized CommandResponse sendOriginCommand(String address, short[] data) { CommandResponse commandResponse = new CommandResponse(false); if (null == data || data.length == 0) { commandResponse.setMessage("æ°æ®ä¸ºç©º"); return commandResponse; } OperateResult write = siemensNet.Write(address, data); if (write.IsSuccess) { log.info("åå ¥åå§å½ä»¤æåãå°å={}ï¼æ°æ®={}", address, JSON.toJSON(data)); commandResponse.setResult(true); } else { log.error("åå ¥åå§å½ä»¤å¤±è´¥ãå°å={}ï¼æ°æ®={}", address, JSON.toJSON(data)); commandResponse.setResult(false); } return commandResponse; } @Override public byte[] readOriginCommand(String address, int length) { OperateResultExOne<byte[]> result = siemensNet.Read(address, (short) length); if (result.IsSuccess) { return result.Content; } return new byte[0]; } private ZyStationStatusEntity findStatusEntityByBarcodeIdx(Integer barcodeIdx) { Integer stationId = null; for (StationObjModel stationObjModel : barcodeOriginList) { if (stationObjModel.getBarcodeIdx().equals(barcodeIdx)) { stationId = stationObjModel.getStationId(); break; } } for (ZyStationStatusEntity zyStationStatusEntity : statusList) { if(zyStationStatusEntity.getStationId().equals(stationId)) { return zyStationStatusEntity; } } return null; } private int getTaskWriteIdx(int stationId) { int useIdx = -1; int stationIdx = findIndex(stationId); if (stationIdx != -1) { ZyStationStatusEntity statusEntity = statusList.get(stationIdx); Integer taskWriteIdx = statusEntity.getTaskWriteIdx(); if (taskWriteIdx > 0) { OperateResultExOne<byte[]> resultTask = siemensNet.Read("DB13." + (stationId * 48), (short) 48); if (resultTask.IsSuccess) { int taskNo = siemensNet.getByteTransform().TransInt32(resultTask.Content, taskWriteIdx * 12); int startPoint = siemensNet.getByteTransform().TransInt16(resultTask.Content, taskWriteIdx * 12 + 4); int targetPoint = siemensNet.getByteTransform().TransInt16(resultTask.Content, taskWriteIdx * 12 + 6); if (taskNo == 0 && startPoint == 0 && targetPoint == 0) { useIdx = taskWriteIdx; } } } } return useIdx; } private int findIndex(Integer stationId) { for (int i = 0; i < statusList.size(); i++) { ZyStationStatusEntity statusEntity = statusList.get(i); if (statusEntity.getStationId().equals(stationId)) { return i; } } return -1; } } src/main/java/com/zy/core/thread/impl/ZyStationV4Thread.java
New file @@ -0,0 +1,478 @@ package com.zy.core.thread.impl; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.core.common.Cools; import com.core.common.DateUtils; import com.core.common.SpringUtils; import com.zy.asrs.entity.BasDevp; import com.zy.asrs.entity.BasStationOpt; import com.zy.asrs.entity.DeviceConfig; import com.zy.asrs.entity.DeviceDataLog; import com.zy.asrs.service.BasDevpService; import com.zy.asrs.service.BasStationOptService; import com.zy.asrs.utils.Utils; import com.zy.common.model.NavigateNode; import com.zy.common.utils.NavigateUtils; import com.zy.common.utils.RedisUtil; import com.zy.core.cache.MessageQueue; import com.zy.core.cache.OutputQueue; import com.zy.core.cache.SlaveConnection; import com.zy.core.enums.RedisKeyType; import com.zy.core.enums.SlaveType; import com.zy.core.enums.StationCommandType; import com.zy.core.model.CommandResponse; import com.zy.core.model.Task; import com.zy.core.model.command.StationCommand; import com.zy.core.model.protocol.StationProtocol; import com.zy.core.network.DeviceConnectPool; import com.zy.core.network.ZyStationConnectDriver; import com.zy.core.network.entity.ZyStationStatusEntity; import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.text.MessageFormat; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Data @Slf4j public class ZyStationV4Thread implements Runnable, com.zy.core.thread.StationThread { private List<StationProtocol> statusList = new ArrayList<>(); private DeviceConfig deviceConfig; private RedisUtil redisUtil; private ZyStationConnectDriver zyStationConnectDriver; private int deviceLogCollectTime = 200; private long deviceDataLogTime = System.currentTimeMillis(); private ExecutorService executor = Executors.newFixedThreadPool(9999); public ZyStationV4Thread(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; } @Override @SuppressWarnings("InfiniteLoopStatement") public void run() { this.connect(); deviceLogCollectTime = Utils.getDeviceLogCollectTime(); Thread readThread = new Thread(() -> { while (true) { try { deviceLogCollectTime = Utils.getDeviceLogCollectTime(); readStatus(); Thread.sleep(100); } catch (Exception e) { log.error("StationV4Thread Fail", e); } } }); readThread.start(); Thread processThread = new Thread(() -> { while (true) { try { int step = 1; Task task = MessageQueue.poll(SlaveType.Devp, deviceConfig.getDeviceNo()); if (task != null) { step = task.getStep(); } if (step == 2) { StationCommand cmd = (StationCommand) task.getData(); executor.submit(() -> executeMoveWithSeg(cmd)); } Thread.sleep(100); } catch (Exception e) { log.error("StationV4Process Fail", e); } } }); processThread.start(); } private void readStatus() { if (zyStationConnectDriver == null) { return; } if (statusList.isEmpty()) { BasDevpService basDevpService = null; try { basDevpService = SpringUtils.getBean(BasDevpService.class); } catch (Exception e) { } if (basDevpService == null) { return; } BasDevp basDevp = basDevpService .selectOne(new EntityWrapper<BasDevp>().eq("devp_no", deviceConfig.getDeviceNo())); if (basDevp == null) { return; } List<ZyStationStatusEntity> list = JSONObject.parseArray(basDevp.getStationList(), ZyStationStatusEntity.class); for (ZyStationStatusEntity entity : list) { StationProtocol stationProtocol = new StationProtocol(); stationProtocol.setStationId(entity.getStationId()); statusList.add(stationProtocol); } } List<ZyStationStatusEntity> zyStationStatusEntities = zyStationConnectDriver.getStatus(); for (ZyStationStatusEntity statusEntity : zyStationStatusEntities) { for (StationProtocol stationProtocol : statusList) { if (stationProtocol.getStationId().equals(statusEntity.getStationId())) { stationProtocol.setTaskNo(statusEntity.getTaskNo()); stationProtocol.setTargetStaNo(statusEntity.getTargetStaNo()); stationProtocol.setAutoing(statusEntity.isAutoing()); stationProtocol.setLoading(statusEntity.isLoading()); stationProtocol.setInEnable(statusEntity.isInEnable()); stationProtocol.setOutEnable(statusEntity.isOutEnable()); stationProtocol.setEmptyMk(statusEntity.isEmptyMk()); stationProtocol.setFullPlt(statusEntity.isFullPlt()); stationProtocol.setPalletHeight(statusEntity.getPalletHeight()); stationProtocol.setError(statusEntity.getError()); stationProtocol.setErrorMsg(statusEntity.getErrorMsg()); stationProtocol.setBarcode(statusEntity.getBarcode()); stationProtocol.setRunBlock(statusEntity.isRunBlock()); stationProtocol.setEnableIn(statusEntity.isEnableIn()); stationProtocol.setWeight(statusEntity.getWeight()); stationProtocol.setTaskWriteIdx(statusEntity.getTaskWriteIdx()); } if (!Cools.isEmpty(stationProtocol.getSystemWarning())) { if (stationProtocol.isAutoing() && !stationProtocol.isLoading() ) { stationProtocol.setSystemWarning(""); } } } } OutputQueue.DEVP.offer(MessageFormat.format("ã{0}ã[id:{1}] <<<<< 宿¶æ°æ®æ´æ°æå", DateUtils.convert(new Date()), deviceConfig.getDeviceNo())); if (System.currentTimeMillis() - deviceDataLogTime > deviceLogCollectTime) { DeviceDataLog deviceDataLog = new DeviceDataLog(); deviceDataLog.setOriginData(JSON.toJSONString(zyStationStatusEntities)); deviceDataLog.setWcsData(JSON.toJSONString(statusList)); deviceDataLog.setType(String.valueOf(SlaveType.Devp)); deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo()); deviceDataLog.setCreateTime(new Date()); redisUtil.set(RedisKeyType.DEVICE_LOG_KEY.key + System.currentTimeMillis(), deviceDataLog, 60 * 60 * 24); deviceDataLogTime = System.currentTimeMillis(); } } @Override public boolean connect() { zyStationConnectDriver = new ZyStationConnectDriver(deviceConfig, redisUtil); zyStationConnectDriver.start(); DeviceConnectPool.put(SlaveType.Devp, deviceConfig.getDeviceNo(), zyStationConnectDriver); return true; } @Override public void close() { if (zyStationConnectDriver != null) { zyStationConnectDriver.close(); } if (executor != null) { try { executor.shutdownNow(); } catch (Exception ignore) {} } } @Override public List<StationProtocol> getStatus() { return statusList; } @Override public Map<Integer, StationProtocol> getStatusMap() { Map<Integer, StationProtocol> map = new HashMap<>(); for (StationProtocol stationProtocol : statusList) { map.put(stationProtocol.getStationId(), stationProtocol); } return map; } @Override public StationCommand getCommand(StationCommandType commandType, Integer taskNo, Integer stationId, Integer targetStationId, Integer palletSize) { StationCommand stationCommand = new StationCommand(); stationCommand.setTaskNo(taskNo); stationCommand.setStationId(stationId); stationCommand.setTargetStaNo(targetStationId); stationCommand.setPalletSize(palletSize); stationCommand.setCommandType(commandType); if (commandType == StationCommandType.MOVE) { if (!stationId.equals(targetStationId)) { List<NavigateNode> nodes = calcPathNavigateNodes(stationId, targetStationId); List<Integer> path = new ArrayList<>(); List<Integer> liftTransferPath = new ArrayList<>(); for (NavigateNode n : nodes) { JSONObject v = JSONObject.parseObject(n.getNodeValue()); if (v == null) { continue; } Integer stationNo = v.getInteger("stationId"); if (stationNo == null) { continue; } path.add(stationNo); if (Boolean.TRUE.equals(n.getIsLiftTransferPoint())) { liftTransferPath.add(stationNo); } } stationCommand.setNavigatePath(path); stationCommand.setLiftTransferPath(liftTransferPath); } } return stationCommand; } @Override public CommandResponse sendCommand(StationCommand command) { CommandResponse commandResponse = null; try { commandResponse = zyStationConnectDriver.sendCommand(command); } catch (Exception e) { e.printStackTrace(); } finally { BasStationOptService optService = SpringUtils.getBean(BasStationOptService.class); List<ZyStationStatusEntity> statusListEntity = zyStationConnectDriver.getStatus(); ZyStationStatusEntity matched = null; if (statusListEntity != null) { for (ZyStationStatusEntity e : statusListEntity) { if (e.getStationId() != null && e.getStationId().equals(command.getStationId())) { matched = e; break; } } } BasStationOpt basStationOpt = new BasStationOpt( command.getTaskNo(), command.getStationId(), new Date(), String.valueOf(command.getCommandType()), command.getStationId(), command.getTargetStaNo(), null, null, null, JSON.toJSONString(command), JSON.toJSONString(matched), 1, JSON.toJSONString(commandResponse) ); if (optService != null) { optService.insert(basStationOpt); } } return commandResponse; } @Override public CommandResponse sendOriginCommand(String address, short[] data) { return zyStationConnectDriver.sendOriginCommand(address, data); } @Override public byte[] readOriginCommand(String address, int length) { return zyStationConnectDriver.readOriginCommand(address, length); } private List<NavigateNode> calcPathNavigateNodes(Integer startStationId, Integer targetStationId) { NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class); if (navigateUtils == null) { return new ArrayList<>(); } return navigateUtils.calcByStationId(startStationId, targetStationId); } private void executeMoveWithSeg(StationCommand original) { if(original.getCommandType() == StationCommandType.MOVE){ List<Integer> path = JSON.parseArray(JSON.toJSONString(original.getNavigatePath(), SerializerFeature.DisableCircularReferenceDetect), Integer.class); List<Integer> liftTransferPath = JSON.parseArray(JSON.toJSONString(original.getLiftTransferPath(), SerializerFeature.DisableCircularReferenceDetect), Integer.class); if (path == null || path.isEmpty()) { // åç«ç¹ä»»å¡ä¸ä¼çæè·¯å¾ï¼ä½ä»éä¸åå½ä»¤åå ¥ä»»å¡æ°æ® if (Objects.equals(original.getStationId(), original.getTargetStaNo())) { while (true) { CommandResponse commandResponse = sendCommand(original); if (commandResponse != null && commandResponse.getResult()) { break; } try { Thread.sleep(200); } catch (Exception ignore) {} } } return; } int total = path.size(); List<Integer> segmentEndIndices = new ArrayList<>(); if (liftTransferPath != null) { for (Integer liftTransferStationId : liftTransferPath) { int endIndex = path.indexOf(liftTransferStationId); // é¿å 以起ç¹ä½ä¸ºåç¹å¯¼è´ç©ºå段 if (endIndex <= 0) { continue; } if (segmentEndIndices.isEmpty() || endIndex > segmentEndIndices.get(segmentEndIndices.size() - 1)) { segmentEndIndices.add(endIndex); } } } if (segmentEndIndices.isEmpty() || segmentEndIndices.get(segmentEndIndices.size() - 1) != total - 1) { segmentEndIndices.add(total - 1); } List<StationCommand> segmentCommands = new ArrayList<>(); int buildStartIdx = 0; for (Integer endIdx : segmentEndIndices) { if (endIdx == null || endIdx < buildStartIdx) { continue; } List<Integer> segmentPath = new ArrayList<>(path.subList(buildStartIdx, endIdx + 1)); if (segmentPath.isEmpty()) { buildStartIdx = endIdx + 1; continue; } StationCommand segmentCommand = new StationCommand(); segmentCommand.setTaskNo(original.getTaskNo()); segmentCommand.setCommandType(original.getCommandType()); segmentCommand.setPalletSize(original.getPalletSize()); segmentCommand.setBarcode(original.getBarcode()); segmentCommand.setOriginalNavigatePath(path); segmentCommand.setNavigatePath(segmentPath); // æ¯æ®µå½ä»¤ï¼èµ·ç¹=å½å段é¦ç«ç¹ï¼ç»ç¹=å½å段æ«ç«ç¹ segmentCommand.setStationId(segmentPath.get(0)); segmentCommand.setTargetStaNo(segmentPath.get(segmentPath.size() - 1)); segmentCommands.add(segmentCommand); // åæ®µè¾¹çç¹éè¦åæ¶ä½ä¸ºä¸ä¸æ®µçèµ·ç¹ï¼ä¾å¦ [221,220,219] + [219,213,212]ï¼ buildStartIdx = endIdx; } if (segmentCommands.isEmpty()) { return; } int segCursor = 0; while (true) { CommandResponse commandResponse = sendCommand(segmentCommands.get(segCursor)); if (commandResponse == null) { try { Thread.sleep(200); } catch (Exception ignore) {} continue; } if (commandResponse.getResult()) { break; } try { Thread.sleep(200); } catch (Exception ignore) {} } long runTime = System.currentTimeMillis(); boolean firstRun = true; while (true) { try { Object cancel = redisUtil.get(RedisKeyType.DEVICE_STATION_MOVE_RESET.key + original.getTaskNo()); if (cancel != null) { break;//æ¶å°ä¸æä¿¡å· } StationProtocol currentStation = findCurrentStationByTask(original.getTaskNo()); if (currentStation == null) { if(System.currentTimeMillis() - runTime > 1000 * 60){ break; } Thread.sleep(500); continue; } runTime = System.currentTimeMillis(); if (!firstRun && currentStation.isRunBlock()) { break; } int currentIndex = path.indexOf(currentStation.getStationId()); if (currentIndex < 0) { Thread.sleep(500); continue; } int remaining = total - currentIndex - 1; if (remaining <= 0) { break; } int currentSegEndIndex = segmentEndIndices.get(segCursor); int currentSegStartIndex = segCursor == 0 ? 0 : segmentEndIndices.get(segCursor - 1); int segLen = currentSegEndIndex - currentSegStartIndex + 1; int remainingSegment = Math.max(0, currentSegEndIndex - currentIndex); int thresholdSegment = (int) Math.ceil(segLen * 0.3); if (remainingSegment <= thresholdSegment && segCursor < segmentCommands.size() - 1) { segCursor++; while (true) { CommandResponse commandResponse = sendCommand(segmentCommands.get(segCursor)); if (commandResponse == null) { Thread.sleep(200); continue; } if (commandResponse.getResult()) { break; } Thread.sleep(200); } } Thread.sleep(500); } catch (Exception e) { break; } firstRun = false; } }else { sendCommand(original); } } private StationProtocol findCurrentStationByTask(Integer taskNo) { try { com.zy.asrs.service.DeviceConfigService deviceConfigService = SpringUtils.getBean(com.zy.asrs.service.DeviceConfigService.class); if (deviceConfigService == null) { return null; } List<DeviceConfig> devpList = deviceConfigService.selectList(new EntityWrapper<DeviceConfig>() .eq("device_type", String.valueOf(SlaveType.Devp))); for (DeviceConfig dc : devpList) { com.zy.core.thread.StationThread t = (com.zy.core.thread.StationThread) SlaveConnection.get(SlaveType.Devp, dc.getDeviceNo()); if (t == null) { continue; } Map<Integer, StationProtocol> m = t.getStatusMap(); if (m == null || m.isEmpty()) { continue; } for (StationProtocol sp : m.values()) { if (sp.getTaskNo() != null && sp.getTaskNo().equals(taskNo) && sp.isLoading()) { return sp; } } } } catch (Exception e) { return null; } return null; } } src/main/java/com/zy/system/entity/license/LicenseUtils.java
New file @@ -0,0 +1,26 @@ package com.zy.system.entity.license; public class LicenseUtils { /** * è·åå½åæå¡å¨éè¦é¢å¤æ ¡éªçLicenseåæ° */ public static LicenseCheck getServerInfos(){ //æä½ç³»ç»ç±»å String osName = System.getProperty("os.name").toLowerCase(); osName = osName.toLowerCase(); AbstractServerInfos abstractServerInfos = null; //æ ¹æ®ä¸åæä½ç³»ç»ç±»åéæ©ä¸åçæ°æ®è·åæ¹æ³ if (osName.startsWith("windows")) { abstractServerInfos = new WindowsServerInfos(); } else if (osName.startsWith("linux")) { abstractServerInfos = new LinuxServerInfos(); }else{//å ¶ä»æå¡å¨ç±»å abstractServerInfos = new LinuxServerInfos(); } return abstractServerInfos.getServerInfos(); } } src/main/resources/docs/~$SÍⲿHTTP API½Ó¿ÚV1.7.docxBinary files differ
src/main/resources/map/~$СËɵØÍ¼.xlsxBinary files differ
src/main/resources/sql/20260109084743.nb3Binary files differ
src/main/webapp/views/ai/llm_config.html
New file @@ -0,0 +1,1014 @@ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>AIé ç½®</title> <link rel="stylesheet" href="../../static/vue/element/element.css" /> <style> body { margin: 0; font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; background: radial-gradient(1200px 500px at 10% -10%, rgba(26, 115, 232, 0.14), transparent 50%), radial-gradient(900px 450px at 100% 0%, rgba(38, 166, 154, 0.11), transparent 55%), #f4f7fb; } .container { max-width: 1640px; margin: 16px auto; padding: 0 14px; } .hero { background: linear-gradient(135deg, #0f4c81 0%, #1f6fb2 45%, #2aa198 100%); color: #fff; border-radius: 14px; padding: 14px 16px; margin-bottom: 10px; box-shadow: 0 10px 28px rgba(23, 70, 110, 0.22); } .hero-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .hero-title { display: flex; align-items: center; gap: 10px; } .hero-title .main { font-size: 16px; font-weight: 700; letter-spacing: 0.2px; } .hero-title .sub { font-size: 12px; opacity: 0.9; } .hero-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } .summary-grid { margin-top: 10px; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; } .summary-card { border-radius: 10px; background: rgba(255, 255, 255, 0.16); border: 1px solid rgba(255, 255, 255, 0.24); padding: 8px 10px; min-height: 56px; backdrop-filter: blur(3px); } .summary-card .k { font-size: 11px; opacity: 0.88; } .summary-card .v { margin-top: 4px; font-size: 22px; font-weight: 700; line-height: 1.1; } .route-board { border-radius: 14px; border: 1px solid #dbe5f2; background: radial-gradient(800px 200px at -10% 0, rgba(52, 119, 201, 0.06), transparent 55%), radial-gradient(700px 220px at 110% 20%, rgba(39, 154, 136, 0.08), transparent 58%), #f9fbff; box-shadow: 0 8px 30px rgba(26, 53, 84, 0.10); padding: 12px; min-height: 64vh; } .route-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(390px, 1fr)); gap: 12px; } .route-card { border-radius: 14px; border: 1px solid #e4ebf5; background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%); box-shadow: 0 10px 24px rgba(14, 38, 68, 0.08); padding: 12px; transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; animation: card-in 0.24s ease both; } .route-card:hover { transform: translateY(-2px); box-shadow: 0 14px 26px rgba(14, 38, 68, 0.12); border-color: #d4e2f2; } .route-card.cooling { border-color: #f2d8a2; background: linear-gradient(180deg, #fffdf6 0%, #fffaf0 100%); } .route-card.disabled { opacity: 0.84; } .route-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-bottom: 10px; } .route-title { display: flex; flex-direction: column; gap: 5px; min-width: 0; flex: 1; } .route-id-line { color: #8294aa; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .route-state { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; justify-content: flex-end; max-width: 46%; } .route-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .field-full { grid-column: 1 / -1; } .field-label { font-size: 11px; color: #6f8094; margin-bottom: 4px; } .switch-line { margin-top: 10px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } .switch-item { border: 1px solid #e7edf7; border-radius: 10px; padding: 6px 8px; background: #fff; display: flex; align-items: center; justify-content: space-between; font-size: 12px; color: #2f3f53; } .stats-box { margin-top: 10px; border: 1px solid #e8edf6; border-radius: 10px; background: linear-gradient(180deg, #fcfdff 0%, #f7faff 100%); padding: 8px 10px; font-size: 12px; color: #4c5f76; line-height: 1.6; } .stats-box .light { color: #7f91a8; } .route-actions { margin-top: 10px; display: flex; align-items: center; justify-content: space-between; gap: 8px; } .action-left, .action-right { display: flex; align-items: center; gap: 8px; } .empty-shell { min-height: 48vh; border-radius: 12px; border: 1px dashed #cfd8e5; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #7d8ea4; gap: 8px; background: rgba(255, 255, 255, 0.55); } .log-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; } .log-text { max-width: 360px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #6c7f95; font-size: 12px; } .log-detail-body { max-height: 62vh; overflow: auto; border: 1px solid #dfe8f3; border-radius: 8px; background: #f8fbff; padding: 10px 12px; white-space: pre-wrap; word-break: break-word; line-height: 1.55; color: #2e3c4f; font-size: 12px; font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace; } @keyframes card-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .mono { font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 12px; } @media (max-width: 1280px) { .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .route-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); } .route-fields { grid-template-columns: 1fr; } .switch-line { grid-template-columns: 1fr; } } </style> </head> <body> <div id="app" class="container"> <div class="hero"> <div class="hero-top"> <div class="hero-title"> <div v-html="headerIcon" style="display:flex;"></div> <div> <div class="main">AIé ç½® - LLMè·¯ç±</div> <div class="sub">æ¯æå¤APIã夿¨¡åãå¤Keyï¼é¢åº¦èå°½ææ éèªå¨åæ¢</div> </div> </div> <div class="hero-actions"> <el-button type="primary" size="mini" @click="addRoute">æ°å¢è·¯ç±</el-button> <el-button size="mini" @click="exportRoutes">导åºJSON</el-button> <el-button size="mini" @click="triggerImport">å¯¼å ¥JSON</el-button> <el-button size="mini" @click="loadRoutes">å·æ°</el-button> <el-button size="mini" @click="openLogDialog">è°ç¨æ¥å¿</el-button> </div> </div> <div class="summary-grid"> <div class="summary-card"> <div class="k">æ»è·¯ç±</div> <div class="v">{{ summary.total }}</div> </div> <div class="summary-card"> <div class="k">å¯ç¨</div> <div class="v">{{ summary.enabled }}</div> </div> <div class="summary-card"> <div class="k">æ é忢å¼å¯</div> <div class="v">{{ summary.errorSwitch }}</div> </div> <div class="summary-card"> <div class="k">é¢åº¦åæ¢å¼å¯</div> <div class="v">{{ summary.quotaSwitch }}</div> </div> <div class="summary-card"> <div class="k">å·å´ä¸</div> <div class="v">{{ summary.cooling }}</div> </div> </div> </div> <input ref="importFileInput" type="file" accept="application/json,.json" style="display:none;" @change="handleImportFileChange" /> <div class="route-board" v-loading="loading"> <div v-if="!routes || routes.length === 0" class="empty-shell"> <div style="font-size:14px;font-weight:600;">ææ è·¯ç±é ç½®</div> <div style="font-size:12px;">ç¹å»å³ä¸è§âæ°å¢è·¯ç±âåå»ºç¬¬ä¸æ¡é ç½®</div> </div> <div v-else class="route-grid"> <div class="route-card" :class="routeCardClass(route)" v-for="(route, idx) in routes" :key="route.id ? ('route_' + route.id) : ('new_' + idx)"> <div class="route-head"> <div class="route-title"> <el-input v-model="route.name" size="mini" placeholder="è·¯ç±åç§°"></el-input> <div class="route-id-line">#{{ route.id || 'new' }} · ä¼å 级 {{ route.priority || 0 }}</div> </div> <div class="route-state"> <el-tag size="mini" :type="route.status === 1 ? 'success' : 'info'">{{ route.status === 1 ? 'å¯ç¨' : 'ç¦ç¨' }}</el-tag> <el-tag size="mini" type="warning" v-if="isRouteCooling(route)">å·å´ä¸</el-tag> </div> </div> <div class="route-fields"> <div class="field-full"> <div class="field-label">Base URL</div> <el-input v-model="route.baseUrl" class="mono" size="mini" placeholder="å¿ å¡«ï¼ä¾å¦: https://dashscope.aliyuncs.com/compatible-mode/v1"></el-input> </div> <div> <div class="field-label">模å</div> <el-input v-model="route.model" class="mono" size="mini" placeholder="å¿ å¡«"></el-input> </div> <div> <div class="field-label">ä¼å 级ï¼è¶å°è¶ä¼å ï¼</div> <el-input-number v-model="route.priority" size="mini" :min="0" :max="99999" :controls="false" style="width:100%;"></el-input-number> </div> <div class="field-full"> <div class="field-label">API Key</div> <el-input v-model="route.apiKey" class="mono" type="password" size="mini" placeholder="å¿ å¡«"> <template slot="append"> <el-button type="text" style="padding:0 8px;" @click="copyApiKey(route)">å¤å¶</el-button> </template> </el-input> </div> <div> <div class="field-label">å·å´ç§æ°</div> <el-input-number v-model="route.cooldownSeconds" size="mini" :min="0" :max="86400" :controls="false" style="width:100%;"></el-input-number> </div> </div> <div class="switch-line"> <div class="switch-item"> <span>ç¶æ</span> <el-switch v-model="route.status" :active-value="1" :inactive-value="0"></el-switch> </div> <div class="switch-item"> <span>æè</span> <el-switch v-model="route.thinking" :active-value="1" :inactive-value="0"></el-switch> </div> <div class="switch-item"> <span>é¢åº¦åæ¢</span> <el-switch v-model="route.switchOnQuota" :active-value="1" :inactive-value="0"></el-switch> </div> <div class="switch-item"> <span>æ é忢</span> <el-switch v-model="route.switchOnError" :active-value="1" :inactive-value="0"></el-switch> </div> </div> <div class="stats-box"> <div>æå {{ route.successCount || 0 }} / 失败 {{ route.failCount || 0 }} / è¿ç»å¤±è´¥ {{ route.consecutiveFailCount || 0 }}</div> <div class="light">å·å´å°: {{ formatDateTime(route.cooldownUntil) }}</div> <div class="light">æè¿é误: {{ route.lastError || '-' }}</div> </div> <div class="route-actions"> <div class="action-left"> <el-button type="primary" size="mini" @click="saveRoute(route)">ä¿å</el-button> <el-button size="mini" :loading="route.__testing === true" @click="testRoute(route)"> {{ route.__testing === true ? 'æµè¯ä¸...' : 'æµè¯' }} </el-button> </div> <div class="action-right"> <el-dropdown trigger="click" @command="function(cmd){ handleRouteCommand(cmd, route, idx); }"> <el-button size="mini" plain> æ´å¤<i class="el-icon-arrow-down el-icon--right"></i> </el-button> <el-dropdown-menu slot="dropdown"> <el-dropdown-item command="cooldown" :disabled="!route.id">æ¸ å·å´</el-dropdown-item> <el-dropdown-item command="delete" divided>å é¤</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </div> </div> </div> </div> <el-dialog title="LLMè°ç¨æ¥å¿" :visible.sync="logDialogVisible" width="88%" :close-on-click-modal="false"> <div class="log-toolbar"> <el-select v-model="logQuery.scene" size="mini" clearable placeholder="åºæ¯" style="width:180px;"> <el-option label="chat" value="chat"></el-option> <el-option label="chat_completion" value="chat_completion"></el-option> <el-option label="chat_completion_tools" value="chat_completion_tools"></el-option> <el-option label="chat_stream" value="chat_stream"></el-option> <el-option label="chat_stream_tools" value="chat_stream_tools"></el-option> </el-select> <el-select v-model="logQuery.success" size="mini" clearable placeholder="ç»æ" style="width:120px;"> <el-option label="æå" :value="1"></el-option> <el-option label="失败" :value="0"></el-option> </el-select> <el-input v-model="logQuery.traceId" size="mini" placeholder="traceId" style="width:260px;"></el-input> <el-button type="primary" size="mini" @click="loadLogs(1)">æ¥è¯¢</el-button> <el-button size="mini" @click="resetLogQuery">éç½®</el-button> <el-button type="danger" plain size="mini" @click="clearLogs">æ¸ ç©ºæ¥å¿</el-button> </div> <el-table :data="logPage.records" border stripe height="56vh" v-loading="logLoading" :header-cell-style="{background:'#f7f9fc', color:'#2e3a4d', fontWeight:600}"> <el-table-column label="æ¶é´" width="165"> <template slot-scope="scope"> {{ formatDateTime(scope.row.createTime) }} </template> </el-table-column> <el-table-column prop="scene" label="åºæ¯" width="165"></el-table-column> <el-table-column prop="attemptNo" label="å°è¯" width="70"></el-table-column> <el-table-column prop="routeName" label="è·¯ç±" width="170"></el-table-column> <el-table-column prop="model" label="模å" width="150"></el-table-column> <el-table-column label="ç»æ" width="85"> <template slot-scope="scope"> <el-tag size="mini" :type="scope.row.success === 1 ? 'success' : 'danger'"> {{ scope.row.success === 1 ? 'æå' : '失败' }} </el-tag> </template> </el-table-column> <el-table-column prop="httpStatus" label="ç¶æç " width="90"></el-table-column> <el-table-column prop="latencyMs" label="èæ¶(ms)" width="95"></el-table-column> <el-table-column prop="traceId" label="TraceId" width="230"></el-table-column> <el-table-column label="é误" min-width="220"> <template slot-scope="scope"> <div class="log-text">{{ scope.row.errorMessage || '-' }}</div> </template> </el-table-column> <el-table-column label="æä½" width="120" fixed="right"> <template slot-scope="scope"> <el-button type="text" size="mini" @click="showLogDetail(scope.row)">详æ </el-button> <el-button type="text" size="mini" style="color:#F56C6C;" @click="deleteLog(scope.row)">å é¤</el-button> </template> </el-table-column> </el-table> <div style="margin-top:10px;text-align:right;"> <el-pagination background layout="total, prev, pager, next" :current-page="logPage.curr" :page-size="logPage.limit" :total="logPage.total" @current-change="loadLogs"> </el-pagination> </div> </el-dialog> <el-dialog :title="logDetailTitle || 'æ¥å¿è¯¦æ '" :visible.sync="logDetailVisible" width="82%" :close-on-click-modal="false" append-to-body> <div class="log-detail-body">{{ logDetailText || '-' }}</div> <span slot="footer" class="dialog-footer"> <el-button size="mini" @click="copyText(logDetailText)">å¤å¶å ¨æ</el-button> <el-button type="primary" size="mini" @click="logDetailVisible = false">å ³é</el-button> </span> </el-dialog> </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> new Vue({ el: '#app', data: function() { return { headerIcon: getAiIconHtml(34, 34), loading: false, routes: [], logDialogVisible: false, logLoading: false, logDetailVisible: false, logDetailTitle: '', logDetailText: '', logQuery: { scene: '', success: '', traceId: '' }, logPage: { records: [], curr: 1, limit: 20, total: 0 } }; }, computed: { summary: function() { var now = Date.now(); var total = this.routes.length; var enabled = 0, quotaSwitch = 0, errorSwitch = 0, cooling = 0; for (var i = 0; i < this.routes.length; i++) { var x = this.routes[i]; if (x.status === 1) enabled++; if (x.switchOnQuota === 1) quotaSwitch++; if (x.switchOnError === 1) errorSwitch++; if (x.cooldownUntil && new Date(x.cooldownUntil).getTime() > now) cooling++; } return { total: total, enabled: enabled, quotaSwitch: quotaSwitch, errorSwitch: errorSwitch, cooling: cooling }; } }, methods: { formatDateTime: function(input) { if (!input) return '-'; var d = input instanceof Date ? input : new Date(input); if (isNaN(d.getTime())) return String(input); var pad = function(n) { return n < 10 ? ('0' + n) : String(n); }; var y = d.getFullYear(); var m = pad(d.getMonth() + 1); var day = pad(d.getDate()); var h = pad(d.getHours()); var mm = pad(d.getMinutes()); var s = pad(d.getSeconds()); return y + '-' + m + '-' + day + ' ' + h + ':' + mm + ':' + s; }, isRouteCooling: function(route) { if (!route || !route.cooldownUntil) return false; var x = new Date(route.cooldownUntil).getTime(); return !isNaN(x) && x > Date.now(); }, routeCardClass: function(route) { return { cooling: this.isRouteCooling(route), disabled: route && route.status !== 1 }; }, copyApiKey: function(route) { var self = this; var text = route && route.apiKey ? String(route.apiKey) : ''; if (!text) { self.$message.warning('API Key 为空'); return; } var afterCopy = function(ok) { if (ok) self.$message.success('API Key å·²å¤å¶'); else self.$message.error('å¤å¶å¤±è´¥ï¼è¯·æå¨å¤å¶'); }; if (navigator && navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text) .then(function(){ afterCopy(true); }) .catch(function(){ afterCopy(false); }); return; } var ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', 'readonly'); ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); var ok = false; try { ok = document.execCommand('copy'); } catch (e) { ok = false; } document.body.removeChild(ta); afterCopy(ok); }, copyText: function(text) { var self = this; var val = text ? String(text) : ''; if (!val) { self.$message.warning('没æå¯å¤å¶å 容'); return; } var done = function(ok) { if (ok) self.$message.success('å·²å¤å¶'); else self.$message.error('å¤å¶å¤±è´¥ï¼è¯·æå¨å¤å¶'); }; if (navigator && navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(val).then(function(){ done(true); }).catch(function(){ done(false); }); return; } var ta = document.createElement('textarea'); ta.value = val; ta.setAttribute('readonly', 'readonly'); ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); var ok = false; try { ok = document.execCommand('copy'); } catch (e) { ok = false; } document.body.removeChild(ta); done(ok); }, authHeaders: function() { return { 'token': localStorage.getItem('token') }; }, exportRoutes: function() { var self = this; fetch(baseUrl + '/ai/llm/config/export/auth', { headers: self.authHeaders() }) .then(function(r){ return r.json(); }) .then(function(res){ if (!res || res.code !== 200) { self.$message.error((res && res.msg) ? res.msg : '导åºå¤±è´¥'); return; } var payload = res.data || {}; var text = JSON.stringify(payload, null, 2); var name = 'llm_routes_' + self.buildExportTimestamp() + '.json'; var blob = new Blob([text], { type: 'application/json;charset=utf-8' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; document.body.appendChild(a); a.click(); setTimeout(function() { URL.revokeObjectURL(a.href); document.body.removeChild(a); }, 0); self.$message.success('å¯¼åºæå'); }) .catch(function(){ self.$message.error('导åºå¤±è´¥'); }); }, buildExportTimestamp: function() { var d = new Date(); var pad = function(n) { return n < 10 ? ('0' + n) : String(n); }; return d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '_' + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()); }, triggerImport: function() { var input = this.$refs.importFileInput; if (!input) return; input.value = ''; input.click(); }, handleImportFileChange: function(evt) { var self = this; var files = evt && evt.target && evt.target.files ? evt.target.files : null; var file = files && files.length > 0 ? files[0] : null; if (!file) return; var reader = new FileReader(); reader.onload = function(e) { var text = e && e.target ? e.target.result : ''; var parsed; try { parsed = JSON.parse(text || '{}'); } catch (err) { self.$message.error('JSON æ ¼å¼ä¸æ£ç¡®'); return; } var routes = self.extractImportRoutes(parsed); if (!routes || routes.length === 0) { self.$message.warning('æªæ¾å°å¯å¯¼å ¥ç routes'); return; } self.$confirm( 'è¯·éæ©å¯¼å ¥æ¹å¼ï¼è¦çå¯¼å ¥ä¼å æ¸ ç©ºç°æè·¯ç±ï¼ç¹å»âåå¹¶å¯¼å ¥âåæIDæ´æ°ææ°å¢ã', 'å¯¼å ¥ç¡®è®¤', { type: 'warning', distinguishCancelAndClose: true, confirmButtonText: 'è¦çå¯¼å ¥', cancelButtonText: 'åå¹¶å¯¼å ¥', closeOnClickModal: false } ).then(function() { self.doImportRoutes(routes, true); }).catch(function(action) { if (action === 'cancel') { self.doImportRoutes(routes, false); } }); }; reader.onerror = function() { self.$message.error('读åæä»¶å¤±è´¥'); }; reader.readAsText(file, 'utf-8'); }, extractImportRoutes: function(parsed) { if (Array.isArray(parsed)) return parsed; if (!parsed || typeof parsed !== 'object') return []; if (Array.isArray(parsed.routes)) return parsed.routes; if (parsed.data && Array.isArray(parsed.data.routes)) return parsed.data.routes; if (Array.isArray(parsed.data)) return parsed.data; return []; }, doImportRoutes: function(routes, replace) { var self = this; fetch(baseUrl + '/ai/llm/config/import/auth', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, self.authHeaders()), body: JSON.stringify({ replace: replace === true, routes: routes }) }) .then(function(r){ return r.json(); }) .then(function(res){ if (!res || res.code !== 200) { self.$message.error((res && res.msg) ? res.msg : 'å¯¼å ¥å¤±è´¥'); return; } var d = res.data || {}; var msg = 'å¯¼å ¥å®æï¼æ°å¢ ' + (d.inserted || 0) + 'ï¼æ´æ° ' + (d.updated || 0) + 'ï¼è·³è¿ ' + (d.skipped || 0); if (d.errorCount && d.errorCount > 0) { msg += 'ï¼å¼å¸¸ ' + d.errorCount; } self.$message.success(msg); if (Array.isArray(d.errors) && d.errors.length > 0) { self.$alert(d.errors.join('\n'), 'å¯¼å ¥å¼å¸¸æç»ï¼æå¤20æ¡ï¼', { confirmButtonText: 'ç¡®å®', type: 'warning' }); } self.loadRoutes(); }) .catch(function(){ self.$message.error('å¯¼å ¥å¤±è´¥'); }); }, handleRouteCommand: function(command, route, idx) { if (command === 'test') return this.testRoute(route); if (command === 'save') return this.saveRoute(route); if (command === 'cooldown') return this.clearCooldown(route); if (command === 'delete') return this.deleteRoute(route, idx); }, openLogDialog: function() { this.logDialogVisible = true; this.loadLogs(1); }, resetLogQuery: function() { this.logQuery.scene = ''; this.logQuery.success = ''; this.logQuery.traceId = ''; this.loadLogs(1); }, buildLogQuery: function(curr) { var q = []; q.push('curr=' + encodeURIComponent(curr || 1)); q.push('limit=' + encodeURIComponent(this.logPage.limit)); if (this.logQuery.scene) q.push('scene=' + encodeURIComponent(this.logQuery.scene)); if (this.logQuery.success !== '' && this.logQuery.success !== null && this.logQuery.success !== undefined) { q.push('success=' + encodeURIComponent(this.logQuery.success)); } if (this.logQuery.traceId) q.push('traceId=' + encodeURIComponent(this.logQuery.traceId)); return q.join('&'); }, loadLogs: function(curr) { var self = this; self.logLoading = true; fetch(baseUrl + '/ai/llm/log/list/auth?' + self.buildLogQuery(curr), { headers: self.authHeaders() }) .then(function(r){ return r.json(); }) .then(function(res){ self.logLoading = false; if (!res || res.code !== 200) { self.$message.error((res && res.msg) ? res.msg : 'æ¥å¿å 载失败'); return; } var p = res.data || {}; self.logPage.records = Array.isArray(p.records) ? p.records : []; self.logPage.curr = p.current || curr || 1; self.logPage.limit = p.size || self.logPage.limit; self.logPage.total = p.total || 0; }) .catch(function(){ self.logLoading = false; self.$message.error('æ¥å¿å 载失败'); }); }, showLogDetail: function(row) { var text = '' + 'æ¶é´: ' + this.formatDateTime(row.createTime) + '\n' + 'TraceId: ' + (row.traceId || '-') + '\n' + 'åºæ¯: ' + (row.scene || '-') + '\n' + 'è·¯ç±: ' + (row.routeName || '-') + '\n' + '模å: ' + (row.model || '-') + '\n' + 'ç¶æç : ' + (row.httpStatus != null ? row.httpStatus : '-') + '\n' + 'èæ¶: ' + (row.latencyMs != null ? row.latencyMs : '-') + ' ms\n' + 'ç»æ: ' + (row.success === 1 ? 'æå' : '失败') + '\n' + 'é误: ' + (row.errorMessage || '-') + '\n\n' + '请æ±:\n' + (row.requestContent || '-') + '\n\n' + 'ååº:\n' + (row.responseContent || '-'); this.logDetailTitle = 'æ¥å¿è¯¦æ - ' + (row.traceId || row.id || ''); this.logDetailText = text; this.logDetailVisible = true; }, deleteLog: function(row) { var self = this; if (!row || !row.id) return; self.$confirm('ç¡®å®å é¤è¯¥æ¥å¿åï¼', 'æç¤º', { type: 'warning' }).then(function() { fetch(baseUrl + '/ai/llm/log/delete/auth?id=' + encodeURIComponent(row.id), { method: 'POST', headers: self.authHeaders() }) .then(function(r){ return r.json(); }) .then(function(res){ if (res && res.code === 200) { self.$message.success('å 餿å'); self.loadLogs(self.logPage.curr); } else { self.$message.error((res && res.msg) ? res.msg : 'å é¤å¤±è´¥'); } }) .catch(function(){ self.$message.error('å é¤å¤±è´¥'); }); }).catch(function(){}); }, clearLogs: function() { var self = this; self.$confirm('ç¡®å®æ¸ ç©ºå ¨é¨LLMè°ç¨æ¥å¿åï¼', 'æç¤º', { type: 'warning' }).then(function() { fetch(baseUrl + '/ai/llm/log/clear/auth', { method: 'POST', headers: self.authHeaders() }) .then(function(r){ return r.json(); }) .then(function(res){ if (res && res.code === 200) { self.$message.success('å·²æ¸ ç©º'); self.loadLogs(1); } else { self.$message.error((res && res.msg) ? res.msg : 'æ¸ ç©ºå¤±è´¥'); } }) .catch(function(){ self.$message.error('æ¸ ç©ºå¤±è´¥'); }); }).catch(function(){}); }, loadRoutes: function() { var self = this; self.loading = true; fetch(baseUrl + '/ai/llm/config/list/auth', { headers: self.authHeaders() }) .then(function(r){ return r.json(); }) .then(function(res){ self.loading = false; if (res && res.code === 200) { self.routes = Array.isArray(res.data) ? res.data : []; } else { self.$message.error((res && res.msg) ? res.msg : 'å 载失败'); } }) .catch(function(){ self.loading = false; self.$message.error('å 载失败'); }); }, addRoute: function() { this.routes.unshift({ id: null, name: '', baseUrl: '', apiKey: '', model: '', thinking: 0, priority: 100, status: 1, switchOnQuota: 1, switchOnError: 1, cooldownSeconds: 300, successCount: 0, failCount: 0, consecutiveFailCount: 0, cooldownUntil: null, lastError: null }); }, buildPayload: function(route) { return { id: route.id, name: route.name, baseUrl: route.baseUrl, apiKey: route.apiKey, model: route.model, thinking: route.thinking, priority: route.priority, status: route.status, switchOnQuota: route.switchOnQuota, switchOnError: route.switchOnError, cooldownSeconds: route.cooldownSeconds, memo: route.memo }; }, saveRoute: function(route) { var self = this; fetch(baseUrl + '/ai/llm/config/save/auth', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, self.authHeaders()), body: JSON.stringify(self.buildPayload(route)) }) .then(function(r){ return r.json(); }) .then(function(res){ if (res && res.code === 200) { self.$message.success('ä¿åæå'); self.loadRoutes(); } else { self.$message.error((res && res.msg) ? res.msg : 'ä¿å失败'); } }) .catch(function(){ self.$message.error('ä¿å失败'); }); }, deleteRoute: function(route, idx) { var self = this; if (!route.id) { self.routes.splice(idx, 1); return; } self.$confirm('ç¡®å®å é¤è¯¥è·¯ç±åï¼', 'æç¤º', { type: 'warning' }).then(function() { fetch(baseUrl + '/ai/llm/config/delete/auth?id=' + encodeURIComponent(route.id), { method: 'POST', headers: self.authHeaders() }) .then(function(r){ return r.json(); }) .then(function(res){ if (res && res.code === 200) { self.$message.success('å 餿å'); self.loadRoutes(); } else { self.$message.error((res && res.msg) ? res.msg : 'å é¤å¤±è´¥'); } }) .catch(function(){ self.$message.error('å é¤å¤±è´¥'); }); }).catch(function(){}); }, clearCooldown: function(route) { var self = this; if (!route.id) return; fetch(baseUrl + '/ai/llm/config/clearCooldown/auth?id=' + encodeURIComponent(route.id), { method: 'POST', headers: self.authHeaders() }) .then(function(r){ return r.json(); }) .then(function(res){ if (res && res.code === 200) { self.$message.success('å·²æ¸ é¤å·å´'); self.loadRoutes(); } else { self.$message.error((res && res.msg) ? res.msg : 'æä½å¤±è´¥'); } }) .catch(function(){ self.$message.error('æä½å¤±è´¥'); }); }, testRoute: function(route) { var self = this; if (route.__testing === true) return; if (!route.id) { self.$message.warning('å½åæ¯æªä¿åé ç½®ï¼æµè¯éè¿åä»éå ä¿åæä¼çæ'); } self.$set(route, '__testing', true); fetch(baseUrl + '/ai/llm/config/test/auth', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, self.authHeaders()), body: JSON.stringify(self.buildPayload(route)) }) .then(function(r){ return r.json(); }) .then(function(res){ if (!res || res.code !== 200) { self.$message.error((res && res.msg) ? res.msg : 'æµè¯å¤±è´¥'); return; } var data = res.data || {}; var ok = data.ok === true; var title = ok ? 'æµè¯æå' : 'æµè¯å¤±è´¥'; var msg = '' + 'è·¯ç±: ' + (route.name || '-') + '\n' + 'Base URL: ' + (route.baseUrl || '-') + '\n' + 'ç¶æç : ' + (data.statusCode != null ? data.statusCode : '-') + '\n' + 'èæ¶: ' + (data.latencyMs != null ? data.latencyMs : '-') + ' ms\n' + 'ç»æ: ' + (data.message || '-') + '\n' + 'è¿åçæ®µ: ' + (data.responseSnippet || '-'); self.$alert(msg, title, { confirmButtonText: 'ç¡®å®', type: ok ? 'success' : 'error' }); }) .catch(function(){ self.$message.error('æµè¯å¤±è´¥'); }) .finally(function(){ self.$set(route, '__testing', false); }); } }, mounted: function() { this.loadRoutes(); } }); </script> </body> </html>