mateclaw/mateclaw-server/src/main/java/vip/mate/system/service/SystemSettingService.java
matevip 9075abfef2 feat(channel + settings): wecom quote message parsing + sidecar config preservation
WeCom quoted-message context:
- Parse the body.quote field that arrives alongside any inbound message
  (text / image / voice / file / mixed sub-types). When a user long-
  presses a previous bot bubble and types a follow-up like "解释一下",
  the agent now sees both the user's new text and the referenced
  content as proper context — replies stay on topic instead of
  guessing what was being explained.
- Quoted images / files are downloaded through the same pipeline as
  inbound new media (magic-byte sniff, ZIP container peek for
  DOCX / XLSX / PPTX recovery, chat-uploads layout) so the vision
  sidecar and document tools can actually analyse what was quoted.
- Reading order in the assembled prompt: "[引用消息: ...]\n<user text>"
  first, then quoted media parts, then the user's own current-message
  media. Mixed quotes flatten into a space-joined summary.

Multimodal sidecar settings preservation:
- The bulk settings PUT used to unconditionally overwrite the vision /
  video sidecar model ids — null in a partial payload became "" in
  the DB, silently wiping the configured sidecar every time a user
  saved an unrelated settings page (System / Music / Image / etc.).
  Symptom: "I picked a vision model, saved a different settings tab,
  now the bot can't see images anymore."
- Bulk save now guards both keys with non-null checks, matching the
  pattern used for music / 3D / image / video / tts / stt blocks.
- A dedicated /settings/sidecar endpoint always writes both keys, so
  the sidecar UI can still explicitly clear via null without leaking
  the write-on-null semantics into every other settings save.
- Frontend sidecar card switches to the dedicated endpoint; other
  settings pages keep their existing partial-payload behaviour.
2026-05-10 11:38:35 +08:00

446 lines
23 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package vip.mate.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import vip.mate.system.model.SystemSettingEntity;
import vip.mate.system.model.SystemSettingsDTO;
import vip.mate.system.repository.SystemSettingMapper;
@Service
@RequiredArgsConstructor
public class SystemSettingService {
private static final String LANGUAGE_KEY = "language";
private static final String STREAM_ENABLED_KEY = "streamEnabled";
private static final String DEBUG_MODE_KEY = "debugMode";
private static final String STATEGRAPH_ENABLED_KEY = "stateGraphEnabled";
// 搜索服务配置 keys
private static final String SEARCH_ENABLED_KEY = "searchEnabled";
private static final String SEARCH_PROVIDER_KEY = "searchProvider";
private static final String SEARCH_FALLBACK_ENABLED_KEY = "searchFallbackEnabled";
private static final String SERPER_API_KEY_KEY = "serperApiKey";
private static final String SERPER_BASE_URL_KEY = "serperBaseUrl";
private static final String TAVILY_API_KEY_KEY = "tavilyApiKey";
private static final String TAVILY_BASE_URL_KEY = "tavilyBaseUrl";
private static final String DUCKDUCKGO_ENABLED_KEY = "duckduckgoEnabled";
private static final String SEARXNG_BASE_URL_KEY = "searxngBaseUrl";
// 视频生成配置 keys
private static final String VIDEO_ENABLED_KEY = "videoEnabled";
private static final String VIDEO_PROVIDER_KEY = "videoProvider";
private static final String VIDEO_FALLBACK_ENABLED_KEY = "videoFallbackEnabled";
// 图片生成配置 keys
private static final String IMAGE_ENABLED_KEY = "imageEnabled";
private static final String IMAGE_PROVIDER_KEY = "imageProvider";
private static final String IMAGE_FALLBACK_ENABLED_KEY = "imageFallbackEnabled";
// TTS 配置 keys
private static final String TTS_ENABLED_KEY = "ttsEnabled";
private static final String TTS_PROVIDER_KEY = "ttsProvider";
private static final String TTS_FALLBACK_ENABLED_KEY = "ttsFallbackEnabled";
private static final String TTS_AUTO_MODE_KEY = "ttsAutoMode";
private static final String TTS_DEFAULT_VOICE_KEY = "ttsDefaultVoice";
private static final String TTS_SPEED_KEY = "ttsSpeed";
// STT 配置 keys
private static final String STT_ENABLED_KEY = "sttEnabled";
private static final String STT_PROVIDER_KEY = "sttProvider";
private static final String STT_FALLBACK_ENABLED_KEY = "sttFallbackEnabled";
// Issue #76: let the OpenAI STT provider point at any OpenAI-compat endpoint.
private static final String STT_OPENAI_COMPAT_PROVIDER_ID_KEY = "sttOpenAiCompatProviderId";
private static final String STT_OPENAI_COMPAT_MODEL_KEY = "sttOpenAiCompatModel";
// 音乐生成配置 keys
private static final String MUSIC_ENABLED_KEY = "musicEnabled";
private static final String MUSIC_PROVIDER_KEY = "musicProvider";
private static final String MUSIC_FALLBACK_ENABLED_KEY = "musicFallbackEnabled";
// 3D 模型生成配置 keys
private static final String MODEL3D_ENABLED_KEY = "model3dEnabled";
private static final String MODEL3D_PROVIDER_KEY = "model3dProvider";
private static final String MODEL3D_FALLBACK_ENABLED_KEY = "model3dFallbackEnabled";
// Multimodal sidecar routing keys (id values; references mate_model_config.id)
private static final String DEFAULT_VISION_MODEL_KEY = "default.vision_model";
private static final String DEFAULT_VIDEO_MODEL_KEY = "default.video_model";
private static final String ZHIPU_API_KEY_KEY = "zhipuApiKey";
private static final String ZHIPU_BASE_URL_KEY = "zhipuBaseUrl";
private static final String FAL_API_KEY_KEY = "falApiKey";
private static final String KLING_ACCESS_KEY_KEY = "klingAccessKey";
private static final String KLING_SECRET_KEY_KEY = "klingSecretKey";
private static final String RUNWAY_API_KEY_KEY = "runwayApiKey";
private static final String MINIMAX_API_KEY_KEY = "minimaxApiKey";
private static final String MINIMAX_REGION_KEY = "minimaxRegion";
private final SystemSettingMapper systemSettingMapper;
/**
* Resolve the SearXNG base URL: DB value takes priority; fall back to the
* {@code SEARXNG_BASE_URL} environment variable so Docker deployments work
* out-of-the-box without manual configuration in the UI.
*/
private String resolveSearxngBaseUrl() {
String dbValue = getValue(SEARXNG_BASE_URL_KEY, "");
if (dbValue != null && !dbValue.isBlank()) return dbValue;
String envValue = System.getenv("SEARXNG_BASE_URL");
return (envValue != null && !envValue.isBlank()) ? envValue : "";
}
public SystemSettingsDTO getSettings() {
SystemSettingsDTO dto = new SystemSettingsDTO();
dto.setLanguage(getValue(LANGUAGE_KEY, "zh-CN"));
dto.setStreamEnabled(Boolean.parseBoolean(getValue(STREAM_ENABLED_KEY, "true")));
dto.setDebugMode(Boolean.parseBoolean(getValue(DEBUG_MODE_KEY, "false")));
dto.setStateGraphEnabled(Boolean.parseBoolean(getValue(STATEGRAPH_ENABLED_KEY, "false")));
// 搜索服务配置
dto.setSearchEnabled(Boolean.parseBoolean(getValue(SEARCH_ENABLED_KEY, "true")));
dto.setSearchProvider(getValue(SEARCH_PROVIDER_KEY, "serper"));
dto.setSearchFallbackEnabled(Boolean.parseBoolean(getValue(SEARCH_FALLBACK_ENABLED_KEY, "false")));
dto.setSerperBaseUrl(getValue(SERPER_BASE_URL_KEY, "https://google.serper.dev/search"));
dto.setTavilyBaseUrl(getValue(TAVILY_BASE_URL_KEY, "https://api.tavily.com/search"));
// Keyless provider 配置
dto.setDuckduckgoEnabled(Boolean.parseBoolean(getValue(DUCKDUCKGO_ENABLED_KEY, "true")));
dto.setSearxngBaseUrl(resolveSearxngBaseUrl());
// API Key 脱敏回显
dto.setSerperApiKeyMasked(maskApiKey(getValue(SERPER_API_KEY_KEY, "")));
dto.setTavilyApiKeyMasked(maskApiKey(getValue(TAVILY_API_KEY_KEY, "")));
// 视频生成配置
dto.setVideoEnabled(Boolean.parseBoolean(getValue(VIDEO_ENABLED_KEY, "false")));
dto.setVideoProvider(getValue(VIDEO_PROVIDER_KEY, "auto"));
dto.setVideoFallbackEnabled(Boolean.parseBoolean(getValue(VIDEO_FALLBACK_ENABLED_KEY, "true")));
dto.setZhipuBaseUrl(getValue(ZHIPU_BASE_URL_KEY, ""));
dto.setZhipuApiKeyMasked(maskApiKey(getValue(ZHIPU_API_KEY_KEY, "")));
dto.setFalApiKeyMasked(maskApiKey(getValue(FAL_API_KEY_KEY, "")));
dto.setKlingAccessKeyMasked(maskApiKey(getValue(KLING_ACCESS_KEY_KEY, "")));
dto.setKlingSecretKeyMasked(maskApiKey(getValue(KLING_SECRET_KEY_KEY, "")));
dto.setRunwayApiKeyMasked(maskApiKey(getValue(RUNWAY_API_KEY_KEY, "")));
dto.setMinimaxApiKeyMasked(maskApiKey(getValue(MINIMAX_API_KEY_KEY, "")));
dto.setMinimaxRegion(getValue(MINIMAX_REGION_KEY, "global"));
// 图片生成配置
dto.setImageEnabled(Boolean.parseBoolean(getValue(IMAGE_ENABLED_KEY, "false")));
dto.setImageProvider(getValue(IMAGE_PROVIDER_KEY, "auto"));
dto.setImageFallbackEnabled(Boolean.parseBoolean(getValue(IMAGE_FALLBACK_ENABLED_KEY, "true")));
// TTS 配置
dto.setTtsEnabled(Boolean.parseBoolean(getValue(TTS_ENABLED_KEY, "false")));
dto.setTtsProvider(getValue(TTS_PROVIDER_KEY, "auto"));
dto.setTtsFallbackEnabled(Boolean.parseBoolean(getValue(TTS_FALLBACK_ENABLED_KEY, "true")));
dto.setTtsAutoMode(getValue(TTS_AUTO_MODE_KEY, "off"));
dto.setTtsDefaultVoice(getValue(TTS_DEFAULT_VOICE_KEY, ""));
String speedStr = getValue(TTS_SPEED_KEY, "1.0");
try { dto.setTtsSpeed(Double.parseDouble(speedStr)); } catch (NumberFormatException e) { dto.setTtsSpeed(1.0); }
// STT 配置
dto.setSttEnabled(Boolean.parseBoolean(getValue(STT_ENABLED_KEY, "false")));
dto.setSttProvider(getValue(STT_PROVIDER_KEY, "auto"));
dto.setSttFallbackEnabled(Boolean.parseBoolean(getValue(STT_FALLBACK_ENABLED_KEY, "true")));
// Issue #76: default to "openai" so upgrades behave identically to the
// old hard-coded path; users can swap to any OpenAI-compat provider row.
dto.setSttOpenAiCompatProviderId(getValue(STT_OPENAI_COMPAT_PROVIDER_ID_KEY, "openai"));
dto.setSttOpenAiCompatModel(getValue(STT_OPENAI_COMPAT_MODEL_KEY, "whisper-1"));
// 音乐生成配置
dto.setMusicEnabled(Boolean.parseBoolean(getValue(MUSIC_ENABLED_KEY, "false")));
dto.setMusicProvider(getValue(MUSIC_PROVIDER_KEY, "auto"));
dto.setMusicFallbackEnabled(Boolean.parseBoolean(getValue(MUSIC_FALLBACK_ENABLED_KEY, "true")));
// 3D 模型生成配置
dto.setModel3dEnabled(Boolean.parseBoolean(getValue(MODEL3D_ENABLED_KEY, "false")));
dto.setModel3dProvider(getValue(MODEL3D_PROVIDER_KEY, "auto"));
dto.setModel3dFallbackEnabled(Boolean.parseBoolean(getValue(MODEL3D_FALLBACK_ENABLED_KEY, "true")));
// Multimodal sidecar routing — empty string means "not configured"
dto.setDefaultVisionModelId(parseIdOrNull(getValue(DEFAULT_VISION_MODEL_KEY, "")));
dto.setDefaultVideoModelId(parseIdOrNull(getValue(DEFAULT_VIDEO_MODEL_KEY, "")));
return dto;
}
private Long parseIdOrNull(String value) {
if (value == null || value.isBlank()) return null;
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
return null;
}
}
/**
* 获取全部配置(内部使用,包含明文 API Key— 供 VideoGenerationService 等后端服务使用
*/
public SystemSettingsDTO getAllSettings() {
SystemSettingsDTO dto = getSettings();
// 补充搜索明文 Key
dto.setSerperApiKey(getValue(SERPER_API_KEY_KEY, ""));
dto.setTavilyApiKey(getValue(TAVILY_API_KEY_KEY, ""));
// 补充视频明文 Key
dto.setZhipuApiKey(getValue(ZHIPU_API_KEY_KEY, ""));
dto.setFalApiKey(getValue(FAL_API_KEY_KEY, ""));
dto.setKlingAccessKey(getValue(KLING_ACCESS_KEY_KEY, ""));
dto.setKlingSecretKey(getValue(KLING_SECRET_KEY_KEY, ""));
dto.setRunwayApiKey(getValue(RUNWAY_API_KEY_KEY, ""));
dto.setMinimaxApiKey(getValue(MINIMAX_API_KEY_KEY, ""));
return dto;
}
/**
* 获取搜索配置(内部使用,包含明文 API Key
*/
public SystemSettingsDTO getSearchSettings() {
SystemSettingsDTO dto = new SystemSettingsDTO();
dto.setSearchEnabled(Boolean.parseBoolean(getValue(SEARCH_ENABLED_KEY, "true")));
dto.setSearchProvider(getValue(SEARCH_PROVIDER_KEY, "serper"));
dto.setSearchFallbackEnabled(Boolean.parseBoolean(getValue(SEARCH_FALLBACK_ENABLED_KEY, "false")));
dto.setSerperApiKey(getValue(SERPER_API_KEY_KEY, ""));
dto.setSerperBaseUrl(getValue(SERPER_BASE_URL_KEY, "https://google.serper.dev/search"));
dto.setTavilyApiKey(getValue(TAVILY_API_KEY_KEY, ""));
dto.setTavilyBaseUrl(getValue(TAVILY_BASE_URL_KEY, "https://api.tavily.com/search"));
dto.setDuckduckgoEnabled(Boolean.parseBoolean(getValue(DUCKDUCKGO_ENABLED_KEY, "true")));
dto.setSearxngBaseUrl(resolveSearxngBaseUrl());
return dto;
}
public SystemSettingsDTO saveSettings(SystemSettingsDTO dto) {
saveValue(LANGUAGE_KEY, dto.getLanguage(), "当前界面语言");
saveValue(STREAM_ENABLED_KEY, String.valueOf(Boolean.TRUE.equals(dto.getStreamEnabled())), "是否开启流式响应");
saveValue(DEBUG_MODE_KEY, String.valueOf(Boolean.TRUE.equals(dto.getDebugMode())), "是否开启调试模式");
saveValue(STATEGRAPH_ENABLED_KEY, String.valueOf(Boolean.TRUE.equals(dto.getStateGraphEnabled())), "启用 StateGraph 架构的 ReAct Agent");
// 搜索服务配置
if (dto.getSearchEnabled() != null) {
saveValue(SEARCH_ENABLED_KEY, String.valueOf(dto.getSearchEnabled()), "是否启用搜索功能");
}
if (dto.getSearchProvider() != null) {
saveValue(SEARCH_PROVIDER_KEY, dto.getSearchProvider(), "搜索服务提供商");
}
if (dto.getSearchFallbackEnabled() != null) {
saveValue(SEARCH_FALLBACK_ENABLED_KEY, String.valueOf(dto.getSearchFallbackEnabled()), "搜索失败时是否回退到备用提供商");
}
// API Key 仅在非空时保存(前端不回传明文,避免覆盖为空)
if (dto.getSerperApiKey() != null && !dto.getSerperApiKey().isBlank()) {
saveValue(SERPER_API_KEY_KEY, dto.getSerperApiKey(), "Serper API Key");
}
if (dto.getSerperBaseUrl() != null) {
saveValue(SERPER_BASE_URL_KEY, dto.getSerperBaseUrl(), "Serper 接口地址");
}
if (dto.getTavilyApiKey() != null && !dto.getTavilyApiKey().isBlank()) {
saveValue(TAVILY_API_KEY_KEY, dto.getTavilyApiKey(), "Tavily API Key");
}
if (dto.getTavilyBaseUrl() != null) {
saveValue(TAVILY_BASE_URL_KEY, dto.getTavilyBaseUrl(), "Tavily 接口地址");
}
// Keyless provider 配置
if (dto.getDuckduckgoEnabled() != null) {
saveValue(DUCKDUCKGO_ENABLED_KEY, String.valueOf(dto.getDuckduckgoEnabled()), "DuckDuckGo 免 Key 搜索(零配置兜底)");
}
if (dto.getSearxngBaseUrl() != null) {
saveValue(SEARXNG_BASE_URL_KEY, dto.getSearxngBaseUrl(), "SearXNG 实例地址");
}
// 视频生成配置
if (dto.getVideoEnabled() != null) {
saveValue(VIDEO_ENABLED_KEY, String.valueOf(dto.getVideoEnabled()), "是否启用视频生成");
}
if (dto.getVideoProvider() != null) {
saveValue(VIDEO_PROVIDER_KEY, dto.getVideoProvider(), "视频生成首选 Provider");
}
if (dto.getVideoFallbackEnabled() != null) {
saveValue(VIDEO_FALLBACK_ENABLED_KEY, String.valueOf(dto.getVideoFallbackEnabled()), "视频 Provider 级 Fallback");
}
if (dto.getZhipuApiKey() != null && !dto.getZhipuApiKey().isBlank()) {
saveValue(ZHIPU_API_KEY_KEY, dto.getZhipuApiKey(), "智谱 CogVideo API Key");
}
if (dto.getZhipuBaseUrl() != null) {
saveValue(ZHIPU_BASE_URL_KEY, dto.getZhipuBaseUrl(), "智谱 API Base URL");
}
if (dto.getFalApiKey() != null && !dto.getFalApiKey().isBlank()) {
saveValue(FAL_API_KEY_KEY, dto.getFalApiKey(), "fal.ai API Key");
}
if (dto.getKlingAccessKey() != null && !dto.getKlingAccessKey().isBlank()) {
saveValue(KLING_ACCESS_KEY_KEY, dto.getKlingAccessKey(), "快手可灵 Access Key");
}
if (dto.getKlingSecretKey() != null && !dto.getKlingSecretKey().isBlank()) {
saveValue(KLING_SECRET_KEY_KEY, dto.getKlingSecretKey(), "快手可灵 Secret Key");
}
if (dto.getRunwayApiKey() != null && !dto.getRunwayApiKey().isBlank()) {
saveValue(RUNWAY_API_KEY_KEY, dto.getRunwayApiKey(), "Runway API Key");
}
if (dto.getMinimaxApiKey() != null && !dto.getMinimaxApiKey().isBlank()) {
saveValue(MINIMAX_API_KEY_KEY, dto.getMinimaxApiKey(), "MiniMax API Key");
}
if (dto.getMinimaxRegion() != null && !dto.getMinimaxRegion().isBlank()) {
saveValue(MINIMAX_REGION_KEY, dto.getMinimaxRegion(), "MiniMax API 区域 (global / cn)");
}
// 图片生成配置
if (dto.getImageEnabled() != null) {
saveValue(IMAGE_ENABLED_KEY, String.valueOf(dto.getImageEnabled()), "是否启用图片生成");
}
if (dto.getImageProvider() != null) {
saveValue(IMAGE_PROVIDER_KEY, dto.getImageProvider(), "图片生成首选 Provider");
}
if (dto.getImageFallbackEnabled() != null) {
saveValue(IMAGE_FALLBACK_ENABLED_KEY, String.valueOf(dto.getImageFallbackEnabled()), "图片 Provider 级 Fallback");
}
// TTS 配置
if (dto.getTtsEnabled() != null) {
saveValue(TTS_ENABLED_KEY, String.valueOf(dto.getTtsEnabled()), "是否启用 TTS 语音合成");
}
if (dto.getTtsProvider() != null) {
saveValue(TTS_PROVIDER_KEY, dto.getTtsProvider(), "TTS 首选 Provider");
}
if (dto.getTtsFallbackEnabled() != null) {
saveValue(TTS_FALLBACK_ENABLED_KEY, String.valueOf(dto.getTtsFallbackEnabled()), "TTS Provider 级 Fallback");
}
if (dto.getTtsAutoMode() != null) {
saveValue(TTS_AUTO_MODE_KEY, dto.getTtsAutoMode(), "TTS 自动模式off/always");
}
if (dto.getTtsDefaultVoice() != null) {
saveValue(TTS_DEFAULT_VOICE_KEY, dto.getTtsDefaultVoice(), "TTS 默认语音");
}
if (dto.getTtsSpeed() != null) {
saveValue(TTS_SPEED_KEY, String.valueOf(dto.getTtsSpeed()), "TTS 默认语速");
}
// STT 配置
if (dto.getSttEnabled() != null) {
saveValue(STT_ENABLED_KEY, String.valueOf(dto.getSttEnabled()), "是否启用 STT 语音识别");
}
if (dto.getSttProvider() != null) {
saveValue(STT_PROVIDER_KEY, dto.getSttProvider(), "STT 首选 Provider");
}
if (dto.getSttFallbackEnabled() != null) {
saveValue(STT_FALLBACK_ENABLED_KEY, String.valueOf(dto.getSttFallbackEnabled()), "STT Provider 级 Fallback");
}
// Issue #76: persist the OpenAI-compatible STT routing target.
if (dto.getSttOpenAiCompatProviderId() != null) {
saveValue(STT_OPENAI_COMPAT_PROVIDER_ID_KEY, dto.getSttOpenAiCompatProviderId(),
"OpenAI-compat STT 凭证来源 provider id");
}
if (dto.getSttOpenAiCompatModel() != null) {
saveValue(STT_OPENAI_COMPAT_MODEL_KEY, dto.getSttOpenAiCompatModel(),
"OpenAI-compat STT 模型名");
}
// 音乐生成配置
if (dto.getMusicEnabled() != null) {
saveValue(MUSIC_ENABLED_KEY, String.valueOf(dto.getMusicEnabled()), "是否启用音乐生成");
}
if (dto.getMusicProvider() != null) {
saveValue(MUSIC_PROVIDER_KEY, dto.getMusicProvider(), "音乐生成首选 Provider");
}
if (dto.getMusicFallbackEnabled() != null) {
saveValue(MUSIC_FALLBACK_ENABLED_KEY, String.valueOf(dto.getMusicFallbackEnabled()), "音乐 Provider 级 Fallback");
}
// 3D 模型生成配置
if (dto.getModel3dEnabled() != null) {
saveValue(MODEL3D_ENABLED_KEY, String.valueOf(dto.getModel3dEnabled()), "是否启用 3D 模型生成");
}
if (dto.getModel3dProvider() != null) {
saveValue(MODEL3D_PROVIDER_KEY, dto.getModel3dProvider(), "3D 模型生成首选 Provider");
}
if (dto.getModel3dFallbackEnabled() != null) {
saveValue(MODEL3D_FALLBACK_ENABLED_KEY, String.valueOf(dto.getModel3dFallbackEnabled()), "3D Provider 级 Fallback");
}
// Multimodal sidecar routing — guarded with null check, matching the
// pattern used for music / 3D / image / video / tts / stt blocks
// above. The bulk PUT /settings is used by every settings page (System,
// Music, Video, Image, Stt, Tts, Model3D), each sending a partial
// payload that omits sidecar fields. Without this guard, saving any
// unrelated setting would silently write "" into the sidecar keys
// (Long? defaultVisionModelId deserializes to null when absent), which
// wiped users' configured vision/video models the moment they touched
// an unrelated settings page. Explicit clearing via the sidecar UI now
// routes through {@link #updateSidecarSettings} instead.
if (dto.getDefaultVisionModelId() != null) {
saveValue(DEFAULT_VISION_MODEL_KEY,
String.valueOf(dto.getDefaultVisionModelId()),
"Default vision-capable model id (mate_model_config.id) for sidecar routing");
}
if (dto.getDefaultVideoModelId() != null) {
saveValue(DEFAULT_VIDEO_MODEL_KEY,
String.valueOf(dto.getDefaultVideoModelId()),
"Default video-capable model id (mate_model_config.id) for sidecar routing");
}
return getSettings();
}
/**
* Dedicated update path for the multimodal sidecar configuration.
* <p>
* This endpoint is the ONLY place vision/video model ids can be written
* unconditionally — null is treated as an explicit "clear" and writes
* an empty string (parse-back returns null). The bulk
* {@link #saveSettings} now guards both keys with non-null checks so
* unrelated settings pages can't accidentally clobber sidecar config.
* <p>
* Both fields are always written so a single API call can independently
* assign / clear either modality.
*/
public SystemSettingsDTO updateSidecarSettings(Long visionModelId, Long videoModelId) {
saveValue(DEFAULT_VISION_MODEL_KEY,
visionModelId == null ? "" : String.valueOf(visionModelId),
"Default vision-capable model id (mate_model_config.id) for sidecar routing");
saveValue(DEFAULT_VIDEO_MODEL_KEY,
videoModelId == null ? "" : String.valueOf(videoModelId),
"Default video-capable model id (mate_model_config.id) for sidecar routing");
return getSettings();
}
public String getLanguage() {
return getValue(LANGUAGE_KEY, "zh-CN");
}
public String saveLanguage(String language) {
saveValue(LANGUAGE_KEY, language, "当前界面语言");
return getLanguage();
}
public boolean isStateGraphEnabled() {
return Boolean.parseBoolean(getValue(STATEGRAPH_ENABLED_KEY, "false"));
}
private String getValue(String key, String defaultValue) {
SystemSettingEntity entity = systemSettingMapper.selectOne(new LambdaQueryWrapper<SystemSettingEntity>()
.eq(SystemSettingEntity::getSettingKey, key)
.last("LIMIT 1"));
return entity != null && entity.getSettingValue() != null ? entity.getSettingValue() : defaultValue;
}
private String maskApiKey(String apiKey) {
if (apiKey == null || apiKey.isBlank()) {
return "";
}
if (apiKey.length() <= 4) {
return "****";
}
return "****" + apiKey.substring(apiKey.length() - 4);
}
private void saveValue(String key, String value, String description) {
SystemSettingEntity entity = systemSettingMapper.selectOne(new LambdaQueryWrapper<SystemSettingEntity>()
.eq(SystemSettingEntity::getSettingKey, key)
.last("LIMIT 1"));
if (entity == null) {
entity = new SystemSettingEntity();
entity.setSettingKey(key);
entity.setDescription(description);
entity.setSettingValue(value);
systemSettingMapper.insert(entity);
return;
}
entity.setSettingValue(value);
entity.setDescription(description);
systemSettingMapper.updateById(entity);
}
}