refactor: 优化模型配置验证和测试逻辑
- 更新 `AiModelServiceImpl` 中的验证逻辑,改进错误信息 - 在 `pushAsrConfig` 方法中添加对空白 `apiKey` 的处理 - 添加单元测试以验证自定义 LLM 和 ASR 模型在没有 `apiKey` 时的行为 - 更新前端 `AiModels.tsx` 中的表单验证逻辑,移除 `apiKey` 的必填规则并添加警告提示dev_na
parent
6a08fb1a3b
commit
324e283f41
|
|
@ -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<String, Object> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LlmModel> 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<String, Object> 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<AsrModel> captor = ArgumentCaptor.forClass(AsrModel.class);
|
||||
verify(asrModelMapper, times(1)).insert(captor.capture());
|
||||
assertNull(captor.getValue().getApiKey());
|
||||
}
|
||||
|
||||
private void captureRequest(HttpExchange exchange,
|
||||
AtomicReference<String> requestPath,
|
||||
AtomicReference<String> authorization,
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<Form.Item
|
||||
name="apiKey"
|
||||
label="API Key"
|
||||
rules={isLocalProvider ? [{ required: true, message: "本地模型必须填写 API Key" }] : undefined}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
|
|
|
|||
Loading…
Reference in New Issue