| | |
| | | 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 |
| | |
| | | |
| | | private final LlmRouteConfigService llmRouteConfigService; |
| | | private final LlmRoutingService llmRoutingService; |
| | | private final ObjectMapper objectMapper; |
| | | |
| | | @GetMapping("/list/auth") |
| | | @ManagerAuth |
| | |
| | | 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(); |
| | | } |