From 71fcaf8628e10b57e848a8b4b633513df78ecd63 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期二, 03 三月 2026 13:13:17 +0800
Subject: [PATCH] #AI LLM路由
---
src/main/java/com/zy/ai/controller/LlmRouteConfigController.java | 199 +++++++++++++++++++++++++++++++++
src/main/webapp/views/ai/llm_config.html | 144 +++++++++++++++++++++++
2 files changed, 342 insertions(+), 1 deletions(-)
diff --git a/src/main/java/com/zy/ai/controller/LlmRouteConfigController.java b/src/main/java/com/zy/ai/controller/LlmRouteConfigController.java
index f43f408..43c7453 100644
--- a/src/main/java/com/zy/ai/controller/LlmRouteConfigController.java
+++ b/src/main/java/com/zy/ai/controller/LlmRouteConfigController.java
@@ -3,17 +3,24 @@
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
@@ -21,6 +28,7 @@
private final LlmRouteConfigService llmRouteConfigService;
private final LlmRoutingService llmRoutingService;
+ private final ObjectMapper objectMapper;
@GetMapping("/list/auth")
@ManagerAuth
@@ -125,6 +133,197 @@
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("瀵煎叆鏁版嵁涓虹┖鎴栨牸寮忎笉姝g‘锛屽繀椤诲寘鍚� 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();
}
diff --git a/src/main/webapp/views/ai/llm_config.html b/src/main/webapp/views/ai/llm_config.html
index e5c052e..c80314c 100644
--- a/src/main/webapp/views/ai/llm_config.html
+++ b/src/main/webapp/views/ai/llm_config.html
@@ -47,6 +47,13 @@
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;
@@ -260,8 +267,10 @@
<div class="sub">鏀寔澶欰PI銆佸妯″瀷銆佸Key锛岄搴﹁�楀敖鎴栨晠闅滆嚜鍔ㄥ垏鎹�</div>
</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>
@@ -289,6 +298,7 @@
</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">
@@ -597,6 +607,138 @@
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(
+ '璇烽�夋嫨瀵煎叆鏂瑰紡锛氳鐩栧鍏ヤ細鍏堟竻绌虹幇鏈夎矾鐢憋紱鐐瑰嚮鈥滃悎骞跺鍏モ�濆垯鎸塈D鏇存柊鎴栨柊澧炪��',
+ '瀵煎叆纭',
+ {
+ 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);
--
Gitblit v1.9.1