From 324e283f41ba7e842e375978c3a8563d216d4a81 Mon Sep 17 00:00:00 2001 From: chenhao Date: Wed, 22 Apr 2026 09:53:34 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E9=85=8D=E7=BD=AE=E9=AA=8C=E8=AF=81=E5=92=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 `AiModelServiceImpl` 中的验证逻辑,改进错误信息 - 在 `pushAsrConfig` 方法中添加对空白 `apiKey` 的处理 - 添加单元测试以验证自定义 LLM 和 ASR 模型在没有 `apiKey` 时的行为 - 更新前端 `AiModels.tsx` 中的表单验证逻辑,移除 `apiKey` 的必填规则并添加警告提示 --- .../service/biz/impl/AiModelServiceImpl.java | 9 +-- .../biz/impl/AiModelServiceImplTest.java | 71 +++++++++++++++++++ frontend/src/pages/business/AiModels.tsx | 10 ++- 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java index 64a30cd..366e635 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java @@ -718,12 +718,9 @@ public class AiModelServiceImpl implements AiModelService { private void validateModel(AiModelDTO dto) { if (dto == null) { - throw new RuntimeException("妯″瀷閰嶇疆涓嶈兘涓虹┖"); + throw new RuntimeException("模型配置不能为空"); } if ("custom".equals(normalizeProvider(dto.getProvider()))) { - if (dto.getApiKey() == null || dto.getApiKey().isBlank()) { - throw new RuntimeException("鏈湴妯″瀷蹇呴』濉啓API Key"); - } if (TYPE_ASR.equals(normalizeType(dto.getModelType()))) { Map mediaConfig = dto.getMediaConfig() == null ? Collections.emptyMap() : dto.getMediaConfig(); if (readConfigString(mediaConfig.get("speakerModel")) == null) { @@ -810,6 +807,10 @@ public class AiModelServiceImpl implements AiModelService { private void pushAsrConfig(AsrModel entity) { if ("custom".equals(normalizeProvider(entity.getProvider()))) { + if (entity.getApiKey() == null || entity.getApiKey().isBlank()) { + log.info("Skip syncing local ASR profile because apiKey is blank, modelName={}", entity.getModelName()); + return; + } updateLocalProfile(entity); return; } diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/AiModelServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/AiModelServiceImplTest.java index 2dd75d3..bcc0044 100644 --- a/backend/src/test/java/com/imeeting/service/biz/impl/AiModelServiceImplTest.java +++ b/backend/src/test/java/com/imeeting/service/biz/impl/AiModelServiceImplTest.java @@ -3,12 +3,15 @@ package com.imeeting.service.biz.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.imeeting.dto.biz.AiModelDTO; +import com.imeeting.entity.biz.AsrModel; +import com.imeeting.entity.biz.LlmModel; import com.imeeting.mapper.biz.AsrModelMapper; import com.imeeting.mapper.biz.LlmModelMapper; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import java.io.IOException; import java.io.OutputStream; @@ -16,10 +19,17 @@ import java.lang.reflect.Field; import java.net.InetSocketAddress; import java.net.http.HttpClient; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; class AiModelServiceImplTest { @@ -142,6 +152,67 @@ class AiModelServiceImplTest { assertEquals(HttpClient.Version.HTTP_1_1, httpClient.version()); } + @Test + void saveModelShouldAllowCustomLlmWithoutApiKey() { + AsrModelMapper asrModelMapper = mock(AsrModelMapper.class); + LlmModelMapper llmModelMapper = mock(LlmModelMapper.class); + when(llmModelMapper.insert(any(LlmModel.class))).thenReturn(1); + + AiModelServiceImpl service = new AiModelServiceImpl( + objectMapper, + asrModelMapper, + llmModelMapper + ); + + AiModelDTO dto = new AiModelDTO(); + dto.setModelType("LLM"); + dto.setModelName("custom-llm"); + dto.setProvider("custom"); + dto.setBaseUrl("http://127.0.0.1:9000"); + dto.setApiPath("/v1/chat/completions"); + dto.setModelCode("llm-test"); + dto.setIsDefault(0); + dto.setStatus(1); + + service.saveModel(dto); + + ArgumentCaptor captor = ArgumentCaptor.forClass(LlmModel.class); + verify(llmModelMapper, times(1)).insert(captor.capture()); + assertNull(captor.getValue().getApiKey()); + } + + @Test + void saveModelShouldAllowCustomAsrWithoutApiKeyAndSkipSync() { + AsrModelMapper asrModelMapper = mock(AsrModelMapper.class); + LlmModelMapper llmModelMapper = mock(LlmModelMapper.class); + when(asrModelMapper.insert(any(AsrModel.class))).thenReturn(1); + + AiModelServiceImpl service = new AiModelServiceImpl( + objectMapper, + asrModelMapper, + llmModelMapper + ); + + AiModelDTO dto = new AiModelDTO(); + dto.setModelType("ASR"); + dto.setModelName("custom-asr"); + dto.setProvider("custom"); + dto.setBaseUrl("http://127.0.0.1:9001"); + dto.setModelCode("asr-test"); + Map mediaConfig = new HashMap<>(); + mediaConfig.put("speakerModel", "speaker-test"); + mediaConfig.put("svThreshold", 0.45); + dto.setMediaConfig(mediaConfig); + dto.setIsDefault(0); + dto.setStatus(1); + + service.saveModel(dto); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AsrModel.class); + verify(asrModelMapper, times(1)).insert(captor.capture()); + assertNull(captor.getValue().getApiKey()); + } + private void captureRequest(HttpExchange exchange, AtomicReference requestPath, AtomicReference authorization, diff --git a/frontend/src/pages/business/AiModels.tsx b/frontend/src/pages/business/AiModels.tsx index b314225..6ba84e9 100644 --- a/frontend/src/pages/business/AiModels.tsx +++ b/frontend/src/pages/business/AiModels.tsx @@ -302,17 +302,22 @@ const AiModels: React.FC = () => { return; } - const values = await form.validateFields(["provider", "baseUrl", "apiKey"]); + const values = await form.validateFields(["provider", "baseUrl"]); if (String(values.provider || "").toLowerCase() !== "custom") { message.warning("仅本地模型支持连通性测试"); return; } + const { apiKey } = form.getFieldsValue(["apiKey"]); + if (!apiKey) { + message.warning("请先填写 API Key 后再测试连接"); + return; + } setConnectivityLoading(true); try { const res = await testLocalModelConnectivity({ baseUrl: values.baseUrl, - apiKey: values.apiKey, + apiKey, }); const profile = (res as any)?.data?.data ?? (res as any)?.data ?? (res as any); applyLocalProfile(profile as AiLocalProfileVO, values.baseUrl); @@ -506,7 +511,6 @@ const AiModels: React.FC = () => {