diff --git a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java index 9d715d2..6f3c436 100644 --- a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java @@ -205,7 +205,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ null, null, null, - null, + true, null, null, List.of() @@ -281,7 +281,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ null, null, null, - null, + true, null, null, List.of() 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 836ad3d..bdfe5e8 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 @@ -49,6 +49,8 @@ public class AiModelServiceImpl implements AiModelService { private static final String MEDIA_TENCENT_APP_ID = "tencentAppId"; private static final String MEDIA_TENCENT_SECRET_ID = "tencentSecretId"; private static final String MEDIA_TENCENT_SECRET_KEY = "tencentSecretKey"; + private static final String MEDIA_TENCENT_OFFLINE_MODEL_CODE = "tencentOfflineModelCode"; + private static final String MEDIA_TENCENT_REALTIME_MODEL_CODE = "tencentRealtimeModelCode"; private static final int DEFAULT_SORT_ORDER = 0; private static final String DEFAULT_LLM_API_PATH = "/v1/chat/completions"; private static final String DEFAULT_ANTHROPIC_API_PATH = "/messages"; @@ -964,8 +966,11 @@ public class AiModelServiceImpl implements AiModelService { if (readConfigString(mediaConfig.get(MEDIA_TENCENT_SECRET_KEY)) == null) { throw new RuntimeException("腾讯 ASR 模型必须配置 mediaConfig.tencentSecretKey"); } - if (dto.getModelCode() == null || dto.getModelCode().isBlank()) { - throw new RuntimeException("腾讯 ASR 模型必须配置 modelCode"); + if (readConfigString(mediaConfig.get(MEDIA_TENCENT_OFFLINE_MODEL_CODE)) == null) { + throw new RuntimeException("腾讯 ASR 模型必须配置 mediaConfig.tencentOfflineModelCode"); + } + if (readConfigString(mediaConfig.get(MEDIA_TENCENT_REALTIME_MODEL_CODE)) == null) { + throw new RuntimeException("腾讯 ASR 模型必须配置 mediaConfig.tencentRealtimeModelCode"); } } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index a24b717..6e6e606 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -84,6 +84,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme private static final String MEDIA_TENCENT_APP_ID = "tencentAppId"; private static final String MEDIA_TENCENT_SECRET_ID = "tencentSecretId"; private static final String MEDIA_TENCENT_SECRET_KEY = "tencentSecretKey"; + private static final String MEDIA_TENCENT_OFFLINE_MODEL_CODE = "tencentOfflineModelCode"; private static final String TENCENT_ASR_REGION = "ap-guangzhou"; private final MeetingMapper meetingMapper; @@ -756,7 +757,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme protected Map buildTencentOfflineCreateRequest(Meeting meeting, AiTask taskRecord, AiModelVO asrModel) { Map req = new HashMap<>(); - req.put("engineModelType", asrModel.getModelCode()); + req.put("engineModelType", resolveTencentOfflineModelCode(asrModel)); req.put("channelNum", 1L); req.put("resTextFormat", 2L); req.put("sourceType", 0L); @@ -1646,6 +1647,15 @@ public class AiTaskServiceImpl extends ServiceImpl impleme transcript.setEndTime(endTime); } + private String resolveTencentOfflineModelCode(AiModelVO asrModel) { + Map mediaConfig = asrModel == null || asrModel.getMediaConfig() == null ? Map.of() : asrModel.getMediaConfig(); + String offlineModelCode = stringValue(mediaConfig.get(MEDIA_TENCENT_OFFLINE_MODEL_CODE)); + if (offlineModelCode != null && !offlineModelCode.isBlank()) { + return offlineModelCode.trim(); + } + return asrModel == null ? null : asrModel.getModelCode(); + } + private AiModelVO resolveAsrModelForRevision(AiTask asrTask) { if (asrTask == null || asrTask.getTaskConfig() == null) { return null; diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/RealtimeMeetingSocketSessionServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/RealtimeMeetingSocketSessionServiceImpl.java index b15fcf7..e7603e2 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/RealtimeMeetingSocketSessionServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/RealtimeMeetingSocketSessionServiceImpl.java @@ -26,6 +26,8 @@ import java.util.UUID; public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingSocketSessionService { private static final String WS_PATH = "/ws/meeting/realtime"; + private static final String TENCENT_PROVIDER = "tencent"; + private static final String MEDIA_TENCENT_REALTIME_MODEL_CODE = "tencentRealtimeModelCode"; private final RealtimeMeetingSocketSessionCache socketSessionCache; private final MeetingAccessService meetingAccessService; @@ -87,7 +89,7 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS sessionData.setAsrModelId(asrModelId); sessionData.setProvider(realtimeAsrChannelFactory.normalizeProvider(asrModel.getProvider())); sessionData.setTargetWsUrl(targetWsUrl); - sessionData.setModelCode(asrModel.getModelCode()); + sessionData.setModelCode(resolveRealtimeModelCode(asrModel)); sessionData.setMediaConfig(asrModel.getMediaConfig()); String sessionToken = UUID.randomUUID().toString().replace("-", ""); @@ -111,6 +113,25 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS return vo; } + private String resolveRealtimeModelCode(AiModelVO asrModel) { + if (asrModel == null) { + return null; + } + if (!TENCENT_PROVIDER.equalsIgnoreCase(asrModel.getProvider())) { + return asrModel.getModelCode(); + } + Map mediaConfig = asrModel.getMediaConfig(); + if (mediaConfig == null) { + return asrModel.getModelCode(); + } + Object realtimeModelCode = mediaConfig.get(MEDIA_TENCENT_REALTIME_MODEL_CODE); + if (realtimeModelCode == null) { + return asrModel.getModelCode(); + } + String value = String.valueOf(realtimeModelCode).trim(); + return value.isEmpty() ? asrModel.getModelCode() : value; + } + @Override public RealtimeSocketSessionData getSessionData(String sessionToken) { return socketSessionCache.get(sessionToken); 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 15aac02..4fa68a6 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 @@ -397,6 +397,31 @@ class AiModelServiceImplTest { assertEquals("腾讯 ASR 模型必须配置 mediaConfig.tencentSecretKey", ex.getMessage()); } + @Test + void saveModelShouldRejectTencentAsrWithoutRealtimeModelCode() { + AiModelServiceImpl service = new AiModelServiceImpl( + objectMapper, + mock(AsrModelMapper.class), + mock(LlmModelMapper.class) + ); + + AiModelDTO dto = new AiModelDTO(); + dto.setModelType("ASR"); + dto.setModelName("tencent-asr"); + dto.setProvider("tencent"); + dto.setIsDefault(0); + dto.setStatus(1); + dto.setMediaConfig(Map.of( + "tencentAppId", "app-id", + "tencentSecretId", "secret-id", + "tencentSecretKey", "secret-key", + "tencentOfflineModelCode", "16k_zh" + )); + + RuntimeException ex = assertThrows(RuntimeException.class, () -> service.saveModel(dto)); + assertEquals("腾讯 ASR 模型必须配置 mediaConfig.tencentRealtimeModelCode", ex.getMessage()); + } + @Test void saveModelShouldPersistTencentAsrWithoutBaseUrl() { AsrModelMapper asrModelMapper = mock(AsrModelMapper.class); @@ -412,13 +437,14 @@ class AiModelServiceImplTest { dto.setModelType("ASR"); dto.setModelName("tencent-asr"); dto.setProvider("tencent"); - dto.setModelCode("16k_zh"); dto.setIsDefault(0); dto.setStatus(1); dto.setMediaConfig(Map.of( "tencentAppId", "app-id", "tencentSecretId", "secret-id", - "tencentSecretKey", "secret-key" + "tencentSecretKey", "secret-key", + "tencentOfflineModelCode", "16k_zh", + "tencentRealtimeModelCode", "16k_zh_realtime" )); service.saveModel(dto); @@ -426,7 +452,9 @@ class AiModelServiceImplTest { ArgumentCaptor captor = ArgumentCaptor.forClass(AsrModel.class); verify(asrModelMapper, times(1)).insert(captor.capture()); assertEquals("tencent", captor.getValue().getProvider()); - assertEquals("16k_zh", captor.getValue().getModelCode()); + assertNull(captor.getValue().getModelCode()); + assertEquals("16k_zh", captor.getValue().getMediaConfig().get("tencentOfflineModelCode")); + assertEquals("16k_zh_realtime", captor.getValue().getMediaConfig().get("tencentRealtimeModelCode")); assertEquals("secret-key", captor.getValue().getMediaConfig().get("tencentSecretKey")); assertNull(captor.getValue().getBaseUrl()); } diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/AiTaskServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/AiTaskServiceImplTest.java index 112c888..352332c 100644 --- a/backend/src/test/java/com/imeeting/service/biz/impl/AiTaskServiceImplTest.java +++ b/backend/src/test/java/com/imeeting/service/biz/impl/AiTaskServiceImplTest.java @@ -133,11 +133,13 @@ class AiTaskServiceImplTest { ))); AiModelVO model = new AiModelVO(); - model.setModelCode("16k_zh"); + model.setModelCode("legacy-model-code"); model.setMediaConfig(Map.of( "tencentAppId", "123456", "tencentSecretId", "secret-id", - "tencentSecretKey", "secret-key" + "tencentSecretKey", "secret-key", + "tencentOfflineModelCode", "16k_zh", + "tencentRealtimeModelCode", "16k_zh_realtime" )); @SuppressWarnings("unchecked") @@ -211,11 +213,13 @@ class AiTaskServiceImplTest { AiModelVO model = new AiModelVO(); model.setProvider("tencent"); - model.setModelCode("16k_zh"); + model.setModelCode("legacy-model-code"); model.setMediaConfig(Map.of( "tencentAppId", "123456", "tencentSecretId", "secret-id", - "tencentSecretKey", "secret-key" + "tencentSecretKey", "secret-key", + "tencentOfflineModelCode", "16k_zh", + "tencentRealtimeModelCode", "16k_zh_realtime" )); TaskStatus doing = new TaskStatus(); diff --git a/frontend/src/pages/business/AiModels.tsx b/frontend/src/pages/business/AiModels.tsx index 6886584..c68cdf2 100644 --- a/frontend/src/pages/business/AiModels.tsx +++ b/frontend/src/pages/business/AiModels.tsx @@ -24,6 +24,7 @@ import { testLocalModelConnectivity, updateAiModel, } from "../../api/business/aimodel"; +import {getMeetingCreateConfig, type MeetingCreateConfig} from "../../api/business/meeting"; import AppPagination from "../../components/shared/AppPagination"; const { Option } = Select; @@ -31,6 +32,12 @@ const { Title } = Typography; type ModelType = "ASR" | "LLM"; +const DEFAULT_CREATE_CONFIG: MeetingCreateConfig = { + offlineEnabled: true, + realtimeEnabled: false, + offlineAudioMaxSizeMb: 1024, +}; + const PROVIDER_BASE_URL_MAP: Record = { openai: "https://api.openai.com", deepseek: "https://api.deepseek.com", @@ -64,6 +71,7 @@ const AiModels: React.FC = () => { const [connectivityLoading, setConnectivityLoading] = useState(false); const [remoteModels, setRemoteModels] = useState([]); const [speakerModels, setSpeakerModels] = useState([]); + const [createConfig, setCreateConfig] = useState(DEFAULT_CREATE_CONFIG); const modelNameAutoFilledRef = useRef(false); const localProfileLoadedRef = useRef(false); @@ -86,6 +94,20 @@ const AiModels: React.FC = () => { void fetchData(); }, [current, size, searchName, activeType]); + useEffect(() => { + getMeetingCreateConfig() + .then((res) => { + const config = (res as any)?.data?.data ?? (res as any); + setCreateConfig({ + ...DEFAULT_CREATE_CONFIG, + ...(config || {}), + }); + }) + .catch(() => { + setCreateConfig(DEFAULT_CREATE_CONFIG); + }); + }, []); + useEffect(() => { if (!drawerVisible || !provider) { return; @@ -137,6 +159,8 @@ const AiModels: React.FC = () => { const tencentAppId = record.mediaConfig?.tencentAppId; const tencentSecretId = record.mediaConfig?.tencentSecretId; const tencentSecretKey = record.mediaConfig?.tencentSecretKey; + const tencentOfflineModelCode = record.mediaConfig?.tencentOfflineModelCode || record.modelCode; + const tencentRealtimeModelCode = record.mediaConfig?.tencentRealtimeModelCode || record.modelCode; form.setFieldsValue({ ...record, modelType: record.modelType, @@ -145,6 +169,8 @@ const AiModels: React.FC = () => { tencentAppId, tencentSecretId, tencentSecretKey, + tencentOfflineModelCode, + tencentRealtimeModelCode, isDefaultChecked: record.isDefault === 1, statusChecked: record.status === 1, }); @@ -261,8 +287,8 @@ const AiModels: React.FC = () => { baseUrl: values.baseUrl, apiPath: values.apiPath, apiKey: values.apiKey, - modelCode: values.modelCode, - wsUrl: values.wsUrl, + modelCode: activeType === "ASR" && isTencentProvider ? values.tencentOfflineModelCode : values.modelCode, + wsUrl: activeType === "ASR" ? values.wsUrl : undefined, mediaConfig: activeType === "ASR" && isLocalProvider ? { @@ -274,6 +300,8 @@ const AiModels: React.FC = () => { tencentAppId: values.tencentAppId, tencentSecretId: values.tencentSecretId, tencentSecretKey: values.tencentSecretKey, + tencentOfflineModelCode: values.tencentOfflineModelCode, + tencentRealtimeModelCode: values.tencentRealtimeModelCode, } : undefined, temperature: values.temperature, @@ -567,13 +595,14 @@ const AiModels: React.FC = () => { - + + + + + + + + + + )}