2个文件已删除
18个文件已添加
10个文件已修改
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | |
| | | WcsDiagnosisRequest request = aiUtils.makeAiRequest(1000, "对å½åç³»ç»è¿è¡å·¡æ£ï¼å¦ææå¼å¸¸æ
åµå°±è¿è¡è¯¦ç»çåæï¼å¦ææ²¡æå¼å¸¸æ
åµå彿䏿¬¡æ£æ¥\n\n"); |
| | | wcsDiagnosisService.diagnoseStream(request, emitter); |
| | | } catch (Exception e) { |
| | | emitter.completeWithError(e); |
| | | try { emitter.complete(); } catch (Exception ignore) {} |
| | | } |
| | | }).start(); |
| | | |
| | |
| | | WcsDiagnosisRequest request = aiUtils.makeAiRequest(100, null); |
| | | wcsDiagnosisService.askStream(request, prompt, chatId, reset, emitter); |
| | | } catch (Exception e) { |
| | | emitter.completeWithError(e); |
| | | try { emitter.complete(); } catch (Exception ignore) {} |
| | | } |
| | | }).start(); |
| | | return emitter; |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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> { |
| | | } |
| New file |
| | |
| | | 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> { |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | |
| | | package com.zy.ai.service; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.zy.ai.entity.ChatCompletionRequest; |
| | | import com.zy.ai.entity.ChatCompletionResponse; |
| | | import com.zy.ai.entity.LlmCallLog; |
| | | import com.zy.ai.entity.LlmRouteConfig; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.web.reactive.function.client.WebClient; |
| | | import reactor.core.publisher.Mono; |
| | | import reactor.core.publisher.Flux; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.ArrayList; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.function.Consumer; |
| | | import java.util.UUID; |
| | | import java.util.concurrent.LinkedBlockingQueue; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import java.util.function.Consumer; |
| | | |
| | | @Slf4j |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class LlmChatService { |
| | | |
| | | private final WebClient llmWebClient; |
| | | private static final int LOG_TEXT_LIMIT = 16000; |
| | | |
| | | @Value("${llm.api-key}") |
| | | private String apiKey; |
| | | private final LlmRoutingService llmRoutingService; |
| | | private final LlmCallLogService llmCallLogService; |
| | | |
| | | @Value("${llm.model}") |
| | | private String model; |
| | | @Value("${llm.base-url:}") |
| | | private String fallbackBaseUrl; |
| | | |
| | | @Value("${llm.pythonPlatformUrl}") |
| | | private String pythonPlatformUrl; |
| | | @Value("${llm.api-key:}") |
| | | private String fallbackApiKey; |
| | | |
| | | @Value("${llm.thinking}") |
| | | private String thinking; |
| | | @Value("${llm.model:}") |
| | | private String fallbackModel; |
| | | |
| | | @Value("${llm.thinking:false}") |
| | | private String fallbackThinking; |
| | | |
| | | /** |
| | | * éç¨å¯¹è¯æ¹æ³ï¼ä¼ å
¥ messagesï¼è¿åå¤§æ¨¡åææ¬åå¤ |
| | |
| | | Integer maxTokens) { |
| | | |
| | | ChatCompletionRequest req = new ChatCompletionRequest(); |
| | | req.setModel(model); |
| | | req.setMessages(messages); |
| | | req.setTemperature(temperature != null ? temperature : 0.3); |
| | | req.setMax_tokens(maxTokens != null ? maxTokens : 1024); |
| | | req.setStream(false); |
| | | |
| | | ChatCompletionResponse response = llmWebClient.post() |
| | | .uri("/chat/completions") |
| | | .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) |
| | | .contentType(MediaType.APPLICATION_JSON) |
| | | .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM) |
| | | .bodyValue(req) |
| | | .exchangeToMono(resp -> resp.bodyToFlux(String.class) |
| | | .collectList() |
| | | .map(list -> { |
| | | String payload = String.join("\n\n", list); |
| | | return parseCompletion(payload); |
| | | })) |
| | | .doOnError(ex -> log.error("è°ç¨ LLM 失败", ex)) |
| | | .onErrorResume(ex -> Mono.empty()) |
| | | .block(); |
| | | ChatCompletionResponse response = complete(req, "chat"); |
| | | |
| | | if (response == null || |
| | | response.getChoices() == null || |
| | |
| | | Integer maxTokens, |
| | | List<Object> tools) { |
| | | ChatCompletionRequest req = new ChatCompletionRequest(); |
| | | req.setModel(model); |
| | | req.setMessages(messages); |
| | | req.setTemperature(temperature != null ? temperature : 0.3); |
| | | req.setMax_tokens(maxTokens != null ? maxTokens : 1024); |
| | | req.setStream(false); |
| | | |
| | | if(thinking.equals("enable")) { |
| | | ChatCompletionRequest.Thinking thinking = new ChatCompletionRequest.Thinking(); |
| | | thinking.setType("enable"); |
| | | req.setThinking(thinking); |
| | | } |
| | | if (tools != null && !tools.isEmpty()) { |
| | | req.setTools(tools); |
| | | req.setTool_choice("auto"); |
| | | } |
| | | return complete(req); |
| | | return complete(req, tools != null && !tools.isEmpty() ? "chat_completion_tools" : "chat_completion"); |
| | | } |
| | | |
| | | public ChatCompletionResponse complete(ChatCompletionRequest req) { |
| | | try { |
| | | return llmWebClient.post() |
| | | .uri("/chat/completions") |
| | | .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) |
| | | .contentType(MediaType.APPLICATION_JSON) |
| | | .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM) |
| | | .bodyValue(req) |
| | | .exchangeToMono(resp -> resp.bodyToFlux(String.class) |
| | | .collectList() |
| | | .map(list -> { |
| | | String payload = String.join("\n\n", list); |
| | | return parseCompletion(payload); |
| | | })) |
| | | .doOnError(ex -> log.error("è°ç¨ LLM 失败", ex)) |
| | | .onErrorResume(ex -> Mono.empty()) |
| | | .block(); |
| | | } catch (Exception e) { |
| | | log.error("è°ç¨ LLM 失败", e); |
| | | return complete(req, "completion"); |
| | | } |
| | | |
| | | private ChatCompletionResponse complete(ChatCompletionRequest req, String scene) { |
| | | String traceId = nextTraceId(); |
| | | List<ResolvedRoute> routes = resolveRoutes(); |
| | | if (routes.isEmpty()) { |
| | | log.error("è°ç¨ LLM 失败: æªé
ç½®å¯ç¨ LLM è·¯ç±"); |
| | | recordCall(traceId, scene, false, 1, null, false, null, 0L, req, null, "none", |
| | | new RuntimeException("æªé
ç½®å¯ç¨ LLM è·¯ç±"), "no_route"); |
| | | return null; |
| | | } |
| | | |
| | | Throwable last = null; |
| | | for (int i = 0; i < routes.size(); i++) { |
| | | ResolvedRoute route = routes.get(i); |
| | | boolean hasNext = i < routes.size() - 1; |
| | | ChatCompletionRequest routeReq = applyRoute(cloneRequest(req), route, false); |
| | | long start = System.currentTimeMillis(); |
| | | try { |
| | | CompletionCallResult callResult = callCompletion(route, routeReq); |
| | | ChatCompletionResponse resp = callResult.response; |
| | | if (!isValidCompletion(resp)) { |
| | | RuntimeException ex = new RuntimeException("LLM ååºä¸ºç©º"); |
| | | boolean canSwitch = shouldSwitch(route, false); |
| | | markFailure(route, ex, canSwitch); |
| | | recordCall(traceId, scene, false, i + 1, route, false, callResult.statusCode, |
| | | System.currentTimeMillis() - start, routeReq, callResult.payload, "error", ex, |
| | | "invalid_completion"); |
| | | if (hasNext && canSwitch) { |
| | | log.warn("LLM 忢å°ä¸ä¸è·¯ç±, current={}, reason={}", route.tag(), ex.getMessage()); |
| | | continue; |
| | | } |
| | | log.error("è°ç¨ LLM 失败, route={}", route.tag(), ex); |
| | | last = ex; |
| | | break; |
| | | } |
| | | markSuccess(route); |
| | | recordCall(traceId, scene, false, i + 1, route, true, callResult.statusCode, |
| | | System.currentTimeMillis() - start, routeReq, buildResponseText(resp, callResult.payload), |
| | | "none", null, null); |
| | | return resp; |
| | | } catch (Throwable ex) { |
| | | last = ex; |
| | | boolean quota = isQuotaExhausted(ex); |
| | | boolean canSwitch = shouldSwitch(route, quota); |
| | | markFailure(route, ex, canSwitch); |
| | | recordCall(traceId, scene, false, i + 1, route, false, statusCodeOf(ex), |
| | | System.currentTimeMillis() - start, routeReq, responseBodyOf(ex), |
| | | quota ? "quota" : "error", ex, null); |
| | | if (hasNext && canSwitch) { |
| | | log.warn("LLM 忢å°ä¸ä¸è·¯ç±, current={}, reason={}", route.tag(), errorText(ex)); |
| | | continue; |
| | | } |
| | | log.error("è°ç¨ LLM 失败, route={}", route.tag(), ex); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (last != null) { |
| | | log.error("è°ç¨ LLM å
¨é¨è·¯ç±å¤±è´¥: {}", errorText(last)); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public void chatStream(List<ChatCompletionRequest.Message> messages, |
| | |
| | | Consumer<Throwable> onError) { |
| | | |
| | | ChatCompletionRequest req = new ChatCompletionRequest(); |
| | | req.setModel(model); |
| | | req.setMessages(messages); |
| | | req.setTemperature(temperature != null ? temperature : 0.3); |
| | | req.setMax_tokens(maxTokens != null ? maxTokens : 1024); |
| | | req.setStream(true); |
| | | |
| | | |
| | | Flux<String> flux = llmWebClient.post() |
| | | .uri("/chat/completions") |
| | | .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) |
| | | .contentType(MediaType.APPLICATION_JSON) |
| | | .accept(MediaType.TEXT_EVENT_STREAM) |
| | | .bodyValue(req) |
| | | .retrieve() |
| | | .bodyToFlux(String.class) |
| | | .doOnError(ex -> log.error("è°ç¨ LLM æµå¼å¤±è´¥", ex)); |
| | | |
| | | AtomicBoolean doneSeen = new AtomicBoolean(false); |
| | | AtomicBoolean errorSeen = new AtomicBoolean(false); |
| | | LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); |
| | | |
| | | Thread drain = new Thread(() -> { |
| | | try { |
| | | while (true) { |
| | | String s = queue.poll(2, TimeUnit.SECONDS); |
| | | if (s != null) { |
| | | try { onChunk.accept(s); } catch (Exception ignore) {} |
| | | } |
| | | if (doneSeen.get() && queue.isEmpty()) { |
| | | if (!errorSeen.get()) { |
| | | try { if (onComplete != null) onComplete.run(); } catch (Exception ignore) {} |
| | | } |
| | | break; |
| | | } |
| | | } |
| | | } catch (InterruptedException ignore) { |
| | | ignore.printStackTrace(); |
| | | } |
| | | }); |
| | | drain.setDaemon(true); |
| | | drain.start(); |
| | | |
| | | flux.subscribe(payload -> { |
| | | if (payload == null || payload.isEmpty()) return; |
| | | String[] events = payload.split("\\r?\\n\\r?\\n"); |
| | | for (String part : events) { |
| | | String s = part; |
| | | if (s == null || s.isEmpty()) continue; |
| | | if (s.startsWith("data:")) { |
| | | s = s.substring(5); |
| | | if (s.startsWith(" ")) s = s.substring(1); |
| | | } |
| | | if ("[DONE]".equals(s.trim())) { |
| | | doneSeen.set(true); |
| | | continue; |
| | | } |
| | | try { |
| | | JSONObject obj = JSON.parseObject(s); |
| | | JSONArray choices = obj.getJSONArray("choices"); |
| | | if (choices != null && !choices.isEmpty()) { |
| | | JSONObject c0 = choices.getJSONObject(0); |
| | | JSONObject delta = c0.getJSONObject("delta"); |
| | | if (delta != null) { |
| | | String content = delta.getString("content"); |
| | | if (content != null) { |
| | | try { queue.offer(content); } catch (Exception ignore) {} |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | }, err -> { |
| | | errorSeen.set(true); |
| | | doneSeen.set(true); |
| | | if (onError != null) onError.accept(err); |
| | | }, () -> { |
| | | if (!doneSeen.get()) { |
| | | errorSeen.set(true); |
| | | doneSeen.set(true); |
| | | if (onError != null) onError.accept(new RuntimeException("LLM æµæå¤å®æ")); |
| | | } else { |
| | | doneSeen.set(true); |
| | | } |
| | | }); |
| | | streamWithFailover(req, onChunk, onComplete, onError, "chat_stream"); |
| | | } |
| | | |
| | | public void chatStreamWithTools(List<ChatCompletionRequest.Message> messages, |
| | |
| | | Runnable onComplete, |
| | | Consumer<Throwable> onError) { |
| | | ChatCompletionRequest req = new ChatCompletionRequest(); |
| | | req.setModel(model); |
| | | req.setMessages(messages); |
| | | req.setTemperature(temperature != null ? temperature : 0.3); |
| | | req.setMax_tokens(maxTokens != null ? maxTokens : 1024); |
| | | req.setStream(true); |
| | | if(thinking.equals("enable")) { |
| | | ChatCompletionRequest.Thinking thinking = new ChatCompletionRequest.Thinking(); |
| | | thinking.setType("enable"); |
| | | req.setThinking(thinking); |
| | | } |
| | | if (tools != null && !tools.isEmpty()) { |
| | | req.setTools(tools); |
| | | req.setTool_choice("auto"); |
| | | } |
| | | Flux<String> flux = llmWebClient.post() |
| | | .uri("/chat/completions") |
| | | .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) |
| | | .contentType(MediaType.APPLICATION_JSON) |
| | | .accept(MediaType.TEXT_EVENT_STREAM) |
| | | .bodyValue(req) |
| | | .retrieve() |
| | | .bodyToFlux(String.class) |
| | | .doOnError(ex -> log.error("è°ç¨ LLM æµå¼å¤±è´¥", ex)); |
| | | |
| | | AtomicBoolean doneSeen = new AtomicBoolean(false); |
| | | AtomicBoolean errorSeen = new AtomicBoolean(false); |
| | | LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); |
| | | |
| | | Thread drain = new Thread(() -> { |
| | | try { |
| | | while (true) { |
| | | String s = queue.poll(5, TimeUnit.SECONDS); |
| | | if (s != null) { |
| | | try { onChunk.accept(s); } catch (Exception ignore) {} |
| | | } |
| | | if (doneSeen.get() && queue.isEmpty()) { |
| | | if (!errorSeen.get()) { |
| | | try { if (onComplete != null) onComplete.run(); } catch (Exception ignore) {} |
| | | } |
| | | break; |
| | | } |
| | | } |
| | | } catch (InterruptedException ignore) { |
| | | ignore.printStackTrace(); |
| | | } |
| | | }); |
| | | drain.setDaemon(true); |
| | | drain.start(); |
| | | |
| | | flux.subscribe(payload -> { |
| | | if (payload == null || payload.isEmpty()) return; |
| | | String[] events = payload.split("\\r?\\n\\r?\\n"); |
| | | for (String part : events) { |
| | | String s = part; |
| | | if (s == null || s.isEmpty()) continue; |
| | | if (s.startsWith("data:")) { |
| | | s = s.substring(5); |
| | | if (s.startsWith(" ")) s = s.substring(1); |
| | | } |
| | | if ("[DONE]".equals(s.trim())) { |
| | | doneSeen.set(true); |
| | | continue; |
| | | } |
| | | try { |
| | | JSONObject obj = JSON.parseObject(s); |
| | | JSONArray choices = obj.getJSONArray("choices"); |
| | | if (choices != null && !choices.isEmpty()) { |
| | | JSONObject c0 = choices.getJSONObject(0); |
| | | JSONObject delta = c0.getJSONObject("delta"); |
| | | if (delta != null) { |
| | | String content = delta.getString("content"); |
| | | if (content != null) { |
| | | try { queue.offer(content); } catch (Exception ignore) {} |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | }, err -> { |
| | | errorSeen.set(true); |
| | | doneSeen.set(true); |
| | | if (onError != null) onError.accept(err); |
| | | }, () -> { |
| | | if (!doneSeen.get()) { |
| | | errorSeen.set(true); |
| | | doneSeen.set(true); |
| | | if (onError != null) onError.accept(new RuntimeException("LLM æµæå¤å®æ")); |
| | | } else { |
| | | doneSeen.set(true); |
| | | } |
| | | }); |
| | | streamWithFailover(req, onChunk, onComplete, onError, tools != null && !tools.isEmpty() ? "chat_stream_tools" : "chat_stream"); |
| | | } |
| | | |
| | | public void chatStreamRunPython(String prompt, String chatId, Consumer<String> onChunk, |
| | | private void streamWithFailover(ChatCompletionRequest req, |
| | | Consumer<String> onChunk, |
| | | Runnable onComplete, |
| | | Consumer<Throwable> onError) { |
| | | HashMap<String, Object> req = new HashMap<>(); |
| | | req.put("prompt", prompt); |
| | | req.put("chatId", chatId); |
| | | Consumer<Throwable> onError, |
| | | String scene) { |
| | | String traceId = nextTraceId(); |
| | | List<ResolvedRoute> routes = resolveRoutes(); |
| | | if (routes.isEmpty()) { |
| | | recordCall(traceId, scene, true, 1, null, false, null, 0L, req, null, "none", |
| | | new RuntimeException("æªé
ç½®å¯ç¨ LLM è·¯ç±"), "no_route"); |
| | | if (onError != null) onError.accept(new RuntimeException("æªé
ç½®å¯ç¨ LLM è·¯ç±")); |
| | | return; |
| | | } |
| | | attemptStream(routes, 0, req, onChunk, onComplete, onError, traceId, scene); |
| | | } |
| | | |
| | | Flux<String> flux = llmWebClient.post() |
| | | .uri(pythonPlatformUrl) |
| | | .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) |
| | | .contentType(MediaType.APPLICATION_JSON) |
| | | .accept(MediaType.TEXT_EVENT_STREAM) |
| | | .bodyValue(req) |
| | | .retrieve() |
| | | .bodyToFlux(String.class) |
| | | .doOnError(ex -> log.error("è°ç¨ LLM æµå¼å¤±è´¥", ex)); |
| | | private void attemptStream(List<ResolvedRoute> routes, |
| | | int index, |
| | | ChatCompletionRequest req, |
| | | Consumer<String> onChunk, |
| | | Runnable onComplete, |
| | | Consumer<Throwable> onError, |
| | | String traceId, |
| | | String scene) { |
| | | if (index >= routes.size()) { |
| | | if (onError != null) onError.accept(new RuntimeException("LLM è·¯ç±å
¨é¨å¤±è´¥")); |
| | | return; |
| | | } |
| | | |
| | | ResolvedRoute route = routes.get(index); |
| | | ChatCompletionRequest routeReq = applyRoute(cloneRequest(req), route, true); |
| | | long start = System.currentTimeMillis(); |
| | | StringBuilder outputBuffer = new StringBuilder(); |
| | | |
| | | AtomicBoolean doneSeen = new AtomicBoolean(false); |
| | | AtomicBoolean errorSeen = new AtomicBoolean(false); |
| | | AtomicBoolean emitted = new AtomicBoolean(false); |
| | | LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); |
| | | |
| | | Thread drain = new Thread(() -> { |
| | |
| | | while (true) { |
| | | String s = queue.poll(2, TimeUnit.SECONDS); |
| | | if (s != null) { |
| | | emitted.set(true); |
| | | try { |
| | | onChunk.accept(s); |
| | | } catch (Exception ignore) { |
| | |
| | | } |
| | | } |
| | | } catch (InterruptedException ignore) { |
| | | ignore.printStackTrace(); |
| | | } |
| | | }); |
| | | drain.setDaemon(true); |
| | | drain.start(); |
| | | |
| | | flux.subscribe(payload -> { |
| | | streamFlux(route, routeReq).subscribe(payload -> { |
| | | if (payload == null || payload.isEmpty()) return; |
| | | String[] events = payload.split("\\r?\\n\\r?\\n"); |
| | | for (String part : events) { |
| | |
| | | doneSeen.set(true); |
| | | continue; |
| | | } |
| | | if("<think>".equals(s.trim()) || "</think>".equals(s.trim())) { |
| | | queue.offer(s.trim()); |
| | | continue; |
| | | } |
| | | try { |
| | | JSONObject obj = JSON.parseObject(s); |
| | | JSONArray choices = obj.getJSONArray("choices"); |
| | |
| | | if (delta != null) { |
| | | String content = delta.getString("content"); |
| | | if (content != null) { |
| | | try { |
| | | queue.offer(content); |
| | | } catch (Exception ignore) { |
| | | } |
| | | queue.offer(content); |
| | | appendLimited(outputBuffer, content); |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | log.warn("è§£æ LLM stream çæ®µå¤±è´¥: {}", e.getMessage()); |
| | | } |
| | | } |
| | | }, err -> { |
| | | errorSeen.set(true); |
| | | doneSeen.set(true); |
| | | boolean quota = isQuotaExhausted(err); |
| | | boolean canSwitch = shouldSwitch(route, quota); |
| | | markFailure(route, err, canSwitch); |
| | | recordCall(traceId, scene, true, index + 1, route, false, statusCodeOf(err), |
| | | System.currentTimeMillis() - start, routeReq, outputBuffer.toString(), |
| | | quota ? "quota" : "error", err, "emitted=" + emitted.get()); |
| | | if (!emitted.get() && canSwitch && index < routes.size() - 1) { |
| | | log.warn("LLM è·¯ç±å¤±è´¥ï¼èªå¨åæ¢ï¼current={}, reason={}", route.tag(), errorText(err)); |
| | | attemptStream(routes, index + 1, req, onChunk, onComplete, onError, traceId, scene); |
| | | return; |
| | | } |
| | | if (onError != null) onError.accept(err); |
| | | }, () -> { |
| | | if (!doneSeen.get()) { |
| | | RuntimeException ex = new RuntimeException("LLM æµæå¤å®æ"); |
| | | errorSeen.set(true); |
| | | doneSeen.set(true); |
| | | if (onError != null) onError.accept(new RuntimeException("LLM æµæå¤å®æ")); |
| | | boolean canSwitch = shouldSwitch(route, false); |
| | | markFailure(route, ex, canSwitch); |
| | | recordCall(traceId, scene, true, index + 1, route, false, 200, |
| | | System.currentTimeMillis() - start, routeReq, outputBuffer.toString(), |
| | | "error", ex, "unexpected_stream_end"); |
| | | if (!emitted.get() && canSwitch && index < routes.size() - 1) { |
| | | log.warn("LLM è·¯ç±æµå¼å¸¸å®æï¼èªå¨åæ¢ï¼current={}", route.tag()); |
| | | attemptStream(routes, index + 1, req, onChunk, onComplete, onError, traceId, scene); |
| | | } else { |
| | | if (onError != null) onError.accept(ex); |
| | | } |
| | | } else { |
| | | markSuccess(route); |
| | | recordCall(traceId, scene, true, index + 1, route, true, 200, |
| | | System.currentTimeMillis() - start, routeReq, outputBuffer.toString(), |
| | | "none", null, null); |
| | | doneSeen.set(true); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private Flux<String> streamFlux(ResolvedRoute route, ChatCompletionRequest req) { |
| | | WebClient client = WebClient.builder().baseUrl(route.baseUrl).build(); |
| | | return client.post() |
| | | .uri("/chat/completions") |
| | | .header(HttpHeaders.AUTHORIZATION, "Bearer " + route.apiKey) |
| | | .contentType(MediaType.APPLICATION_JSON) |
| | | .accept(MediaType.TEXT_EVENT_STREAM) |
| | | .bodyValue(req) |
| | | .exchangeToFlux(resp -> { |
| | | int status = resp.rawStatusCode(); |
| | | if (status >= 200 && status < 300) { |
| | | return resp.bodyToFlux(String.class); |
| | | } |
| | | return resp.bodyToMono(String.class) |
| | | .defaultIfEmpty("") |
| | | .flatMapMany(body -> Flux.error(new LlmRouteException(status, body))); |
| | | }) |
| | | .doOnError(ex -> log.error("è°ç¨ LLM æµå¼å¤±è´¥, route={}", route.tag(), ex)); |
| | | } |
| | | |
| | | private CompletionCallResult callCompletion(ResolvedRoute route, ChatCompletionRequest req) { |
| | | WebClient client = WebClient.builder().baseUrl(route.baseUrl).build(); |
| | | RawCompletionResult raw = client.post() |
| | | .uri("/chat/completions") |
| | | .header(HttpHeaders.AUTHORIZATION, "Bearer " + route.apiKey) |
| | | .contentType(MediaType.APPLICATION_JSON) |
| | | .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM) |
| | | .bodyValue(req) |
| | | .exchangeToMono(resp -> resp.bodyToFlux(String.class) |
| | | .collectList() |
| | | .map(list -> new RawCompletionResult(resp.rawStatusCode(), String.join("\\n\\n", list)))) |
| | | .block(); |
| | | |
| | | if (raw == null) { |
| | | throw new RuntimeException("LLM è¿å为空"); |
| | | } |
| | | if (raw.statusCode < 200 || raw.statusCode >= 300) { |
| | | throw new LlmRouteException(raw.statusCode, raw.payload); |
| | | } |
| | | return new CompletionCallResult(raw.statusCode, raw.payload, parseCompletion(raw.payload)); |
| | | } |
| | | |
| | | private ChatCompletionRequest applyRoute(ChatCompletionRequest req, ResolvedRoute route, boolean stream) { |
| | | req.setModel(route.model); |
| | | req.setStream(stream); |
| | | if (route.thinkingEnabled) { |
| | | ChatCompletionRequest.Thinking t = new ChatCompletionRequest.Thinking(); |
| | | t.setType("enable"); |
| | | req.setThinking(t); |
| | | } else { |
| | | req.setThinking(null); |
| | | } |
| | | return req; |
| | | } |
| | | |
| | | private ChatCompletionRequest cloneRequest(ChatCompletionRequest src) { |
| | | ChatCompletionRequest req = new ChatCompletionRequest(); |
| | | req.setModel(src.getModel()); |
| | | req.setMessages(src.getMessages()); |
| | | req.setTemperature(src.getTemperature()); |
| | | req.setMax_tokens(src.getMax_tokens()); |
| | | req.setStream(src.getStream()); |
| | | req.setTools(src.getTools()); |
| | | req.setTool_choice(src.getTool_choice()); |
| | | req.setThinking(src.getThinking()); |
| | | return req; |
| | | } |
| | | |
| | | private boolean isValidCompletion(ChatCompletionResponse response) { |
| | | if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) { |
| | | return false; |
| | | } |
| | | ChatCompletionRequest.Message message = response.getChoices().get(0).getMessage(); |
| | | if (message == null) { |
| | | return false; |
| | | } |
| | | if (!isBlank(message.getContent())) { |
| | | return true; |
| | | } |
| | | return message.getTool_calls() != null && !message.getTool_calls().isEmpty(); |
| | | } |
| | | |
| | | private boolean shouldSwitch(ResolvedRoute route, boolean quota) { |
| | | return quota ? route.switchOnQuota : route.switchOnError; |
| | | } |
| | | |
| | | private void markSuccess(ResolvedRoute route) { |
| | | if (route.id != null) { |
| | | llmRoutingService.markSuccess(route.id); |
| | | } |
| | | } |
| | | |
| | | private void markFailure(ResolvedRoute route, Throwable ex, boolean enterCooldown) { |
| | | if (route.id != null) { |
| | | llmRoutingService.markFailure(route.id, errorText(ex), enterCooldown, route.cooldownSeconds); |
| | | } |
| | | } |
| | | |
| | | private String errorText(Throwable ex) { |
| | | if (ex == null) return "unknown"; |
| | | if (ex instanceof LlmRouteException) { |
| | | LlmRouteException e = (LlmRouteException) ex; |
| | | String body = e.body == null ? "" : e.body; |
| | | if (body.length() > 240) { |
| | | body = body.substring(0, 240); |
| | | } |
| | | return "status=" + e.statusCode + ", body=" + body; |
| | | } |
| | | return ex.getMessage() == null ? ex.toString() : ex.getMessage(); |
| | | } |
| | | |
| | | private boolean isQuotaExhausted(Throwable ex) { |
| | | if (!(ex instanceof LlmRouteException)) return false; |
| | | LlmRouteException e = (LlmRouteException) ex; |
| | | if (e.statusCode == 429) return true; |
| | | String text = (e.body == null ? "" : e.body).toLowerCase(); |
| | | return text.contains("insufficient_quota") |
| | | || text.contains("quota") |
| | | || text.contains("ä½é¢") |
| | | || text.contains("ç¨é") |
| | | || text.contains("è¶
é") |
| | | || text.contains("rate limit"); |
| | | } |
| | | |
| | | private List<ResolvedRoute> resolveRoutes() { |
| | | List<ResolvedRoute> routes = new ArrayList<>(); |
| | | List<LlmRouteConfig> dbRoutes = llmRoutingService.listAvailableRoutes(); |
| | | for (LlmRouteConfig c : dbRoutes) { |
| | | routes.add(ResolvedRoute.fromDb(c)); |
| | | } |
| | | // å
¼å®¹ï¼æ°æ®åºä¸ºç©ºæ¶ï¼åéå° yml |
| | | if (routes.isEmpty() && !isBlank(fallbackBaseUrl) && !isBlank(fallbackApiKey) && !isBlank(fallbackModel)) { |
| | | routes.add(ResolvedRoute.fromFallback(fallbackBaseUrl, fallbackApiKey, fallbackModel, isFallbackThinkingEnabled())); |
| | | } |
| | | return routes; |
| | | } |
| | | |
| | | private boolean isFallbackThinkingEnabled() { |
| | | String x = fallbackThinking == null ? "" : fallbackThinking.trim().toLowerCase(); |
| | | return "true".equals(x) || "1".equals(x) || "enable".equals(x); |
| | | } |
| | | |
| | | private boolean isBlank(String s) { |
| | | return s == null || s.trim().isEmpty(); |
| | | } |
| | | |
| | | private ChatCompletionResponse mergeSseChunk(ChatCompletionResponse acc, String payload) { |
| | |
| | | ChatCompletionResponse.Choice choice = new ChatCompletionResponse.Choice(); |
| | | ChatCompletionRequest.Message msg = new ChatCompletionRequest.Message(); |
| | | choice.setMessage(msg); |
| | | java.util.ArrayList<ChatCompletionResponse.Choice> list = new java.util.ArrayList<>(); |
| | | ArrayList<ChatCompletionResponse.Choice> list = new ArrayList<>(); |
| | | list.add(choice); |
| | | acc.setChoices(list); |
| | | } |
| | |
| | | if (created != null) acc.setCreated(created); |
| | | String object = obj.getString("object"); |
| | | if (object != null && !object.isEmpty()) acc.setObjectName(object); |
| | | } catch (Exception ignore) {} |
| | | } catch (Exception ignore) { |
| | | } |
| | | } |
| | | return acc; |
| | | } |
| | |
| | | if (r != null && r.getChoices() != null && !r.getChoices().isEmpty() && r.getChoices().get(0).getMessage() != null) { |
| | | return r; |
| | | } |
| | | } catch (Exception ignore) {} |
| | | } catch (Exception ignore) { |
| | | } |
| | | ChatCompletionResponse sse = mergeSseChunk(new ChatCompletionResponse(), payload); |
| | | if (sse.getChoices() != null && !sse.getChoices().isEmpty() && sse.getChoices().get(0).getMessage() != null && sse.getChoices().get(0).getMessage().getContent() != null) { |
| | | return sse; |
| | |
| | | msg.setRole("assistant"); |
| | | msg.setContent(payload); |
| | | choice.setMessage(msg); |
| | | java.util.ArrayList<ChatCompletionResponse.Choice> list = new java.util.ArrayList<>(); |
| | | ArrayList<ChatCompletionResponse.Choice> list = new ArrayList<>(); |
| | | list.add(choice); |
| | | r.setChoices(list); |
| | | return r; |
| | | } |
| | | |
| | | private String nextTraceId() { |
| | | return UUID.randomUUID().toString().replace("-", ""); |
| | | } |
| | | |
| | | private void appendLimited(StringBuilder sb, String text) { |
| | | if (sb == null || text == null || text.isEmpty()) { |
| | | return; |
| | | } |
| | | int remain = LOG_TEXT_LIMIT - sb.length(); |
| | | if (remain <= 0) { |
| | | return; |
| | | } |
| | | if (text.length() <= remain) { |
| | | sb.append(text); |
| | | } else { |
| | | sb.append(text, 0, remain); |
| | | } |
| | | } |
| | | |
| | | private Integer statusCodeOf(Throwable ex) { |
| | | if (ex instanceof LlmRouteException) { |
| | | return ((LlmRouteException) ex).statusCode; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String responseBodyOf(Throwable ex) { |
| | | if (ex instanceof LlmRouteException) { |
| | | return cut(((LlmRouteException) ex).body, LOG_TEXT_LIMIT); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String buildResponseText(ChatCompletionResponse resp, String fallbackPayload) { |
| | | if (resp != null && resp.getChoices() != null && !resp.getChoices().isEmpty() |
| | | && resp.getChoices().get(0) != null && resp.getChoices().get(0).getMessage() != null) { |
| | | ChatCompletionRequest.Message m = resp.getChoices().get(0).getMessage(); |
| | | if (!isBlank(m.getContent())) { |
| | | return cut(m.getContent(), LOG_TEXT_LIMIT); |
| | | } |
| | | if (m.getTool_calls() != null && !m.getTool_calls().isEmpty()) { |
| | | return cut(JSON.toJSONString(m), LOG_TEXT_LIMIT); |
| | | } |
| | | } |
| | | return cut(fallbackPayload, LOG_TEXT_LIMIT); |
| | | } |
| | | |
| | | private String safeName(Throwable ex) { |
| | | return ex == null ? null : ex.getClass().getSimpleName(); |
| | | } |
| | | |
| | | private String cut(String text, int maxLen) { |
| | | if (text == null) return null; |
| | | String clean = text.replace("\r", " "); |
| | | return clean.length() > maxLen ? clean.substring(0, maxLen) : clean; |
| | | } |
| | | |
| | | private void recordCall(String traceId, |
| | | String scene, |
| | | boolean stream, |
| | | int attemptNo, |
| | | ResolvedRoute route, |
| | | boolean success, |
| | | Integer httpStatus, |
| | | long latencyMs, |
| | | ChatCompletionRequest req, |
| | | String response, |
| | | String switchMode, |
| | | Throwable err, |
| | | String extra) { |
| | | LlmCallLog item = new LlmCallLog(); |
| | | item.setTraceId(cut(traceId, 64)); |
| | | item.setScene(cut(scene, 64)); |
| | | item.setStream((short) (stream ? 1 : 0)); |
| | | item.setAttemptNo(attemptNo); |
| | | if (route != null) { |
| | | item.setRouteId(route.id); |
| | | item.setRouteName(cut(route.name, 128)); |
| | | item.setBaseUrl(cut(route.baseUrl, 255)); |
| | | item.setModel(cut(route.model, 128)); |
| | | } |
| | | item.setSuccess((short) (success ? 1 : 0)); |
| | | item.setHttpStatus(httpStatus); |
| | | item.setLatencyMs(latencyMs < 0 ? 0 : latencyMs); |
| | | item.setSwitchMode(cut(switchMode, 32)); |
| | | item.setRequestContent(cut(JSON.toJSONString(req), LOG_TEXT_LIMIT)); |
| | | item.setResponseContent(cut(response, LOG_TEXT_LIMIT)); |
| | | item.setErrorType(cut(safeName(err), 128)); |
| | | item.setErrorMessage(err == null ? null : cut(errorText(err), 1024)); |
| | | item.setExtra(cut(extra, 512)); |
| | | item.setCreateTime(new Date()); |
| | | llmCallLogService.saveIgnoreError(item); |
| | | } |
| | | |
| | | private static class CompletionCallResult { |
| | | private final int statusCode; |
| | | private final String payload; |
| | | private final ChatCompletionResponse response; |
| | | |
| | | private CompletionCallResult(int statusCode, String payload, ChatCompletionResponse response) { |
| | | this.statusCode = statusCode; |
| | | this.payload = payload; |
| | | this.response = response; |
| | | } |
| | | } |
| | | |
| | | private static class RawCompletionResult { |
| | | private final int statusCode; |
| | | private final String payload; |
| | | |
| | | private RawCompletionResult(int statusCode, String payload) { |
| | | this.statusCode = statusCode; |
| | | this.payload = payload; |
| | | } |
| | | } |
| | | |
| | | private static class LlmRouteException extends RuntimeException { |
| | | private final int statusCode; |
| | | private final String body; |
| | | |
| | | private LlmRouteException(int statusCode, String body) { |
| | | super("http status=" + statusCode); |
| | | this.statusCode = statusCode; |
| | | this.body = body; |
| | | } |
| | | } |
| | | |
| | | private static class ResolvedRoute { |
| | | private Long id; |
| | | private String name; |
| | | private String baseUrl; |
| | | private String apiKey; |
| | | private String model; |
| | | private boolean thinkingEnabled; |
| | | private boolean switchOnQuota; |
| | | private boolean switchOnError; |
| | | private Integer cooldownSeconds; |
| | | |
| | | private static ResolvedRoute fromDb(LlmRouteConfig c) { |
| | | ResolvedRoute r = new ResolvedRoute(); |
| | | r.id = c.getId(); |
| | | r.name = c.getName(); |
| | | r.baseUrl = c.getBaseUrl(); |
| | | r.apiKey = c.getApiKey(); |
| | | r.model = c.getModel(); |
| | | r.thinkingEnabled = c.getThinking() != null && c.getThinking() == 1; |
| | | r.switchOnQuota = c.getSwitchOnQuota() == null || c.getSwitchOnQuota() == 1; |
| | | r.switchOnError = c.getSwitchOnError() == null || c.getSwitchOnError() == 1; |
| | | r.cooldownSeconds = c.getCooldownSeconds(); |
| | | return r; |
| | | } |
| | | |
| | | private static ResolvedRoute fromFallback(String baseUrl, String apiKey, String model, boolean thinkingEnabled) { |
| | | ResolvedRoute r = new ResolvedRoute(); |
| | | r.name = "fallback-yml"; |
| | | r.baseUrl = baseUrl; |
| | | r.apiKey = apiKey; |
| | | r.model = model; |
| | | r.thinkingEnabled = thinkingEnabled; |
| | | r.switchOnQuota = true; |
| | | r.switchOnError = true; |
| | | r.cooldownSeconds = 300; |
| | | return r; |
| | | } |
| | | |
| | | private String tag() { |
| | | String showName = name == null ? "unnamed" : name; |
| | | String showModel = model == null ? "" : (" model=" + model); |
| | | return showName + showModel; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.ai.service; |
| | | |
| | | import com.baomidou.mybatisplus.service.IService; |
| | | import com.zy.ai.entity.LlmRouteConfig; |
| | | |
| | | public interface LlmRouteConfigService extends IService<LlmRouteConfig> { |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | |
| | | import com.zy.common.utils.RedisUtil; |
| | | import com.zy.core.enums.RedisKeyType; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | |
| | | |
| | | private static final long CHAT_TTL_SECONDS = 7L * 24 * 3600; |
| | | |
| | | @Value("${llm.platform}") |
| | | private String platform; |
| | | @Autowired |
| | | private LlmChatService llmChatService; |
| | | @Autowired |
| | |
| | | private AiUtils aiUtils; |
| | | @Autowired(required = false) |
| | | private McpController mcpController; |
| | | @Autowired |
| | | private PythonService pythonService; |
| | | |
| | | public void diagnoseStream(WcsDiagnosisRequest request, SseEmitter emitter) { |
| | | List<ChatCompletionRequest.Message> messages = new ArrayList<>(); |
| | |
| | | try { |
| | | try { emitter.send(SseEmitter.event().data("ãAIãè¿è¡å·²åæ¢ï¼å¼å¸¸ï¼")); } catch (Exception ignore) {} |
| | | log.error("AI diagnose stream stopped: error", e); |
| | | emitter.completeWithError(e); |
| | | emitter.complete(); |
| | | } catch (Exception ignore) {} |
| | | }); |
| | | } |
| | |
| | | String chatId, |
| | | boolean reset, |
| | | SseEmitter emitter) { |
| | | if (platform.equals("python")) { |
| | | pythonService.runPython(prompt, chatId, emitter); |
| | | return; |
| | | } |
| | | |
| | | List<ChatCompletionRequest.Message> messages = new ArrayList<>(); |
| | | |
| | | List<ChatCompletionRequest.Message> history = null; |
| | |
| | | emitter.complete(); |
| | | } catch (Exception ignore) {} |
| | | }, e -> { |
| | | try { emitter.completeWithError(e); } catch (Exception ignore) {} |
| | | try { |
| | | try { emitter.send(SseEmitter.event().data("ãAIãè¿è¡å·²åæ¢ï¼å¼å¸¸ï¼")); } catch (Exception ignore) {} |
| | | emitter.complete(); |
| | | } catch (Exception ignore) {} |
| | | }); |
| | | } |
| | | |
| | |
| | | try { |
| | | sse(emitter, "\\n\\nãAIãè¿è¡å·²åæ¢ï¼å¼å¸¸ï¼\\n\\n"); |
| | | log.error("AI MCP diagnose stopped: error", e); |
| | | emitter.completeWithError(e); |
| | | emitter.complete(); |
| | | } catch (Exception ignore) {} |
| | | return true; |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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 { |
| | | } |
| | |
| | | DUAL_CRN_TRANSFER_TASK_COMPLETE("dual_crn_transfer_task_complete", "åå·¥ä½å åæºç§»åºä»»å¡æ§è¡å®æ"), |
| | | |
| | | STATION_OUT_TASK_RUN("station_out_task_run","è¾éç«ç¹åºåºä»»å¡è¿è¡ä¸"), |
| | | STATION_OUT_TASK_RUN_COMPLETE("station_out_task_run_complete","è¾éç«ç¹åºåºä»»å¡è¿è¡å®æ"), |
| | | ; |
| | | |
| | | public String flag; |
| | |
| | | continue; |
| | | } |
| | | |
| | | if (!locMast.getLocSts().equals("R")) { |
| | | log.info("[workNo={}]åºä½ç¶æä¸å¤äºR", wrkMast.getWrkNo()); |
| | | if (!(locMast.getLocSts().equals("R") || locMast.getLocSts().equals("O"))) { |
| | | log.info("[workNo={}]åºä½ç¶æä¸å¤äºR or O", wrkMast.getWrkNo()); |
| | | continue; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | @Scheduled(cron = "0/1 * * * * ? ") |
| | | @Transactional |
| | | public void processOutStationRun(){ |
| | | List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("wrk_sts", WrkStsType.STATION_RUN.sts)); |
| | | if (wrkMasts.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | for (WrkMast wrkMast : wrkMasts) { |
| | | String locNo = wrkMast.getSourceLocNo(); |
| | | LocMast locMast = locMastService.queryByLoc(locNo); |
| | | if (locMast == null) { |
| | | log.info("[workNo={}]åºä½ä¸åå¨", wrkMast.getWrkNo()); |
| | | continue; |
| | | } |
| | | |
| | | if (locMast.getLocSts().equals("O")) { |
| | | continue; |
| | | } |
| | | |
| | | if (!locMast.getLocSts().equals("R")) { |
| | | log.info("[workNo={}]åºä½ç¶æä¸å¤äºR", wrkMast.getWrkNo()); |
| | | continue; |
| | | } |
| | | |
| | | locMast.setLocSts("O"); |
| | | locMast.setBarcode(""); |
| | | locMast.setModiTime(new Date()); |
| | | boolean result = locMastService.updateById(locMast); |
| | | if (!result) { |
| | | log.info("[workNo={}]åºä½ç¶æOæ´æ°å¤±è´¥", wrkMast.getWrkNo()); |
| | | continue; |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | private NavigateNode Father;//ç¶èç¹ |
| | | private List<String> directionList;//å
许è¡èµ°æ¹å |
| | | private Boolean isInflectionPoint;//æ¯å¦ä¸ºæç¹ |
| | | private Boolean isLiftTransferPoint;//æ¯å¦ä¸ºé¡¶åç§»æ ½ç¹ |
| | | private String direction;//è¡èµ°æ¹å |
| | | private String nodeValue;//èç¹æ°æ® |
| | | private String nodeType;//èç¹ç±»å |
| | |
| | | for (int i = 0; i < fitlerList.size(); i++) { |
| | | NavigateNode currentNode = fitlerList.get(i); |
| | | currentNode.setIsInflectionPoint(false); |
| | | currentNode.setIsLiftTransferPoint(false); |
| | | |
| | | try { |
| | | JSONObject valueObject = JSON.parseObject(currentNode.getNodeValue()); |
| | | if (valueObject != null) { |
| | | Object isLiftTransfer = valueObject.get("isLiftTransfer"); |
| | | if (isLiftTransfer != null) { |
| | | String isLiftTransferStr = isLiftTransfer.toString(); |
| | | if ("1".equals(isLiftTransferStr) || "true".equalsIgnoreCase(isLiftTransferStr)) { |
| | | currentNode.setIsLiftTransferPoint(true); |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception ignore) {} |
| | | |
| | | NavigateNode nextNode = (i + 1 < fitlerList.size()) ? fitlerList.get(i + 1) : null; |
| | | NavigateNode prevNode = (i - 1 >= 0) ? fitlerList.get(i - 1) : null; |
| New file |
| | |
| | | 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()); |
| | | } |
| | | |
| | | 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<Integer> path = calcPathStationIds(stationId, targetStationId); |
| | | stationCommand.setNavigatePath(path); |
| | | } |
| | | } |
| | | 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<Integer> calcPathStationIds(Integer startStationId, Integer targetStationId) { |
| | | NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class); |
| | | if (navigateUtils == null) { |
| | | return new ArrayList<>(); |
| | | } |
| | | List<NavigateNode> nodes = navigateUtils.calcByStationId(startStationId, targetStationId); |
| | | List<Integer> ids = new ArrayList<>(); |
| | | for (NavigateNode n : nodes) { |
| | | JSONObject v = JSONObject.parseObject(n.getNodeValue()); |
| | | if (v != null) { |
| | | ids.add(v.getInteger("stationId")); |
| | | } |
| | | } |
| | | return ids; |
| | | } |
| | | |
| | | private void executeMoveWithSeg(StationCommand original) { |
| | | int stationCommandSendLength = 20; |
| | | Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key); |
| | | if (systemConfigMapObj != null) { |
| | | try { |
| | | HashMap<String, String> systemConfigMap = (HashMap<String, String>) systemConfigMapObj; |
| | | String stationCommandSendLengthStr = systemConfigMap.get("stationCommandSendLength"); |
| | | if(stationCommandSendLengthStr != null){ |
| | | stationCommandSendLength = Integer.parseInt(stationCommandSendLengthStr); |
| | | } |
| | | } catch (Exception ignore) {} |
| | | } |
| | | |
| | | if(original.getCommandType() == StationCommandType.MOVE){ |
| | | List<Integer> path = JSON.parseArray(JSON.toJSONString(original.getNavigatePath(), SerializerFeature.DisableCircularReferenceDetect), Integer.class); |
| | | if (path == null || path.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | int total = path.size(); |
| | | List<Integer> segmentTargets = new ArrayList<>(); |
| | | List<Integer> segmentEndIndices = new ArrayList<>(); |
| | | int idx = 0; |
| | | while (idx < total) { |
| | | int end = Math.min(idx + stationCommandSendLength, total) - 1; |
| | | segmentTargets.add(path.get(end)); |
| | | segmentEndIndices.add(end); |
| | | idx = end + 1; |
| | | } |
| | | |
| | | int segCursor = 0; |
| | | Integer currentTarget = segmentTargets.get(segCursor); |
| | | Integer currentEndIdx = segmentEndIndices.get(segCursor); |
| | | Integer currentStartIdx = 0; |
| | | |
| | | StationCommand segCmd = new StationCommand(); |
| | | segCmd.setTaskNo(original.getTaskNo()); |
| | | segCmd.setStationId(original.getStationId()); |
| | | segCmd.setTargetStaNo(original.getTargetStaNo()); |
| | | segCmd.setCommandType(original.getCommandType()); |
| | | segCmd.setPalletSize(original.getPalletSize()); |
| | | segCmd.setNavigatePath(new ArrayList<>(path.subList(0, currentEndIdx + 1))); |
| | | sendCommand(segCmd); |
| | | |
| | | 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 = path.indexOf(segmentTargets.get(segCursor)); |
| | | int currentSegStartIndex = segCursor == 0 ? 0 : path.indexOf(segmentTargets.get(segCursor - 1)) + 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 < segmentTargets.size() - 1) { |
| | | segCursor++; |
| | | currentEndIdx = segmentEndIndices.get(segCursor); |
| | | currentStartIdx = segmentEndIndices.get(segCursor - 1) + 1; |
| | | |
| | | StationCommand nextCmd = new StationCommand(); |
| | | nextCmd.setTaskNo(original.getTaskNo()); |
| | | nextCmd.setStationId(original.getStationId()); |
| | | nextCmd.setTargetStaNo(original.getTargetStaNo()); |
| | | nextCmd.setCommandType(original.getCommandType()); |
| | | nextCmd.setPalletSize(original.getPalletSize()); |
| | | nextCmd.setNavigatePath(new ArrayList<>(path.subList(currentStartIdx, currentEndIdx + 1))); |
| | | nextCmd.setOriginalNavigatePath(path); |
| | | while (true) { |
| | | CommandResponse commandResponse = sendCommand(nextCmd); |
| | | 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; |
| | | } |
| | | } |
| | |
| | | wrkMast.setWrkSts(WrkStsType.STATION_RUN_COMPLETE.sts); |
| | | wrkMast.setIoTime(new Date()); |
| | | wrkMastService.updateById(wrkMast); |
| | | notifyUtils.notify(String.valueOf(SlaveType.Devp), basStation.getDeviceNo(), String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.STATION_OUT_TASK_RUN_COMPLETE, null); |
| | | redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_COMPLETE_LIMIT.key + wrkMast.getWrkNo(), "lock", 60); |
| | | } |
| | | } |
| | |
| | | # ç³»ç»çæ¬ä¿¡æ¯ |
| | | app: |
| | | version: 1.0.4.1 |
| | | version: 1.0.4.4 |
| | | version-type: dev # prd æ dev |
| | | |
| | | server: |
| | |
| | | expireDays: 7 |
| | | |
| | | llm: |
| | | platform: java |
| | | pythonPlatformUrl: http://127.0.0.1:9000/ai/diagnose/askStream |
| | | thinking: enable |
| | | # base-url: https://api.siliconflow.cn/v1 |
| | | # api-key: sk-sxdtebtquwrugzrmaqqqkzdzmrgzhzmplwwuowysdasccent |
| | | # model: deepseek-ai/DeepSeek-V3.2 |
| | | # base-url: http://47.76.147.249:9998/e/7g7kqxxt1ei2un71 |
| | | # api-key: app-mP0O6aY5WpbfaHs7BNnjVkli |
| | | # model: deepseek-ai/DeepSeek-V3.2 |
| | | # base-url: http://34.2.134.223:3000/v1 |
| | | # api-key: sk-WabrmtOezCFwVo7XvVOrO3QkmfcKG7T7jy0BaVnmQTWm5GXh |
| | | # model: gemini-3-pro-preview |
| | | # base-url: http://127.0.0.1:8317/v1 |
| | | # api-key: WznOjAGJNVFKSe9kBZTr |
| | | # model: gpt-5 |
| | | base-url: https://api.xiaomimimo.com/v1 |
| | | api-key: sk-cw7e4se9cal8cxdgjml8dmtn4pdmqtvfccg5fcermt0ddtys |
| | | model: mimo-v2-flash |
| | | # ç°å·²è¿ç§»å°æ°æ®åºè¡¨ sys_llm_route ç»´æ¤ï¼æ¯æå¤API/夿¨¡å/å¤Keyèªå¨åæ¢ï¼ |
| | | # 以ä¸ä»
ä½ä¸ºæ°æ®åºä¸ºç©ºæ¶çå
¼å®¹åéé
ç½® |
| | | thinking: false |
| | | base-url: |
| | | api-key: |
| | | model: |
| | | |
| | | perf: |
| | | methodTiming: |
| | | enabled: false |
| | | thresholdMs: 50 |
| | | sampleRate: 1.0 |
| | | sampleRate: 1.0 |
| New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| | | <mapper namespace="com.zy.ai.mapper.LlmCallLogMapper"> |
| | | |
| | | <resultMap id="BaseResultMap" type="com.zy.ai.entity.LlmCallLog"> |
| | | <id column="id" property="id"/> |
| | | <result column="trace_id" property="traceId"/> |
| | | <result column="scene" property="scene"/> |
| | | <result column="stream" property="stream"/> |
| | | <result column="attempt_no" property="attemptNo"/> |
| | | <result column="route_id" property="routeId"/> |
| | | <result column="route_name" property="routeName"/> |
| | | <result column="base_url" property="baseUrl"/> |
| | | <result column="model" property="model"/> |
| | | <result column="success" property="success"/> |
| | | <result column="http_status" property="httpStatus"/> |
| | | <result column="latency_ms" property="latencyMs"/> |
| | | <result column="switch_mode" property="switchMode"/> |
| | | <result column="request_content" property="requestContent"/> |
| | | <result column="response_content" property="responseContent"/> |
| | | <result column="error_type" property="errorType"/> |
| | | <result column="error_message" property="errorMessage"/> |
| | | <result column="extra" property="extra"/> |
| | | <result column="create_time" property="createTime"/> |
| | | </resultMap> |
| | | |
| | | </mapper> |
| New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| | | <mapper namespace="com.zy.ai.mapper.LlmRouteConfigMapper"> |
| | | |
| | | <resultMap id="BaseResultMap" type="com.zy.ai.entity.LlmRouteConfig"> |
| | | <id column="id" property="id"/> |
| | | <result column="name" property="name"/> |
| | | <result column="base_url" property="baseUrl"/> |
| | | <result column="api_key" property="apiKey"/> |
| | | <result column="model" property="model"/> |
| | | <result column="thinking" property="thinking"/> |
| | | <result column="priority" property="priority"/> |
| | | <result column="status" property="status"/> |
| | | <result column="switch_on_quota" property="switchOnQuota"/> |
| | | <result column="switch_on_error" property="switchOnError"/> |
| | | <result column="cooldown_seconds" property="cooldownSeconds"/> |
| | | <result column="cooldown_until" property="cooldownUntil"/> |
| | | <result column="fail_count" property="failCount"/> |
| | | <result column="success_count" property="successCount"/> |
| | | <result column="consecutive_fail_count" property="consecutiveFailCount"/> |
| | | <result column="last_error" property="lastError"/> |
| | | <result column="last_used_time" property="lastUsedTime"/> |
| | | <result column="last_fail_time" property="lastFailTime"/> |
| | | <result column="create_time" property="createTime"/> |
| | | <result column="update_time" property="updateTime"/> |
| | | <result column="memo" property="memo"/> |
| | | </resultMap> |
| | | |
| | | </mapper> |
| New file |
| | |
| | | -- å° AIé
ç½® èåæè½½å°ï¼å¼åä¸ç¨ -> AIé
ç½® |
| | | -- 说æï¼æ¬ç³»ç»è忥æºäº sys_resourceï¼æ§è¡æ¬èæ¬å请å¨âè§è²ææâéç»å¯¹åºè§è²å¾éæ°èåã |
| | | |
| | | -- 1) å®ä½âå¼åä¸ç¨âä¸çº§èå |
| | | SET @dev_parent_id := ( |
| | | SELECT id |
| | | FROM sys_resource |
| | | WHERE name = 'å¼åä¸ç¨' AND level = 1 |
| | | ORDER BY id |
| | | LIMIT 1 |
| | | ); |
| | | |
| | | -- 2) æ°å¢äºçº§èåï¼AIé
ç½®ï¼é¡µé¢ï¼ |
| | | INSERT INTO sys_resource(code, name, resource_id, level, sort, status) |
| | | SELECT 'ai/llm_config.html', 'AIé
ç½®', @dev_parent_id, 2, 999, 1 |
| | | FROM dual |
| | | WHERE @dev_parent_id IS NOT NULL |
| | | AND NOT EXISTS ( |
| | | SELECT 1 |
| | | FROM sys_resource |
| | | WHERE code = 'ai/llm_config.html' AND level = 2 |
| | | ); |
| | | |
| | | -- 3) æ°å¢ä¸çº§æé®æéï¼æ¥çï¼ç¨äºè§è²ç»ç²åº¦ææï¼ |
| | | SET @ai_cfg_id := ( |
| | | SELECT id |
| | | FROM sys_resource |
| | | WHERE code = 'ai/llm_config.html' AND level = 2 |
| | | ORDER BY id |
| | | LIMIT 1 |
| | | ); |
| | | |
| | | INSERT INTO sys_resource(code, name, resource_id, level, sort, status) |
| | | SELECT 'ai/llm_config.html#view', 'æ¥ç', @ai_cfg_id, 3, 1, 1 |
| | | FROM dual |
| | | WHERE @ai_cfg_id IS NOT NULL |
| | | AND NOT EXISTS ( |
| | | SELECT 1 |
| | | FROM sys_resource |
| | | WHERE code = 'ai/llm_config.html#view' AND level = 3 |
| | | ); |
| | | |
| | | -- å¯éæ£æ¥ |
| | | SELECT id, code, name, resource_id, level, sort, status |
| | | FROM sys_resource |
| | | WHERE code IN ('ai/llm_config.html', 'ai/llm_config.html#view'); |
| New file |
| | |
| | | CREATE TABLE IF NOT EXISTS `sys_llm_call_log` ( |
| | | `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主é®', |
| | | `trace_id` VARCHAR(64) NOT NULL COMMENT '䏿¬¡è°ç¨é¾è·¯ID', |
| | | `scene` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'è°ç¨åºæ¯', |
| | | `stream` TINYINT NOT NULL DEFAULT 0 COMMENT 'æ¯å¦æµå¼:1æ¯0å¦', |
| | | `attempt_no` INT NOT NULL DEFAULT 1 COMMENT 'ç¬¬å æ¬¡è·¯ç±å°è¯', |
| | | `route_id` BIGINT DEFAULT NULL COMMENT 'è·¯ç±ID', |
| | | `route_name` VARCHAR(128) DEFAULT NULL COMMENT 'è·¯ç±åç§°', |
| | | `base_url` VARCHAR(255) DEFAULT NULL COMMENT '请æ±APIå°å', |
| | | `model` VARCHAR(128) DEFAULT NULL COMMENT '模åå', |
| | | `success` TINYINT NOT NULL DEFAULT 0 COMMENT 'æ¯å¦æå:1æ¯0å¦', |
| | | `http_status` INT DEFAULT NULL COMMENT 'HTTPç¶æç ', |
| | | `latency_ms` BIGINT DEFAULT NULL COMMENT 'èæ¶ms', |
| | | `switch_mode` VARCHAR(32) DEFAULT NULL COMMENT 'åæ¢è§¦åç±»å:none/quota/error', |
| | | `request_content` MEDIUMTEXT COMMENT '请æ±å
容(æªæ)', |
| | | `response_content` MEDIUMTEXT COMMENT 'ååºå
容(æªæ)', |
| | | `error_type` VARCHAR(128) DEFAULT NULL COMMENT 'å¼å¸¸ç±»å', |
| | | `error_message` VARCHAR(1024) DEFAULT NULL COMMENT 'å¼å¸¸ä¿¡æ¯', |
| | | `extra` VARCHAR(512) DEFAULT NULL COMMENT 'æ©å±ä¿¡æ¯', |
| | | `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', |
| | | PRIMARY KEY (`id`), |
| | | KEY `idx_sys_llm_call_log_trace` (`trace_id`), |
| | | KEY `idx_sys_llm_call_log_scene_time` (`scene`, `create_time`), |
| | | KEY `idx_sys_llm_call_log_route_time` (`route_id`, `create_time`), |
| | | KEY `idx_sys_llm_call_log_success_time` (`success`, `create_time`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LLMè°ç¨æ¥å¿'; |
| New file |
| | |
| | | CREATE TABLE IF NOT EXISTS `sys_llm_route` ( |
| | | `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主é®', |
| | | `name` VARCHAR(64) NOT NULL COMMENT 'è·¯ç±åç§°', |
| | | `base_url` VARCHAR(255) DEFAULT NULL COMMENT 'LLM API Base URL', |
| | | `api_key` VARCHAR(512) DEFAULT NULL COMMENT 'API Key', |
| | | `model` VARCHAR(128) DEFAULT NULL COMMENT '模åå', |
| | | `thinking` TINYINT NOT NULL DEFAULT 0 COMMENT 'æ¯å¦å¼å¯æ·±åº¦æè:1æ¯0å¦', |
| | | `priority` INT NOT NULL DEFAULT 100 COMMENT 'ä¼å
级(è¶å°è¶ä¼å
)', |
| | | `status` TINYINT NOT NULL DEFAULT 1 COMMENT 'ç¶æ:1å¯ç¨0ç¦ç¨', |
| | | `switch_on_quota` TINYINT NOT NULL DEFAULT 1 COMMENT 'é¢åº¦èå°½æ¶æ¯å¦åæ¢', |
| | | `switch_on_error` TINYINT NOT NULL DEFAULT 1 COMMENT 'æ
éæ¶æ¯å¦åæ¢', |
| | | `cooldown_seconds` INT NOT NULL DEFAULT 300 COMMENT 'æ
éåå·å´ç§æ°', |
| | | `cooldown_until` DATETIME DEFAULT NULL COMMENT 'å·å´æªæ¢æ¶é´', |
| | | `fail_count` INT NOT NULL DEFAULT 0 COMMENT 'æ»å¤±è´¥æ¬¡æ°', |
| | | `success_count` INT NOT NULL DEFAULT 0 COMMENT 'æ»æåæ¬¡æ°', |
| | | `consecutive_fail_count` INT NOT NULL DEFAULT 0 COMMENT 'è¿ç»å¤±è´¥æ¬¡æ°', |
| | | `last_error` VARCHAR(512) DEFAULT NULL COMMENT 'æè¿é误æè¦', |
| | | `last_used_time` DATETIME DEFAULT NULL COMMENT 'æè¿æåè°ç¨æ¶é´', |
| | | `last_fail_time` DATETIME DEFAULT NULL COMMENT 'æè¿å¤±è´¥æ¶é´', |
| | | `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', |
| | | `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', |
| | | `memo` VARCHAR(255) DEFAULT NULL COMMENT '夿³¨', |
| | | PRIMARY KEY (`id`), |
| | | KEY `idx_sys_llm_route_status_priority` (`status`, `priority`), |
| | | KEY `idx_sys_llm_route_cooldown` (`cooldown_until`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LLM è·¯ç±é
ç½®(å¤API/夿¨¡å/å¤Keyèªå¨åæ¢)'; |
| | | |
| | | -- ç¤ºä¾æ°æ®ï¼æéä¿®æ¹åæ§è¡ï¼ |
| | | -- INSERT INTO sys_llm_route(name, base_url, api_key, model, thinking, priority, status, switch_on_quota, switch_on_error, cooldown_seconds) |
| | | -- VALUES ('主路ç±-gpt5', 'https://api.xiaomimimo.com/v1', 'sk-xxxx', 'gpt-5', 1, 10, 1, 1, 1, 300); |
| | | -- INSERT INTO sys_llm_route(name, base_url, api_key, model, thinking, priority, status, switch_on_quota, switch_on_error, cooldown_seconds) |
| | | -- VALUES ('å¤è·¯ç±-mimo', 'https://api.xiaomimimo.com/v1', 'sk-yyyy', 'mimo-v2-flash', 0, 20, 1, 1, 1, 300); |
| New file |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="UTF-8" /> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | | <title>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> |