diff --git a/mateclaw-server/src/main/java/vip/mate/system/controller/SystemSettingController.java b/mateclaw-server/src/main/java/vip/mate/system/controller/SystemSettingController.java index 9f055aea..741f5b8d 100644 --- a/mateclaw-server/src/main/java/vip/mate/system/controller/SystemSettingController.java +++ b/mateclaw-server/src/main/java/vip/mate/system/controller/SystemSettingController.java @@ -41,8 +41,39 @@ public class SystemSettingController { return R.ok(systemSettingService.saveLanguage(request.getLanguage())); } + /** + * Dedicated endpoint for the multimodal sidecar configuration. + *
+ * Separated from the bulk {@code PUT /settings} because the bulk endpoint
+ * now guards sidecar keys with null checks (so unrelated settings pages
+ * can't clobber them via partial payloads). This endpoint always writes
+ * both fields, so passing {@code null} for either explicitly clears that
+ * sidecar — preserving the "clear via UI" UX without leaking the
+ * write-on-null semantics into every other settings save.
+ */
+ @Operation(summary = "更新多模态 sidecar 配置")
+ @PutMapping("/sidecar")
+ public R
+ * 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.
+ *
+ * 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,
- dto.getDefaultVisionModelId() == null ? "" : String.valueOf(dto.getDefaultVisionModelId()),
+ visionModelId == null ? "" : String.valueOf(visionModelId),
"Default vision-capable model id (mate_model_config.id) for sidecar routing");
saveValue(DEFAULT_VIDEO_MODEL_KEY,
- dto.getDefaultVideoModelId() == null ? "" : String.valueOf(dto.getDefaultVideoModelId()),
+ videoModelId == null ? "" : String.valueOf(videoModelId),
"Default video-capable model id (mate_model_config.id) for sidecar routing");
return getSettings();
}
diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts
index 6a33abab..6ff54a20 100644
--- a/mateclaw-ui/src/api/index.ts
+++ b/mateclaw-ui/src/api/index.ts
@@ -518,6 +518,13 @@ export const settingsApi = {
update: (data: any) => http.put('/settings', data),
getLanguage: () => http.get('/settings/language'),
updateLanguage: (language: string) => http.put('/settings/language', { language }),
+ // Dedicated endpoint for the multimodal sidecar configuration. The bulk
+ // /settings PUT now guards vision/video model ids with non-null checks so
+ // unrelated settings pages can't clobber them via partial payloads. This
+ // endpoint is the only path that writes those fields unconditionally —
+ // pass {defaultVisionModelId: null} here to explicitly clear a sidecar.
+ updateSidecar: (data: { defaultVisionModelId: number | null; defaultVideoModelId: number | null }) =>
+ http.put('/settings/sidecar', data),
}
// ==================== Workspace ====================
diff --git a/mateclaw-ui/src/views/Settings/Models/MultimodalSidecarSection.vue b/mateclaw-ui/src/views/Settings/Models/MultimodalSidecarSection.vue
index 7f9c7ed9..842afc45 100644
--- a/mateclaw-ui/src/views/Settings/Models/MultimodalSidecarSection.vue
+++ b/mateclaw-ui/src/views/Settings/Models/MultimodalSidecarSection.vue
@@ -144,7 +144,12 @@ async function loadAll() {
}
async function persistSettings(payload: { defaultVisionModelId: number | null; defaultVideoModelId: number | null }) {
- await settingsApi.update(payload)
+ // Use the dedicated sidecar endpoint so the bulk /settings PUT can keep
+ // guarding vision/video keys with non-null checks (preventing unrelated
+ // settings pages from clobbering this configuration via partial payloads).
+ // This endpoint always writes both keys, so passing null here means
+ // "explicit clear" which is the original UX of this card.
+ await settingsApi.updateSidecar(payload)
}
async function onSaveVision() {