From 281ea53551d8033de7ad6b616f835a564bac0a82 Mon Sep 17 00:00:00 2001 From: matevip Date: Wed, 19 Aug 2026 23:16:28 -0400 Subject: [PATCH] fix(models): restore disabled custom providers --- .../llm/service/ModelProviderService.java | 32 ++++++++++++++++++- .../ModelProviderServiceEnableTest.java | 31 ++++++++++++++++++ mateclaw-ui/src/i18n/locales/en-US.ts | 6 ++-- mateclaw-ui/src/i18n/locales/zh-CN.ts | 6 ++-- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java index 9718ec38..900b03f9 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java @@ -175,7 +175,11 @@ public class ModelProviderService { throw new MateClawException("err.llm.provider_id_invalid", "Provider id 仅允许字母/数字及 . _ -(不允许斜杠或空格),首字符必须是字母或数字,长度 1-64: " + request.getId()); } - if (modelProviderMapper.selectById(request.getId()) != null) { + ModelProviderEntity existing = modelProviderMapper.selectById(request.getId()); + if (existing != null && canRestoreCustomProvider(existing)) { + return restoreCustomProvider(existing, request); + } + if (existing != null) { throw new MateClawException("err.llm.provider_exists", "Provider 已存在: " + request.getId()); } ModelProtocol protocol = ModelProtocol.resolve(request.getProtocol(), request.getChatModel()); @@ -211,6 +215,32 @@ public class ModelProviderService { return toProviderInfo(provider, modelConfigService.listModelsByProvider(request.getId())); } + private boolean canRestoreCustomProvider(ModelProviderEntity provider) { + return Boolean.TRUE.equals(provider.getIsCustom()) && !Boolean.TRUE.equals(provider.getEnabled()); + } + + private ProviderInfoDTO restoreCustomProvider(ModelProviderEntity provider, CreateCustomProviderRequest request) { + ModelProtocol protocol = ModelProtocol.resolve(request.getProtocol(), request.getChatModel()); + provider.setName(request.getName()); + provider.setApiKeyPrefix(request.getApiKeyPrefix()); + provider.setChatModel(protocol.getChatModelClass()); + provider.setBaseUrl(request.getDefaultBaseUrl()); + provider.setIsCustom(true); + provider.setIsLocal(false); + provider.setEnabled(true); + provider.setSupportModelDiscovery(protocol.supportsSelfConfiguredDiscovery()); + provider.setRequireApiKey(request.getRequireApiKey() == null || Boolean.TRUE.equals(request.getRequireApiKey())); + modelProviderMapper.updateById(provider); + if (request.getModels() != null) { + for (ModelInfoDTO model : request.getModels()) { + modelConfigService.addModelToProvider(request.getId(), model.getId(), model.getName(), false); + } + } + tryAutoActivateModel(request.getId(), provider); + eventPublisher.publishEvent(new ModelConfigChangedEvent("provider-enabled")); + return toProviderInfo(provider, modelConfigService.listModelsByProvider(request.getId())); + } + public void deleteCustomProvider(String providerId) { ModelProviderEntity provider = getProvider(providerId); if (!Boolean.TRUE.equals(provider.getIsCustom())) { diff --git a/mateclaw-server/src/test/java/vip/mate/llm/service/ModelProviderServiceEnableTest.java b/mateclaw-server/src/test/java/vip/mate/llm/service/ModelProviderServiceEnableTest.java index f6de6abf..28b4cf1d 100644 --- a/mateclaw-server/src/test/java/vip/mate/llm/service/ModelProviderServiceEnableTest.java +++ b/mateclaw-server/src/test/java/vip/mate/llm/service/ModelProviderServiceEnableTest.java @@ -14,6 +14,7 @@ import vip.mate.llm.failover.AvailableProviderPool; import vip.mate.llm.failover.ProviderHealthProperties; import vip.mate.llm.failover.ProviderHealthTracker; import vip.mate.llm.failover.ProviderInitProbe; +import vip.mate.llm.model.CreateCustomProviderRequest; import vip.mate.llm.model.EnableResult; import vip.mate.llm.model.ModelConfigEntity; import vip.mate.llm.model.ModelProviderEntity; @@ -112,6 +113,36 @@ class ModelProviderServiceEnableTest { verify(eventPublisher, never()).publishEvent(any()); } + @Test + @DisplayName("createCustomProvider with an existing disabled custom row re-enables and updates it") + void createCustomProviderReEnablesDisabledCustomProvider() { + ModelProviderEntity existing = providerEntity("custom-openai", false); + existing.setIsCustom(true); + existing.setName("Old name"); + when(providerMapper.selectById("custom-openai")).thenReturn(existing); + when(modelConfigService.listModelsByProvider("custom-openai")).thenReturn(new ArrayList<>()); + + CreateCustomProviderRequest request = new CreateCustomProviderRequest(); + request.setId("custom-openai"); + request.setName("New name"); + request.setDefaultBaseUrl("https://new.example.com/v1"); + request.setApiKeyPrefix("mk-"); + request.setProtocol("openai-compatible"); + request.setRequireApiKey(true); + + service.createCustomProvider(request); + + assertTrue(existing.getEnabled(), "disabled custom provider should be visible again"); + assertEquals("New name", existing.getName()); + assertEquals("https://new.example.com/v1", existing.getBaseUrl()); + assertEquals("mk-", existing.getApiKeyPrefix()); + verify(providerMapper).updateById(existing); + verify(providerMapper, never()).insert(any(ModelProviderEntity.class)); + ArgumentCaptor evtCap = ArgumentCaptor.forClass(ModelConfigChangedEvent.class); + verify(eventPublisher).publishEvent(evtCap.capture()); + assertEquals("provider-enabled", evtCap.getValue().reason()); + } + @Test @DisplayName("setEnabled(false) when provider's model is current default: auto-switches and reports new") void disableSwitchesDefault() { diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index decf4cf1..91932603 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -1137,15 +1137,15 @@ export default { livenessUnprobedTooltip: 'Checking availability after startup; will refresh shortly', livenessUnconfigured: 'Needs setup', // RFC-074 PR-2: enable / disable / drawer - enableProviderCta: '+ Enable Provider', + enableProviderCta: '+ Enable / Restore Provider', addCustomProvider: '+ Custom', enable: 'Enable', disable: 'Disable', alreadyEnabled: 'Enabled', disableConfirm: 'Disable provider "{name}"? It will disappear from the dropdown but its config is kept — you can re-enable any time.', defaultSwitchedToast: 'Default model switched to {provider} / {model}', - addProviderDrawerTitle: 'Add Provider', - addProviderDrawerSubtitle: 'Once enabled the provider appears in the model dropdown. Cloud providers still need an API key after enabling.', + addProviderDrawerTitle: 'Enable / Restore Provider', + addProviderDrawerSubtitle: 'Disabled providers can be restored here. Once enabled, the provider appears in the model dropdown. Cloud providers may still need an API key.', catalogEmpty: 'Catalog is empty', catalogSearchPlaceholder: 'Search providers…', catalogSearchEmpty: 'No providers match “{query}”', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 151bb61f..7aadf6c3 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -982,15 +982,15 @@ export default { livenessUnprobedTooltip: '启动后正在检测可用性,稍候自动更新', livenessUnconfigured: '需要配置', // RFC-074 PR-2: enable / disable / drawer - enableProviderCta: '+ 启用提供商', + enableProviderCta: '+ 启用/恢复提供商', addCustomProvider: '+ 自定义提供商', enable: '启用', disable: '禁用', alreadyEnabled: '已启用', disableConfirm: '确定禁用提供商"{name}"?它将从下拉中消失,但配置保留,可随时再启用。', defaultSwitchedToast: '已将默认模型切换到 {provider} / {model}', - addProviderDrawerTitle: '添加提供商', - addProviderDrawerSubtitle: '启用后会出现在模型下拉中。如果是云端提供商,启用后还需要填 API Key。', + addProviderDrawerTitle: '启用/恢复提供商', + addProviderDrawerSubtitle: '禁用后的提供商可在这里恢复;启用后会出现在模型下拉中。云端提供商可能还需要填 API Key。', catalogEmpty: '目录为空', catalogSearchPlaceholder: '搜索提供商…', catalogSearchEmpty: '没有匹配“{query}”的提供商',