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) {
|
private void validateModel(AiModelDTO dto) {
|
||||||
if (dto == null) {
|
if (dto == null) {
|
||||||
throw new RuntimeException("妯″瀷閰嶇疆涓嶈兘涓虹┖");
|
throw new RuntimeException("模型配置不能为空");
|
||||||
}
|
}
|
||||||
if ("custom".equals(normalizeProvider(dto.getProvider()))) {
|
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()))) {
|
if (TYPE_ASR.equals(normalizeType(dto.getModelType()))) {
|
||||||
Map<String, Object> mediaConfig = dto.getMediaConfig() == null ? Collections.emptyMap() : dto.getMediaConfig();
|
Map<String, Object> mediaConfig = dto.getMediaConfig() == null ? Collections.emptyMap() : dto.getMediaConfig();
|
||||||
if (readConfigString(mediaConfig.get("speakerModel")) == null) {
|
if (readConfigString(mediaConfig.get("speakerModel")) == null) {
|
||||||
|
|
@ -810,6 +807,10 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
|
|
||||||
private void pushAsrConfig(AsrModel entity) {
|
private void pushAsrConfig(AsrModel entity) {
|
||||||
if ("custom".equals(normalizeProvider(entity.getProvider()))) {
|
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);
|
updateLocalProfile(entity);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,15 @@ package com.imeeting.service.biz.impl;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.imeeting.dto.biz.AiModelDTO;
|
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.AsrModelMapper;
|
||||||
import com.imeeting.mapper.biz.LlmModelMapper;
|
import com.imeeting.mapper.biz.LlmModelMapper;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
@ -16,10 +19,17 @@ import java.lang.reflect.Field;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.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 {
|
class AiModelServiceImplTest {
|
||||||
|
|
||||||
|
|
@ -142,6 +152,67 @@ class AiModelServiceImplTest {
|
||||||
assertEquals(HttpClient.Version.HTTP_1_1, httpClient.version());
|
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,
|
private void captureRequest(HttpExchange exchange,
|
||||||
AtomicReference<String> requestPath,
|
AtomicReference<String> requestPath,
|
||||||
AtomicReference<String> authorization,
|
AtomicReference<String> authorization,
|
||||||
|
|
|
||||||
|
|
@ -302,17 +302,22 @@ const AiModels: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = await form.validateFields(["provider", "baseUrl", "apiKey"]);
|
const values = await form.validateFields(["provider", "baseUrl"]);
|
||||||
if (String(values.provider || "").toLowerCase() !== "custom") {
|
if (String(values.provider || "").toLowerCase() !== "custom") {
|
||||||
message.warning("仅本地模型支持连通性测试");
|
message.warning("仅本地模型支持连通性测试");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { apiKey } = form.getFieldsValue(["apiKey"]);
|
||||||
|
if (!apiKey) {
|
||||||
|
message.warning("请先填写 API Key 后再测试连接");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setConnectivityLoading(true);
|
setConnectivityLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await testLocalModelConnectivity({
|
const res = await testLocalModelConnectivity({
|
||||||
baseUrl: values.baseUrl,
|
baseUrl: values.baseUrl,
|
||||||
apiKey: values.apiKey,
|
apiKey,
|
||||||
});
|
});
|
||||||
const profile = (res as any)?.data?.data ?? (res as any)?.data ?? (res as any);
|
const profile = (res as any)?.data?.data ?? (res as any)?.data ?? (res as any);
|
||||||
applyLocalProfile(profile as AiLocalProfileVO, values.baseUrl);
|
applyLocalProfile(profile as AiLocalProfileVO, values.baseUrl);
|
||||||
|
|
@ -506,7 +511,6 @@ const AiModels: React.FC = () => {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
label="API Key"
|
label="API Key"
|
||||||
rules={isLocalProvider ? [{ required: true, message: "本地模型必须填写 API Key" }] : undefined}
|
|
||||||
>
|
>
|
||||||
<Input.Password />
|
<Input.Password />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue