diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/SocialMediaController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/SocialMediaController.java index d405500..1434441 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/SocialMediaController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/SocialMediaController.java @@ -157,13 +157,27 @@ public class SocialMediaController extends BaseController } /** - * 获取大模型接入配置(API 地址、密钥等,与 Jarvis 共用 Redis) + * 列出多套大模型接入配置及当前激活 id */ @GetMapping("/llm-config") - public AjaxResult getLlmConfig() + public AjaxResult listLlmProfiles() { try { - return socialMediaService.getLlmConfig(); + return socialMediaService.listLlmProfiles(); + } catch (Exception e) { + logger.error("列出大模型接入配置失败", e); + return AjaxResult.error("获取失败: " + e.getMessage()); + } + } + + /** + * 获取单套配置(编辑) + */ + @GetMapping("/llm-config/profiles/{id}") + public AjaxResult getLlmProfile(@PathVariable("id") String id) + { + try { + return socialMediaService.getLlmProfile(id); } catch (Exception e) { logger.error("获取大模型接入配置失败", e); return AjaxResult.error("获取失败: " + e.getMessage()); @@ -171,32 +185,92 @@ public class SocialMediaController extends BaseController } /** - * 保存大模型接入配置 + * 新增一套配置 */ - @Log(title = "保存大模型接入配置", businessType = BusinessType.UPDATE) - @PostMapping("/llm-config/save") - public AjaxResult saveLlmConfig(@RequestBody Map request) + @Log(title = "新增大模型接入配置", businessType = BusinessType.INSERT) + @PostMapping("/llm-config/profiles") + public AjaxResult createLlmProfile(@RequestBody Map request) { try { - return socialMediaService.saveLlmConfig(request); + return socialMediaService.createLlmProfile(request); } catch (Exception e) { - logger.error("保存大模型接入配置失败", e); + logger.error("新增大模型接入配置失败", e); return AjaxResult.error("保存失败: " + e.getMessage()); } } /** - * 清除大模型接入配置(Jarvis 使用 yml 默认 Ollama) + * 更新一套配置 */ - @Log(title = "重置大模型接入配置", businessType = BusinessType.DELETE) - @DeleteMapping("/llm-config") - public AjaxResult resetLlmConfig() + @Log(title = "更新大模型接入配置", businessType = BusinessType.UPDATE) + @PutMapping("/llm-config/profiles/{id}") + public AjaxResult updateLlmProfile(@PathVariable("id") String id, @RequestBody Map request) { try { - return socialMediaService.resetLlmConfig(); + return socialMediaService.updateLlmProfile(id, request); } catch (Exception e) { - logger.error("重置大模型接入配置失败", e); - return AjaxResult.error("重置失败: " + e.getMessage()); + logger.error("更新大模型接入配置失败", e); + return AjaxResult.error("保存失败: " + e.getMessage()); + } + } + + /** + * 删除一套配置 + */ + @Log(title = "删除大模型接入配置", businessType = BusinessType.DELETE) + @DeleteMapping("/llm-config/profiles/{id}") + public AjaxResult deleteLlmProfile(@PathVariable("id") String id) + { + try { + return socialMediaService.deleteLlmProfile(id); + } catch (Exception e) { + logger.error("删除大模型接入配置失败", e); + return AjaxResult.error("删除失败: " + e.getMessage()); + } + } + + /** + * 激活指定配置为当前使用 + */ + @Log(title = "激活大模型接入配置", businessType = BusinessType.UPDATE) + @PutMapping("/llm-config/active/{id}") + public AjaxResult setActiveLlmProfile(@PathVariable("id") String id) + { + try { + return socialMediaService.setActiveLlmProfile(id); + } catch (Exception e) { + logger.error("激活大模型接入配置失败", e); + return AjaxResult.error("操作失败: " + e.getMessage()); + } + } + + /** + * 取消激活(Jarvis 使用 yml 默认 Ollama) + */ + @Log(title = "取消激活大模型接入配置", businessType = BusinessType.UPDATE) + @DeleteMapping("/llm-config/active") + public AjaxResult clearActiveLlmProfile() + { + try { + return socialMediaService.clearActiveLlmProfile(); + } catch (Exception e) { + logger.error("取消激活大模型接入配置失败", e); + return AjaxResult.error("操作失败: " + e.getMessage()); + } + } + + /** + * 清空所有套及旧版单键 + */ + @Log(title = "清空大模型接入配置", businessType = BusinessType.DELETE) + @DeleteMapping("/llm-config") + public AjaxResult resetAllLlmConfig() + { + try { + return socialMediaService.resetAllLlmConfig(); + } catch (Exception e) { + logger.error("清空大模型接入配置失败", e); + return AjaxResult.error("清除失败: " + e.getMessage()); } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ISocialMediaService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ISocialMediaService.java index 2dba69e..365c204 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ISocialMediaService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ISocialMediaService.java @@ -73,19 +73,28 @@ public interface ISocialMediaService */ Map generateXianyuWenan(String title, String remark); - /** - * 获取社媒大模型接入配置(与 Jarvis 共用 Redis,供 OpenAI 兼容接口或 Ollama) - */ - com.ruoyi.common.core.domain.AjaxResult getLlmConfig(); + /** 列出多套大模型接入配置及当前激活的 id(与 Jarvis 共用 Redis) */ + com.ruoyi.common.core.domain.AjaxResult listLlmProfiles(); - /** - * 保存社媒大模型接入配置 - */ - com.ruoyi.common.core.domain.AjaxResult saveLlmConfig(Map request); + /** 获取单套配置(编辑用,密钥脱敏) */ + com.ruoyi.common.core.domain.AjaxResult getLlmProfile(String id); - /** - * 清除 Redis 中的接入配置,Jarvis 侧回退为 application.yml 默认 Ollama - */ - com.ruoyi.common.core.domain.AjaxResult resetLlmConfig(); + /** 新增一套配置 */ + com.ruoyi.common.core.domain.AjaxResult createLlmProfile(Map request); + + /** 更新一套配置 */ + com.ruoyi.common.core.domain.AjaxResult updateLlmProfile(String id, Map request); + + /** 删除一套配置 */ + com.ruoyi.common.core.domain.AjaxResult deleteLlmProfile(String id); + + /** 激活指定 id,Jarvis 调用时使用该套 */ + com.ruoyi.common.core.domain.AjaxResult setActiveLlmProfile(String id); + + /** 取消激活(Jarvis 回退 yml 默认 Ollama) */ + com.ruoyi.common.core.domain.AjaxResult clearActiveLlmProfile(); + + /** 清空所有套及旧版单键,Jarvis 回退默认 */ + com.ruoyi.common.core.domain.AjaxResult resetAllLlmConfig(); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java index 39af679..d5c48b6 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java @@ -13,8 +13,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -60,7 +64,9 @@ public class SocialMediaServiceImpl implements ISocialMediaService put("xianyu:title_clean_regex", "闲鱼文案·标题/型号清洗正则\n从标题与型号备注中移除营销敏感片段;须为 Java 正则,匹配到的内容会被删除"); }}; - /** 与 Jarvis_java SocialMediaLlmClient 使用相同 Redis 键 */ + /** 多套大模型 JSON 存储,与 Jarvis_java SocialMediaLlmClient 一致 */ + private static final String LLM_PROFILES_STORE_KEY = "social_media:llm:profiles_store"; + /** 旧版单套 Redis 键,首次读取时迁移进 profiles_store 后删除 */ private static final String LLM_KEY_MODE = "social_media:llm:mode"; private static final String LLM_KEY_BASE_URL = "social_media:llm:base_url"; private static final String LLM_KEY_API_KEY = "social_media:llm:api_key"; @@ -518,35 +524,257 @@ public class SocialMediaServiceImpl implements ISocialMediaService } @Override - public AjaxResult getLlmConfig() { + public AjaxResult listLlmProfiles() { try { Map data = new HashMap<>(); if (redisTemplate == null) { data.put("redisAvailable", false); - data.put("mode", LLM_MODE_OLLAMA); - data.put("baseUrl", ""); - data.put("model", ""); - data.put("hasApiKey", false); - data.put("apiKeyMasked", null); + data.put("activeId", null); + data.put("profiles", new ArrayList<>()); return AjaxResult.success(data); } data.put("redisAvailable", true); - String modeRaw = StringUtils.trim(redisTemplate.opsForValue().get(LLM_KEY_MODE)); - String mode = StringUtils.isEmpty(modeRaw) ? LLM_MODE_OLLAMA : modeRaw.toLowerCase(); - data.put("mode", mode); - data.put("baseUrl", StringUtils.trim(redisTemplate.opsForValue().get(LLM_KEY_BASE_URL))); - data.put("model", StringUtils.trim(redisTemplate.opsForValue().get(LLM_KEY_MODEL))); - String rawKey = redisTemplate.opsForValue().get(LLM_KEY_API_KEY); - boolean hasKey = StringUtils.isNotEmpty(StringUtils.trim(rawKey)); - data.put("hasApiKey", hasKey); - data.put("apiKeyMasked", maskLlmApiKey(rawKey)); + JSONObject store = loadOrMigrateLlmStore(); + JSONObject profiles = store.getJSONObject("profiles"); + if (profiles == null) { + profiles = new JSONObject(); + } + String activeId = store.getString("activeId"); + List> list = new ArrayList<>(); + for (String id : profiles.keySet()) { + JSONObject p = profiles.getJSONObject(id); + if (p == null) { + continue; + } + list.add(profileToSummaryMap(id, p, StringUtils.isNotEmpty(activeId) && activeId.equals(id))); + } + list.sort(Comparator.comparing(m -> String.valueOf(m.getOrDefault("name", "")), String.CASE_INSENSITIVE_ORDER)); + data.put("activeId", activeId); + data.put("profiles", list); return AjaxResult.success(data); } catch (Exception e) { - log.error("读取大模型接入配置失败", e); + log.error("列出大模型配置失败", e); return AjaxResult.error("读取失败: " + e.getMessage()); } } + @Override + public AjaxResult getLlmProfile(String id) { + try { + if (redisTemplate == null) { + return AjaxResult.error("Redis未配置"); + } + if (StringUtils.isEmpty(id)) { + return AjaxResult.error("配置 id 不能为空"); + } + JSONObject store = loadOrMigrateLlmStore(); + JSONObject profiles = store.getJSONObject("profiles"); + if (profiles == null || !profiles.containsKey(id)) { + return AjaxResult.error("配置不存在"); + } + JSONObject p = profiles.getJSONObject(id); + Map row = new HashMap<>(); + row.put("id", id); + row.put("name", StringUtils.trim(p.getString("name"))); + row.put("mode", normalizeLlmMode(p.getString("mode"))); + row.put("baseUrl", StringUtils.trim(p.getString("baseUrl"))); + row.put("model", StringUtils.trim(p.getString("model"))); + String rawKey = p.getString("apiKey"); + row.put("hasApiKey", StringUtils.isNotEmpty(StringUtils.trim(rawKey))); + row.put("apiKeyMasked", maskLlmApiKey(rawKey)); + return AjaxResult.success(row); + } catch (Exception e) { + log.error("获取大模型配置失败", e); + return AjaxResult.error("读取失败: " + e.getMessage()); + } + } + + @Override + public AjaxResult createLlmProfile(Map request) { + try { + if (redisTemplate == null) { + return AjaxResult.error("Redis未配置,无法保存"); + } + AjaxResult validation = validateLlmProfilePayload(request, true); + if (validation != null) { + return validation; + } + String name = request.get("name").toString().trim(); + String mode = request.get("mode").toString().trim().toLowerCase(); + String baseUrl = request.get("baseUrl") == null ? "" : request.get("baseUrl").toString().trim(); + String model = request.get("model") == null ? "" : request.get("model").toString().trim(); + + JSONObject store = loadOrMigrateLlmStore(); + JSONObject profiles = store.getJSONObject("profiles"); + if (profiles == null) { + profiles = new JSONObject(); + store.put("profiles", profiles); + } + String id = UUID.randomUUID().toString(); + JSONObject p = new JSONObject(); + p.put("name", name); + p.put("mode", mode); + p.put("baseUrl", baseUrl); + p.put("model", model); + String apiKey = ""; + Object apiKeyObj = request.get("apiKey"); + if (apiKeyObj != null && StringUtils.isNotEmpty(apiKeyObj.toString().trim())) { + apiKey = apiKeyObj.toString().trim(); + } + p.put("apiKey", apiKey); + profiles.put(id, p); + if (StringUtils.isEmpty(store.getString("activeId")) && profiles.size() == 1) { + store.put("activeId", id); + } + persistLlmStore(store); + log.info("新增大模型配置: id={}, name={}", id, name); + Map res = new HashMap<>(); + res.put("id", id); + return AjaxResult.success(res); + } catch (Exception e) { + log.error("新增大模型配置失败", e); + return AjaxResult.error("保存失败: " + e.getMessage()); + } + } + + @Override + public AjaxResult updateLlmProfile(String id, Map request) { + try { + if (redisTemplate == null) { + return AjaxResult.error("Redis未配置,无法保存"); + } + if (StringUtils.isEmpty(id)) { + return AjaxResult.error("配置 id 不能为空"); + } + AjaxResult validation = validateLlmProfilePayload(request, true); + if (validation != null) { + return validation; + } + JSONObject store = loadOrMigrateLlmStore(); + JSONObject profiles = store.getJSONObject("profiles"); + if (profiles == null || !profiles.containsKey(id)) { + return AjaxResult.error("配置不存在"); + } + JSONObject p = profiles.getJSONObject(id); + p.put("name", request.get("name").toString().trim()); + String mode = request.get("mode").toString().trim().toLowerCase(); + p.put("mode", mode); + String baseUrl = request.get("baseUrl") == null ? "" : request.get("baseUrl").toString().trim(); + String model = request.get("model") == null ? "" : request.get("model").toString().trim(); + p.put("baseUrl", baseUrl); + p.put("model", model); + + boolean clearApiKey = Boolean.TRUE.equals(request.get("clearApiKey")) + || "true".equalsIgnoreCase(String.valueOf(request.get("clearApiKey"))); + if (clearApiKey) { + p.put("apiKey", ""); + } else { + Object apiKeyObj = request.get("apiKey"); + if (apiKeyObj != null && StringUtils.isNotEmpty(apiKeyObj.toString().trim())) { + p.put("apiKey", apiKeyObj.toString().trim()); + } + } + persistLlmStore(store); + log.info("更新大模型配置: id={}", id); + return AjaxResult.success("保存成功"); + } catch (Exception e) { + log.error("更新大模型配置失败", e); + return AjaxResult.error("保存失败: " + e.getMessage()); + } + } + + @Override + public AjaxResult deleteLlmProfile(String id) { + try { + if (redisTemplate == null) { + return AjaxResult.error("Redis未配置"); + } + if (StringUtils.isEmpty(id)) { + return AjaxResult.error("配置 id 不能为空"); + } + JSONObject store = loadOrMigrateLlmStore(); + JSONObject profiles = store.getJSONObject("profiles"); + if (profiles == null || !profiles.containsKey(id)) { + return AjaxResult.error("配置不存在"); + } + profiles.remove(id); + String activeId = store.getString("activeId"); + if (id.equals(activeId)) { + if (profiles.isEmpty()) { + store.put("activeId", null); + } else { + store.put("activeId", profiles.keySet().iterator().next()); + } + } + persistLlmStore(store); + log.info("删除大模型配置: id={}", id); + return AjaxResult.success("删除成功"); + } catch (Exception e) { + log.error("删除大模型配置失败", e); + return AjaxResult.error("删除失败: " + e.getMessage()); + } + } + + @Override + public AjaxResult setActiveLlmProfile(String id) { + try { + if (redisTemplate == null) { + return AjaxResult.error("Redis未配置"); + } + if (StringUtils.isEmpty(id)) { + return AjaxResult.error("配置 id 不能为空"); + } + JSONObject store = loadOrMigrateLlmStore(); + JSONObject profiles = store.getJSONObject("profiles"); + if (profiles == null || !profiles.containsKey(id)) { + return AjaxResult.error("配置不存在"); + } + store.put("activeId", id); + persistLlmStore(store); + log.info("已激活大模型配置: id={}", id); + return AjaxResult.success("已切换为当前使用的配置"); + } catch (Exception e) { + log.error("激活大模型配置失败", e); + return AjaxResult.error("操作失败: " + e.getMessage()); + } + } + + @Override + public AjaxResult clearActiveLlmProfile() { + try { + if (redisTemplate == null) { + return AjaxResult.error("Redis未配置"); + } + JSONObject store = loadOrMigrateLlmStore(); + store.put("activeId", null); + persistLlmStore(store); + log.info("已取消激活大模型配置"); + return AjaxResult.success("已取消激活,Jarvis 将使用默认 Ollama"); + } catch (Exception e) { + log.error("取消激活大模型配置失败", e); + return AjaxResult.error("操作失败: " + e.getMessage()); + } + } + + @Override + public AjaxResult resetAllLlmConfig() { + try { + if (redisTemplate == null) { + return AjaxResult.error("Redis未配置"); + } + redisTemplate.delete(LLM_PROFILES_STORE_KEY); + redisTemplate.delete(LLM_KEY_MODE); + redisTemplate.delete(LLM_KEY_BASE_URL); + redisTemplate.delete(LLM_KEY_API_KEY); + redisTemplate.delete(LLM_KEY_MODEL); + log.info("已清空所有大模型接入配置"); + return AjaxResult.success("已清空,Jarvis 将使用默认 Ollama"); + } catch (Exception e) { + log.error("清空大模型配置失败", e); + return AjaxResult.error("清除失败: " + e.getMessage()); + } + } + private static String maskLlmApiKey(String secret) { if (StringUtils.isEmpty(StringUtils.trim(secret))) { return null; @@ -558,78 +786,114 @@ public class SocialMediaServiceImpl implements ISocialMediaService return "****" + s.substring(s.length() - 4); } - @Override - public AjaxResult saveLlmConfig(Map request) { - try { - if (redisTemplate == null) { - return AjaxResult.error("Redis未配置,无法保存"); - } - String mode = request.get("mode") == null ? "" : request.get("mode").toString().trim().toLowerCase(); - if (!LLM_MODE_OLLAMA.equals(mode) && !LLM_MODE_OPENAI.equals(mode)) { - return AjaxResult.error("接入方式只能是 ollama 或 openai"); - } - String baseUrl = request.get("baseUrl") == null ? "" : request.get("baseUrl").toString().trim(); - String model = request.get("model") == null ? "" : request.get("model").toString().trim(); - - if (LLM_MODE_OPENAI.equals(mode)) { - if (StringUtils.isEmpty(baseUrl)) { - return AjaxResult.error("OpenAI 兼容模式须填写 Chat Completions 完整地址(如 https://api.openai.com/v1/chat/completions)"); - } - if (StringUtils.isEmpty(model)) { - return AjaxResult.error("OpenAI 兼容模式须填写模型名称"); - } - } - - boolean clearApiKey = Boolean.TRUE.equals(request.get("clearApiKey")) - || "true".equalsIgnoreCase(String.valueOf(request.get("clearApiKey"))); - if (clearApiKey) { - redisTemplate.delete(LLM_KEY_API_KEY); - } - - Object apiKeyObj = request.get("apiKey"); - if (!clearApiKey && apiKeyObj != null) { - String newKey = apiKeyObj.toString().trim(); - if (StringUtils.isNotEmpty(newKey)) { - redisTemplate.opsForValue().set(LLM_KEY_API_KEY, newKey); - } - } - - redisTemplate.opsForValue().set(LLM_KEY_MODE, mode); - if (StringUtils.isEmpty(baseUrl)) { - redisTemplate.delete(LLM_KEY_BASE_URL); - } else { - redisTemplate.opsForValue().set(LLM_KEY_BASE_URL, baseUrl); - } - if (StringUtils.isEmpty(model)) { - redisTemplate.delete(LLM_KEY_MODEL); - } else { - redisTemplate.opsForValue().set(LLM_KEY_MODEL, model); - } - - log.info("保存大模型接入配置成功, mode={}", mode); - return AjaxResult.success("保存成功"); - } catch (Exception e) { - log.error("保存大模型接入配置失败", e); - return AjaxResult.error("保存失败: " + e.getMessage()); + private static String normalizeLlmMode(String mode) { + if (StringUtils.isEmpty(mode)) { + return LLM_MODE_OLLAMA; } + String m = mode.trim().toLowerCase(); + return LLM_MODE_OPENAI.equals(m) ? LLM_MODE_OPENAI : LLM_MODE_OLLAMA; } - @Override - public AjaxResult resetLlmConfig() { - try { - if (redisTemplate == null) { - return AjaxResult.error("Redis未配置"); - } - redisTemplate.delete(LLM_KEY_MODE); - redisTemplate.delete(LLM_KEY_BASE_URL); - redisTemplate.delete(LLM_KEY_API_KEY); - redisTemplate.delete(LLM_KEY_MODEL); - log.info("已清除大模型接入 Redis 配置"); - return AjaxResult.success("已恢复为 Jarvis 默认 Ollama 配置"); - } catch (Exception e) { - log.error("清除大模型接入配置失败", e); - return AjaxResult.error("清除失败: " + e.getMessage()); + /** + * @return 校验失败时返回 error AjaxResult,成功返回 null + */ + private AjaxResult validateLlmProfilePayload(Map request, boolean requireName) { + if (request == null) { + return AjaxResult.error("请求体不能为空"); } + Object nameObj = request.get("name"); + if (requireName && (nameObj == null || StringUtils.isEmpty(StringUtils.trim(nameObj.toString())))) { + return AjaxResult.error("请填写配置名称(用于区分多套)"); + } + String mode = request.get("mode") == null ? "" : request.get("mode").toString().trim().toLowerCase(); + if (!LLM_MODE_OLLAMA.equals(mode) && !LLM_MODE_OPENAI.equals(mode)) { + return AjaxResult.error("接入方式只能是 ollama 或 openai"); + } + String baseUrl = request.get("baseUrl") == null ? "" : request.get("baseUrl").toString().trim(); + String model = request.get("model") == null ? "" : request.get("model").toString().trim(); + if (LLM_MODE_OPENAI.equals(mode)) { + if (StringUtils.isEmpty(baseUrl)) { + return AjaxResult.error("OpenAI 兼容模式须填写 Chat Completions 完整地址"); + } + if (StringUtils.isEmpty(model)) { + return AjaxResult.error("OpenAI 兼容模式须填写模型名称"); + } + } + return null; + } + + private Map profileToSummaryMap(String id, JSONObject p, boolean active) { + Map m = new HashMap<>(); + m.put("id", id); + m.put("name", StringUtils.trim(p.getString("name"))); + m.put("mode", normalizeLlmMode(p.getString("mode"))); + m.put("baseUrl", StringUtils.trim(p.getString("baseUrl"))); + m.put("model", StringUtils.trim(p.getString("model"))); + String rawKey = p.getString("apiKey"); + m.put("hasApiKey", StringUtils.isNotEmpty(StringUtils.trim(rawKey))); + m.put("apiKeyMasked", maskLlmApiKey(rawKey)); + m.put("active", active); + return m; + } + + private void persistLlmStore(JSONObject store) { + redisTemplate.opsForValue().set(LLM_PROFILES_STORE_KEY, store.toJSONString()); + } + + /** + * 读取存储;若无 JSON 则尝试从旧版单键迁移并写入。 + */ + private JSONObject loadOrMigrateLlmStore() { + String raw = redisTemplate.opsForValue().get(LLM_PROFILES_STORE_KEY); + if (StringUtils.isNotEmpty(raw)) { + try { + JSONObject o = JSON.parseObject(raw); + if (o != null) { + if (!o.containsKey("profiles") || o.get("profiles") == null) { + o.put("profiles", new JSONObject()); + } + return o; + } + } catch (Exception e) { + log.warn("解析 LLM profiles_store 失败,将尝试迁移或重建", e); + } + } + return migrateLegacyLlmToStore(); + } + + private JSONObject migrateLegacyLlmToStore() { + JSONObject store = new JSONObject(); + JSONObject profiles = new JSONObject(); + String modeRaw = StringUtils.trim(redisTemplate.opsForValue().get(LLM_KEY_MODE)); + String baseUrl = StringUtils.trim(redisTemplate.opsForValue().get(LLM_KEY_BASE_URL)); + String model = StringUtils.trim(redisTemplate.opsForValue().get(LLM_KEY_MODEL)); + String apiKeyRaw = redisTemplate.opsForValue().get(LLM_KEY_API_KEY); + String apiKey = StringUtils.trim(apiKeyRaw); + + boolean anyLegacy = StringUtils.isNotEmpty(modeRaw) || StringUtils.isNotEmpty(baseUrl) + || StringUtils.isNotEmpty(model) || StringUtils.isNotEmpty(apiKey); + if (!anyLegacy) { + store.put("profiles", profiles); + store.put("activeId", null); + return store; + } + String id = "migrated-" + UUID.randomUUID().toString().replace("-", "").substring(0, 12); + JSONObject p = new JSONObject(); + p.put("name", "迁移自旧版单套配置"); + p.put("mode", StringUtils.isEmpty(modeRaw) ? LLM_MODE_OLLAMA : modeRaw.toLowerCase()); + p.put("baseUrl", baseUrl == null ? "" : baseUrl); + p.put("model", model == null ? "" : model); + p.put("apiKey", apiKey == null ? "" : apiKey); + profiles.put(id, p); + store.put("profiles", profiles); + store.put("activeId", id); + persistLlmStore(store); + redisTemplate.delete(LLM_KEY_MODE); + redisTemplate.delete(LLM_KEY_BASE_URL); + redisTemplate.delete(LLM_KEY_API_KEY); + redisTemplate.delete(LLM_KEY_MODEL); + log.info("已从旧版单键迁移大模型配置到多套存储"); + return store; } /**