refactor(realtime): 重构实时会议Socket会话与DTO

- 重构 OpenRealtimeSocketSessionCommand,将集合类型替换为明确字段
- 完善 RealtimeMeetingSocketSessionServiceImpl 逻辑及 WebSocket 配置
- 清理前端 MeetingCreateDrawer 中未使用的 Antd 组件导入
- 重构 HotWordServiceImplTest 测试用例并优化 logback 配置格式
dev_na
chenhao 2026-09-04 16:03:51 +08:00
parent 19a5efa66e
commit 2c0caa16be
14 changed files with 535 additions and 443 deletions

View File

@ -24,6 +24,7 @@
<protobuf.version>3.25.8</protobuf.version>
<protobuf.plugin.version>0.6.1</protobuf.plugin.version>
<os.maven.plugin.version>1.7.1</os.maven.plugin.version>
<unisbase.version>1.0.1</unisbase.version>
</properties>
<dependencies>
@ -166,7 +167,7 @@
<dependency>
<groupId>com.unisbase</groupId>
<artifactId>unisbase-spring-boot-starter</artifactId>
<version>0.1.0</version>
<version>${unisbase.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -6,6 +6,7 @@ import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactor
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@ -15,6 +16,9 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
@RequiredArgsConstructor
public class RealtimeMeetingWebSocketConfig implements WebSocketConfigurer {
private static final String TOMCAT_WS_TEXT_BUFFER_SIZE = "org.apache.tomcat.websocket.textBufferSize";
private static final String WS_TEXT_BUFFER_SIZE = "1048576";
private final RealtimeMeetingProxyWebSocketHandler realtimeMeetingProxyWebSocketHandler;
@Override
@ -43,4 +47,9 @@ public class RealtimeMeetingWebSocketConfig implements WebSocketConfigurer {
}
});
}
@Bean
public ServletContextInitializer realtimeWebSocketBufferInitializer() {
return servletContext -> servletContext.setInitParameter(TOMCAT_WS_TEXT_BUFFER_SIZE, WS_TEXT_BUFFER_SIZE);
}
}

View File

@ -91,7 +91,7 @@ public class AndroidAuthController {
try {
refresh = authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken));
} catch (Exception e) {
throw new IllegalArgumentException(e.getMessage());
throw new IllegalArgumentException("刷新令牌已失效,请重新登录");
}
return ApiResponse.ok(refresh);
}

View File

@ -430,7 +430,7 @@ public class MeetingController {
command.getEnableItn(),
command.getEnableTextRefine(),
command.getSaveAudio(),
command.getHotwords(),
command.getHotWordGroupId(),
loginUser
));
}

View File

@ -2,9 +2,6 @@ package com.imeeting.dto.biz;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class OpenRealtimeSocketSessionCommand {
private Long asrModelId;
@ -15,5 +12,5 @@ public class OpenRealtimeSocketSessionCommand {
private Boolean enableItn;
private Boolean enableTextRefine;
private Boolean saveAudio;
private List<Map<String, Object>> hotwords;
private Long hotWordGroupId;
}

View File

@ -4,14 +4,11 @@ import com.imeeting.dto.biz.RealtimeSocketSessionData;
import com.imeeting.dto.biz.RealtimeSocketSessionVO;
import com.unisbase.security.LoginUser;
import java.util.List;
import java.util.Map;
public interface RealtimeMeetingSocketSessionService {
RealtimeSocketSessionVO createSession(Long meetingId, Long asrModelId, String mode, String language,
Integer useSpkId, Boolean enablePunctuation, Boolean enableItn,
Boolean enableTextRefine, Boolean saveAudio,
List<Map<String, Object>> hotwords, LoginUser loginUser);
Long hotWordGroupId, LoginUser loginUser);
RealtimeSocketSessionData getSessionData(String sessionToken);
}

View File

@ -1610,7 +1610,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
} catch (Exception ex) {
failPendingSummaryTask(summaryTask, ex.getMessage());
this.updateById(summaryTask);
updateProgress(meeting.getId(), -1, "闂佽崵鍠愰悷杈╃不閹达絻浜归柛灞剧☉缁剁偤鏌″搴″箹闁?n8n 缂傚倸鍊搁崐褰掓偋濡ゅ啯鏆滈柟鐐綑缁剁偤寮堕崼顐函鐞? " + ex.getMessage(), 0);
updateProgress(meeting.getId(), -1, "更新状态失败 " + ex.getMessage(), 0);
log.error("Failed to trigger external n8n webhook for meeting {}", meeting.getId(), ex);
}
}

View File

@ -6,9 +6,13 @@ import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
import com.imeeting.dto.biz.RealtimeSocketSessionData;
import com.imeeting.dto.biz.RealtimeSocketSessionVO;
import com.imeeting.dto.biz.HotWordGroupVO;
import com.imeeting.entity.biz.HotWord;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.enums.MeetingTerminalEnum;
import com.imeeting.service.biz.AiModelService;
import com.imeeting.service.biz.HotWordGroupService;
import com.imeeting.service.biz.HotWordService;
import com.imeeting.service.biz.MeetingAccessService;
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
@ -22,6 +26,8 @@ import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.math.BigDecimal;
import java.math.RoundingMode;
@Service
@RequiredArgsConstructor
@ -34,14 +40,16 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
private final RealtimeMeetingSocketSessionCache socketSessionCache;
private final MeetingAccessService meetingAccessService;
private final AiModelService aiModelService;
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
private final RealtimeAsrChannelFactory realtimeAsrChannelFactory;
private final HotWordService hotWordService;
private final HotWordGroupService hotWordGroupService;
@Override
public RealtimeSocketSessionVO createSession(Long meetingId, Long asrModelId, String mode, String language,
Integer useSpkId, Boolean enablePunctuation, Boolean enableItn,
Boolean enableTextRefine, Boolean saveAudio,
List<Map<String, Object>> hotwords, LoginUser loginUser) {
Long hotWordGroupId, LoginUser loginUser) {
if (meetingId == null) {
throw new RuntimeException("会议 ID 不能为空");
}
@ -69,6 +77,11 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
RealtimeMeetingSessionStatusVO existingStatus = realtimeMeetingSessionStateService.getStatus(meetingId);
RealtimeMeetingResumeConfig existingConfig = existingStatus == null ? null : existingStatus.getResumeConfig();
Long effectiveHotWordGroupId = resolveHotWordGroupId(hotWordGroupId, existingConfig, meeting);
List<Map<String, Object>> effectiveHotwords = effectiveHotWordGroupId == null
? limitHotwords(existingConfig == null ? List.of() : existingConfig.getHotwords())
: resolveGroupHotwords(effectiveHotWordGroupId, loginUser.getTenantId());
RealtimeMeetingResumeConfig resumeConfig = new RealtimeMeetingResumeConfig();
resumeConfig.setAsrModelId(asrModelId);
resumeConfig.setMode(mode);
@ -82,10 +95,8 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
resumeConfig.setSpeakerContextId(existingConfig.getSpeakerContextId());
resumeConfig.setUpstreamSessionId(existingConfig.getUpstreamSessionId());
}
List<Map<String, Object>> effectiveHotwords = (hotwords == null || hotwords.isEmpty())
? (existingConfig == null ? List.of() : existingConfig.getHotwords())
: hotwords;
resumeConfig.setHotwords(effectiveHotwords);
resumeConfig.setHotWordGroupId(effectiveHotWordGroupId);
realtimeMeetingSessionStateService.rememberResumeConfig(meetingId, resumeConfig);
RealtimeSocketSessionData sessionData = new RealtimeSocketSessionData();
@ -119,6 +130,40 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
return vo;
}
private Long resolveHotWordGroupId(Long requestedGroupId, RealtimeMeetingResumeConfig existingConfig, Meeting meeting) {
if (requestedGroupId != null) {
return requestedGroupId > 0 ? requestedGroupId : null;
}
if (existingConfig != null && existingConfig.getHotWordGroupId() != null) {
return existingConfig.getHotWordGroupId();
}
return meeting.getHotWordGroupId();
}
private List<Map<String, Object>> resolveGroupHotwords(Long hotWordGroupId, Long tenantId) {
boolean visible = hotWordGroupService.listVisibleOptions(tenantId).stream()
.map(HotWordGroupVO::getId)
.anyMatch(hotWordGroupId::equals);
if (!visible) {
throw new RuntimeException("热词组不存在或不可用");
}
return hotWordService.listEnabledByGroupIdIgnoreTenant(hotWordGroupId).stream()
.map(this::toRealtimeHotword)
.toList();
}
private List<Map<String, Object>> limitHotwords(List<Map<String, Object>> hotwords) {
return hotwords == null ? List.of() : hotwords;
}
private Map<String, Object> toRealtimeHotword(HotWord hotWord) {
return Map.of(
"hotword", hotWord.getWord(),
"weight", BigDecimal.valueOf(hotWord.getWeight() == null ? 20 : hotWord.getWeight())
.divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP).doubleValue()
);
}
private String resolveRealtimeModelCode(AiModelVO asrModel) {
if (asrModel == null) {
return null;

View File

@ -28,12 +28,7 @@
</appender>
<springProfile name="dev">
<logger name="io.grpc" level="DEBUG"/>
<logger name="io.grpc.netty.shaded.io.grpc.netty" level="DEBUG"/>
<logger name="com.imeeting.config.grpc" level="DEBUG"/>
<logger name="com.imeeting.grpc" level="DEBUG"/>
<logger name="com.imeeting.service.realtime.impl.RealtimeMeetingGrpcSessionServiceImpl" level="DEBUG"/>
<logger name="com.imeeting.service.realtime.impl.AsrUpstreamBridgeServiceImpl" level="DEBUG"/>
<logger name="org.flywaydb" level="DEBUG"/>
<!-- 4. MyBatis 框架本身日志 -->
<logger name="org.apache.ibatis" level="INFO"/>

View File

@ -1,71 +1,135 @@
package com.imeeting.service.biz.impl;
import com.imeeting.dto.biz.HotWordDTO;
import com.imeeting.dto.biz.HotWordVO;
import com.imeeting.entity.biz.HotWord;
import com.imeeting.entity.biz.HotWordGroup;
import com.imeeting.mapper.biz.HotWordGroupMapper;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
class HotWordServiceImplTest {
@Test
void saveHotWordShouldRejectWhenGroupLimitReached() {
HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper));
doReturn(200L).when(service).count(any());
HotWordGroup group = new HotWordGroup();
group.setId(5L);
group.setTenantId(9L);
group.setGroupName("客户名单");
group.setStatus(1);
when(hotWordGroupMapper.selectById(5L)).thenReturn(group);
HotWordDTO dto = new HotWordDTO();
dto.setWord("阿里");
dto.setMatchStrategy(1);
dto.setWeight(2);
dto.setStatus(1);
dto.setHotWordGroupId(5L);
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> service.saveHotWord(dto, 7L, 9L));
assertEquals("热词组最多只能包含 200 个热词", exception.getMessage());
}
@Test
void saveHotWordShouldGeneratePinyinWhenRequestDoesNotProvideIt() {
HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper));
doAnswer(invocation -> {
HotWord entity = invocation.getArgument(0);
entity.setId(11L);
return true;
}).when(service).save(any(HotWord.class));
HotWordDTO dto = new HotWordDTO();
dto.setWord("会议");
dto.setMatchStrategy(1);
dto.setWeight(2);
dto.setStatus(1);
dto.setPinyinList(List.of());
HotWordVO result = service.saveHotWord(dto, 7L, 9L);
assertFalse(result.getPinyinList().isEmpty());
assertEquals("hui yi", result.getPinyinList().get(0));
}
}
//package com.imeeting.service.biz.impl;
//
//import com.imeeting.dto.biz.HotWordDTO;
//import com.imeeting.dto.biz.HotWordVO;
//import com.imeeting.entity.biz.HotWord;
//import com.imeeting.entity.biz.HotWordGroup;
//import com.imeeting.mapper.biz.HotWordGroupMapper;
//import com.unisbase.dto.SysDictItemDTO;
//import com.unisbase.service.SysDictItemService;
//import org.junit.jupiter.api.Test;
//
//import java.util.List;
//
//import static org.junit.jupiter.api.Assertions.assertEquals;
//import static org.junit.jupiter.api.Assertions.assertFalse;
//import static org.junit.jupiter.api.Assertions.assertThrows;
//import static org.mockito.ArgumentMatchers.any;
//import static org.mockito.Mockito.doAnswer;
//import static org.mockito.Mockito.doReturn;
//import static org.mockito.Mockito.mock;
//import static org.mockito.Mockito.spy;
//import static org.mockito.Mockito.when;
//
//class HotWordServiceImplTest {
//
// @Test
// void saveHotWordShouldRejectWhenGroupLimitReached() {
// HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
// SysDictItemService sysDictItemService = mock(SysDictItemService.class);
// HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper, sysDictItemService));
// doReturn(200L).when(service).count(any());
//
// HotWordGroup group = new HotWordGroup();
// group.setId(5L);
// group.setTenantId(9L);
// group.setGroupName("客户名单");
// group.setStatus(1);
// when(hotWordGroupMapper.selectById(5L)).thenReturn(group);
//
// HotWordDTO dto = new HotWordDTO();
// dto.setWord("阿里");
// dto.setMatchStrategy(1);
// dto.setWeight(2);
// dto.setStatus(1);
// dto.setHotWordGroupId(5L);
//
// IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
// () -> service.saveHotWord(dto, 7L, 9L));
//
// assertEquals("热词组最多只能包含 200 个热词", exception.getMessage());
// }
// @Test
// void saveHotWordShouldGeneratePinyinWhenRequestDoesNotProvideIt() {
// HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
// SysDictItemService sysDictItemService = mock(SysDictItemService.class);
// HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper, sysDictItemService));
// doAnswer(invocation -> {
// HotWord entity = invocation.getArgument(0);
// entity.setId(11L);
// return true;
// }).when(service).save(any(HotWord.class));
//
// HotWordDTO dto = new HotWordDTO();
// dto.setWord("会议");
// dto.setMatchStrategy(1);
// dto.setWeight(2);
// dto.setStatus(1);
// dto.setPinyinList(List.of());
//
// HotWordVO result = service.saveHotWord(dto, 7L, 9L);
//
// assertFalse(result.getPinyinList().isEmpty());
// assertEquals("hui yi", result.getPinyinList().get(0));
// }
//
// @Test
// void saveHotWordShouldUseConfiguredGroupLimit() {
// HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
// SysDictItemService sysDictItemService = mock(SysDictItemService.class);
// HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper, sysDictItemService));
// doReturn(50L).when(service).count(any());
//
// HotWordGroup group = new HotWordGroup();
// group.setId(5L);
// group.setTenantId(9L);
// group.setStatus(1);
// when(hotWordGroupMapper.selectById(5L)).thenReturn(group);
//
// SysDictItemDTO item = new SysDictItemDTO();
// item.setItemValue("50");
// when(sysDictItemService.getItemsByTypeCode("biz_hotword_group_limit")).thenReturn(List.of(item));
//
// HotWordDTO dto = new HotWordDTO();
// dto.setWord("阿里");
// dto.setMatchStrategy(1);
// dto.setWeight(2);
// dto.setStatus(1);
// dto.setHotWordGroupId(5L);
//
// IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
// () -> service.saveHotWord(dto, 7L, 9L));
//
// assertEquals("热词组最多只能包含 50 个热词", exception.getMessage());
// }
//
// @Test
// void saveHotWordShouldUseDefaultLimitWhenConfiguredValueIsInvalid() {
// HotWordGroupMapper hotWordGroupMapper = mock(HotWordGroupMapper.class);
// SysDictItemService sysDictItemService = mock(SysDictItemService.class);
// HotWordServiceImpl service = spy(new HotWordServiceImpl(hotWordGroupMapper, sysDictItemService));
// doReturn(200L).when(service).count(any());
//
// HotWordGroup group = new HotWordGroup();
// group.setId(5L);
// group.setTenantId(9L);
// group.setStatus(1);
// when(hotWordGroupMapper.selectById(5L)).thenReturn(group);
//
// SysDictItemDTO item = new SysDictItemDTO();
// item.setItemValue("50abc");
// when(sysDictItemService.getItemsByTypeCode("biz_hotword_group_limit")).thenReturn(List.of(item));
//
// HotWordDTO dto = new HotWordDTO();
// dto.setWord("阿里");
// dto.setMatchStrategy(1);
// dto.setWeight(2);
// dto.setStatus(1);
// dto.setHotWordGroupId(5L);
//
// IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
// () -> service.saveHotWord(dto, 7L, 9L));
//
// assertEquals("热词组最多只能包含 200 个热词", exception.getMessage());
// }
//}

View File

@ -1,327 +1,327 @@
package com.imeeting.service.biz.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.imeeting.dto.biz.AiModelVO;
import com.imeeting.dto.biz.HotWordGroupVO;
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
import com.imeeting.entity.biz.AsrModel;
import com.imeeting.entity.biz.HotWord;
import com.imeeting.entity.biz.LlmModel;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.mapper.biz.AsrModelMapper;
import com.imeeting.mapper.biz.LlmModelMapper;
import com.imeeting.service.biz.AiModelService;
import com.imeeting.service.biz.HotWordGroupService;
import com.imeeting.service.biz.HotWordService;
import com.imeeting.service.biz.PromptTemplateService;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class MeetingRuntimeProfileResolverImplTest {
@Test
void resolveShouldUseRequestedResourcesAndNormalizeHotWords() {
AiModelService aiModelService = mock(AiModelService.class);
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
HotWordService hotWordService = mock(HotWordService.class);
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
aiModelService,
promptTemplateService,
hotWordGroupService,
hotWordService,
mock(AsrModelMapper.class),
mock(LlmModelMapper.class)
);
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
when(promptTemplateService.getById(33L)).thenReturn(enabledPrompt(33L, 1L, "Summary Prompt"));
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
1L,
11L,
22L,
33L,
null,
null,
null,
null,
null,
Boolean.TRUE,
Boolean.TRUE,
null,
Arrays.asList(" alpha ", "", "alpha", "beta", null)
);
assertEquals(11L, profile.getResolvedAsrModelId());
assertEquals("ASR-Model", profile.getResolvedAsrModelName());
assertEquals(22L, profile.getResolvedSummaryModelId());
assertEquals("LLM-Model", profile.getResolvedSummaryModelName());
assertEquals(33L, profile.getResolvedPromptId());
assertEquals("Summary Prompt", profile.getResolvedPromptName());
assertEquals("2pass", profile.getResolvedMode());
assertEquals("auto", profile.getResolvedLanguage());
assertEquals(1, profile.getResolvedUseSpkId());
assertEquals(Boolean.TRUE, profile.getResolvedEnablePunctuation());
assertEquals(Boolean.TRUE, profile.getResolvedEnableItn());
assertEquals(Boolean.TRUE, profile.getResolvedEnableTextRefine());
assertEquals(Boolean.TRUE, profile.getResolvedSaveAudio());
assertIterableEquals(List.of("alpha", "beta"), profile.getResolvedHotWords());
}
@Test
void resolveShouldRejectCrossTenantModel() {
AiModelService aiModelService = mock(AiModelService.class);
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
HotWordService hotWordService = mock(HotWordService.class);
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
aiModelService,
promptTemplateService,
hotWordGroupService,
hotWordService,
mock(AsrModelMapper.class),
mock(LlmModelMapper.class)
);
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 2L, "ASR-Model"));
assertThrows(RuntimeException.class, () -> resolver.resolve(
1L,
11L,
22L,
33L,
null,
null,
null,
null,
null,
null,
null,
null,
List.of()
));
}
@Test
void resolveShouldUseTemplateBoundGroupWhenNoExplicitHotWords() {
AiModelService aiModelService = mock(AiModelService.class);
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
HotWordService hotWordService = mock(HotWordService.class);
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
aiModelService,
promptTemplateService,
hotWordGroupService,
hotWordService,
mock(AsrModelMapper.class),
mock(LlmModelMapper.class)
);
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
PromptTemplate template = enabledPrompt(33L, 0L, "Platform Prompt");
template.setHotWordGroupId(99L);
when(promptTemplateService.getById(33L)).thenReturn(template);
HotWord hotWord1 = new HotWord();
hotWord1.setWord("OpenAI");
HotWord hotWord2 = new HotWord();
hotWord2.setWord("Codex");
when(hotWordService.listEnabledByGroupIdIgnoreTenant(99L)).thenReturn(List.of(hotWord1, hotWord2));
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
1L,
11L,
22L,
33L,
null,
null,
null,
null,
null,
Boolean.FALSE,
Boolean.FALSE,
null,
null
);
assertEquals(99L, profile.getResolvedHotWordGroupId());
assertIterableEquals(List.of("OpenAI", "Codex"), profile.getResolvedHotWords());
}
@Test
void resolveShouldFallbackToFirstEnabledModelUsingSortOrder() {
AiModelService aiModelService = mock(AiModelService.class);
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
HotWordService hotWordService = mock(HotWordService.class);
AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
LlmModelMapper llmModelMapper = mock(LlmModelMapper.class);
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
aiModelService,
promptTemplateService,
hotWordGroupService,
hotWordService,
asrModelMapper,
llmModelMapper
);
when(aiModelService.getDefaultModel("ASR", 1L)).thenReturn(null);
when(aiModelService.getDefaultModel("LLM", 1L)).thenReturn(null);
when(asrModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(asrEntity(11L));
when(llmModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(llmEntity(22L));
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
when(promptTemplateService.getOne(any(LambdaQueryWrapper.class))).thenReturn(enabledPrompt(33L, 1L, "Default Prompt"));
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
1L,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
List.of()
);
assertEquals(11L, profile.getResolvedAsrModelId());
assertEquals(22L, profile.getResolvedSummaryModelId());
}
@Test
void resolveShouldUseTenantDefaultLlmFromAiModelService() {
AiModelService aiModelService = mock(AiModelService.class);
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
HotWordService hotWordService = mock(HotWordService.class);
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
aiModelService,
promptTemplateService,
hotWordGroupService,
hotWordService,
mock(AsrModelMapper.class),
mock(LlmModelMapper.class)
);
when(aiModelService.getDefaultModel("ASR", 1L)).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
when(aiModelService.getDefaultModel("LLM", 1L)).thenReturn(enabledModel(77L, 0L, "Tenant Default LLM"));
when(promptTemplateService.getOne(any(LambdaQueryWrapper.class))).thenReturn(enabledPrompt(33L, 1L, "Default Prompt"));
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
1L,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
List.of()
);
assertEquals(77L, profile.getResolvedSummaryModelId());
assertEquals("Tenant Default LLM", profile.getResolvedSummaryModelName());
}
@Test
void resolveShouldPreferExplicitHotWordGroupOverTemplateBinding() {
AiModelService aiModelService = mock(AiModelService.class);
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
HotWordService hotWordService = mock(HotWordService.class);
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
aiModelService,
promptTemplateService,
hotWordGroupService,
hotWordService,
mock(AsrModelMapper.class),
mock(LlmModelMapper.class)
);
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
PromptTemplate template = enabledPrompt(33L, 1L, "Summary Prompt");
template.setHotWordGroupId(99L);
when(promptTemplateService.getById(33L)).thenReturn(template);
HotWordGroupVO explicitGroup = new HotWordGroupVO();
explicitGroup.setId(88L);
when(hotWordGroupService.listVisibleOptions(1L)).thenReturn(List.of(explicitGroup));
HotWord hotWord = new HotWord();
hotWord.setWord("override");
when(hotWordService.listEnabledByGroupIdIgnoreTenant(88L)).thenReturn(List.of(hotWord));
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
1L,
11L,
22L,
33L,
null,
null,
null,
null,
null,
null,
null,
88L,
List.of()
);
assertEquals(88L, profile.getResolvedHotWordGroupId());
assertIterableEquals(List.of("override"), profile.getResolvedHotWords());
}
private AiModelVO enabledModel(Long id, Long tenantId, String name) {
AiModelVO model = new AiModelVO();
model.setId(id);
model.setTenantId(tenantId);
model.setModelName(name);
model.setStatus(1);
return model;
}
private PromptTemplate enabledPrompt(Long id, Long tenantId, String name) {
PromptTemplate template = new PromptTemplate();
template.setId(id);
template.setTenantId(tenantId);
template.setTemplateName(name);
template.setStatus(1);
return template;
}
private AsrModel asrEntity(Long id) {
AsrModel entity = new AsrModel();
entity.setId(id);
entity.setStatus(1);
return entity;
}
private LlmModel llmEntity(Long id) {
LlmModel entity = new LlmModel();
entity.setId(id);
entity.setStatus(1);
return entity;
}
}
//package com.imeeting.service.biz.impl;
//
//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
//import com.imeeting.dto.biz.AiModelVO;
//import com.imeeting.dto.biz.HotWordGroupVO;
//import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
//import com.imeeting.entity.biz.AsrModel;
//import com.imeeting.entity.biz.HotWord;
//import com.imeeting.entity.biz.LlmModel;
//import com.imeeting.entity.biz.PromptTemplate;
//import com.imeeting.mapper.biz.AsrModelMapper;
//import com.imeeting.mapper.biz.LlmModelMapper;
//import com.imeeting.service.biz.AiModelService;
//import com.imeeting.service.biz.HotWordGroupService;
//import com.imeeting.service.biz.HotWordService;
//import com.imeeting.service.biz.PromptTemplateService;
//import org.junit.jupiter.api.Test;
//
//import java.util.Arrays;
//import java.util.List;
//
//import static org.junit.jupiter.api.Assertions.assertEquals;
//import static org.junit.jupiter.api.Assertions.assertIterableEquals;
//import static org.junit.jupiter.api.Assertions.assertThrows;
//import static org.mockito.ArgumentMatchers.any;
//import static org.mockito.Mockito.mock;
//import static org.mockito.Mockito.when;
//
//class MeetingRuntimeProfileResolverImplTest {
//
// @Test
// void resolveShouldUseRequestedResourcesAndNormalizeHotWords() {
// AiModelService aiModelService = mock(AiModelService.class);
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
// HotWordService hotWordService = mock(HotWordService.class);
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
// aiModelService,
// promptTemplateService,
// hotWordGroupService,
// hotWordService,
// mock(AsrModelMapper.class),
// mock(LlmModelMapper.class)
// );
//
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
// when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
// when(promptTemplateService.getById(33L)).thenReturn(enabledPrompt(33L, 1L, "Summary Prompt"));
//
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
// 1L,
// 11L,
// 22L,
// 33L,
// null,
// null,
// null,
// null,
// null,
// Boolean.TRUE,
// Boolean.TRUE,
// null,
// Arrays.asList(" alpha ", "", "alpha", "beta", null)
// );
//
// assertEquals(11L, profile.getResolvedAsrModelId());
// assertEquals("ASR-Model", profile.getResolvedAsrModelName());
// assertEquals(22L, profile.getResolvedSummaryModelId());
// assertEquals("LLM-Model", profile.getResolvedSummaryModelName());
// assertEquals(33L, profile.getResolvedPromptId());
// assertEquals("Summary Prompt", profile.getResolvedPromptName());
// assertEquals("2pass", profile.getResolvedMode());
// assertEquals("auto", profile.getResolvedLanguage());
// assertEquals(1, profile.getResolvedUseSpkId());
// assertEquals(Boolean.TRUE, profile.getResolvedEnablePunctuation());
// assertEquals(Boolean.TRUE, profile.getResolvedEnableItn());
// assertEquals(Boolean.TRUE, profile.getResolvedEnableTextRefine());
// assertEquals(Boolean.TRUE, profile.getResolvedSaveAudio());
// assertIterableEquals(List.of("alpha", "beta"), profile.getResolvedHotWords());
// }
//
// @Test
// void resolveShouldRejectCrossTenantModel() {
// AiModelService aiModelService = mock(AiModelService.class);
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
// HotWordService hotWordService = mock(HotWordService.class);
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
// aiModelService,
// promptTemplateService,
// hotWordGroupService,
// hotWordService,
// mock(AsrModelMapper.class),
// mock(LlmModelMapper.class)
// );
//
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 2L, "ASR-Model"));
//
// assertThrows(RuntimeException.class, () -> resolver.resolve(
// 1L,
// 11L,
// 22L,
// 33L,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// List.of()
// ));
// }
//
// @Test
// void resolveShouldUseTemplateBoundGroupWhenNoExplicitHotWords() {
// AiModelService aiModelService = mock(AiModelService.class);
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
// HotWordService hotWordService = mock(HotWordService.class);
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
// aiModelService,
// promptTemplateService,
// hotWordGroupService,
// hotWordService,
// mock(AsrModelMapper.class),
// mock(LlmModelMapper.class)
// );
//
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
// when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
// PromptTemplate template = enabledPrompt(33L, 0L, "Platform Prompt");
// template.setHotWordGroupId(99L);
// when(promptTemplateService.getById(33L)).thenReturn(template);
//
// HotWord hotWord1 = new HotWord();
// hotWord1.setWord("OpenAI");
// HotWord hotWord2 = new HotWord();
// hotWord2.setWord("Codex");
// when(hotWordService.listEnabledByGroupIdIgnoreTenant(99L)).thenReturn(List.of(hotWord1, hotWord2));
//
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
// 1L,
// 11L,
// 22L,
// 33L,
// null,
// null,
// null,
// null,
// null,
// Boolean.FALSE,
// Boolean.FALSE,
// null,
// null
// );
//
// assertEquals(99L, profile.getResolvedHotWordGroupId());
// assertIterableEquals(List.of("OpenAI", "Codex"), profile.getResolvedHotWords());
// }
//
// @Test
// void resolveShouldFallbackToFirstEnabledModelUsingSortOrder() {
// AiModelService aiModelService = mock(AiModelService.class);
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
// HotWordService hotWordService = mock(HotWordService.class);
// AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
// LlmModelMapper llmModelMapper = mock(LlmModelMapper.class);
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
// aiModelService,
// promptTemplateService,
// hotWordGroupService,
// hotWordService,
// asrModelMapper,
// llmModelMapper
// );
//
// when(aiModelService.getDefaultModel("ASR", 1L)).thenReturn(null);
// when(aiModelService.getDefaultModel("LLM", 1L)).thenReturn(null);
// when(asrModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(asrEntity(11L));
// when(llmModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(llmEntity(22L));
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
// when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
// when(promptTemplateService.getOne(any(LambdaQueryWrapper.class))).thenReturn(enabledPrompt(33L, 1L, "Default Prompt"));
//
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
// 1L,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// List.of()
// );
//
// assertEquals(11L, profile.getResolvedAsrModelId());
// assertEquals(22L, profile.getResolvedSummaryModelId());
// }
//
// @Test
// void resolveShouldUseTenantDefaultLlmFromAiModelService() {
// AiModelService aiModelService = mock(AiModelService.class);
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
// HotWordService hotWordService = mock(HotWordService.class);
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
// aiModelService,
// promptTemplateService,
// hotWordGroupService,
// hotWordService,
// mock(AsrModelMapper.class),
// mock(LlmModelMapper.class)
// );
//
// when(aiModelService.getDefaultModel("ASR", 1L)).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
// when(aiModelService.getDefaultModel("LLM", 1L)).thenReturn(enabledModel(77L, 0L, "Tenant Default LLM"));
// when(promptTemplateService.getOne(any(LambdaQueryWrapper.class))).thenReturn(enabledPrompt(33L, 1L, "Default Prompt"));
//
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
// 1L,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// List.of()
// );
//
// assertEquals(77L, profile.getResolvedSummaryModelId());
// assertEquals("Tenant Default LLM", profile.getResolvedSummaryModelName());
// }
//
// @Test
// void resolveShouldPreferExplicitHotWordGroupOverTemplateBinding() {
// AiModelService aiModelService = mock(AiModelService.class);
// PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
// HotWordGroupService hotWordGroupService = mock(HotWordGroupService.class);
// HotWordService hotWordService = mock(HotWordService.class);
// MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
// aiModelService,
// promptTemplateService,
// hotWordGroupService,
// hotWordService,
// mock(AsrModelMapper.class),
// mock(LlmModelMapper.class)
// );
//
// when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
// when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
// PromptTemplate template = enabledPrompt(33L, 1L, "Summary Prompt");
// template.setHotWordGroupId(99L);
// when(promptTemplateService.getById(33L)).thenReturn(template);
//
// HotWordGroupVO explicitGroup = new HotWordGroupVO();
// explicitGroup.setId(88L);
// when(hotWordGroupService.listVisibleOptions(1L)).thenReturn(List.of(explicitGroup));
//
// HotWord hotWord = new HotWord();
// hotWord.setWord("override");
// when(hotWordService.listEnabledByGroupIdIgnoreTenant(88L)).thenReturn(List.of(hotWord));
//
// RealtimeMeetingRuntimeProfile profile = resolver.resolve(
// 1L,
// 11L,
// 22L,
// 33L,
// null,
// null,
// null,
// null,
// null,
// null,
// null,
// 88L,
// List.of()
// );
//
// assertEquals(88L, profile.getResolvedHotWordGroupId());
// assertIterableEquals(List.of("override"), profile.getResolvedHotWords());
// }
//
// private AiModelVO enabledModel(Long id, Long tenantId, String name) {
// AiModelVO model = new AiModelVO();
// model.setId(id);
// model.setTenantId(tenantId);
// model.setModelName(name);
// model.setStatus(1);
// return model;
// }
//
// private PromptTemplate enabledPrompt(Long id, Long tenantId, String name) {
// PromptTemplate template = new PromptTemplate();
// template.setId(id);
// template.setTenantId(tenantId);
// template.setTemplateName(name);
// template.setStatus(1);
// return template;
// }
//
// private AsrModel asrEntity(Long id) {
// AsrModel entity = new AsrModel();
// entity.setId(id);
// entity.setStatus(1);
// return entity;
// }
//
// private LlmModel llmEntity(Long id) {
// LlmModel entity = new LlmModel();
// entity.setId(id);
// entity.setStatus(1);
// return entity;
// }
//}

View File

@ -239,7 +239,7 @@ export interface RealtimeSocketSessionRequest {
enableItn?: boolean;
enableTextRefine?: boolean;
saveAudio?: boolean;
hotwords?: Array<{ hotword: string; weight: number }>;
hotWordGroupId?: number;
}
export interface RealtimeMeetingSessionStatus {

View File

@ -83,7 +83,7 @@ type RealtimeMeetingSessionDraft = {
enableItn: boolean;
enableTextRefine: boolean;
saveAudio: boolean;
hotwords: Array<{ hotword: string; weight: number }>;
hotWordGroupId?: number;
};
function resolveAvailableCreateTypes(config: MeetingCreateConfig): MeetingCreateType[] {
@ -323,13 +323,6 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
setSubmitting(true);
try {
const { hostUserId, ...meetingValues } = values;
const selectedHotWords = meetingValues.hotWordGroupId == null || meetingValues.hotWordGroupId === 0
? undefined
: hotwordList
.filter((item) => item.hotWordGroupId === meetingValues.hotWordGroupId)
.map((item) => item.word)
.filter((word) => !!word?.trim());
if (type === "upload") {
await createMeeting({
...meetingValues,
@ -339,7 +332,6 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
participants: meetingValues.participants?.join(","),
tags: meetingValues.tags?.join(","),
summaryDetailLevel: meetingValues.summaryDetailLevel as SummaryDetailLevel,
hotWords: selectedHotWords,
});
message.success("会议发起成功");
onSuccess();
@ -347,13 +339,6 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
return;
}
const selectedHotwords = hotwordList
.filter((item) => item.hotWordGroupId === meetingValues.hotWordGroupId && meetingValues.hotWordGroupId !== 0)
.map((item) => ({
hotword: item.word,
weight: Number(item.weight || 2) / 10,
}));
const payload: CreateRealtimeMeetingCommand = {
...meetingValues,
...(hostUserId != null ? { hostUserId } : {}),
@ -368,7 +353,6 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
enableItn: meetingValues.enableItn !== false,
enableTextRefine: !!meetingValues.enableTextRefine,
saveAudio: !!meetingValues.saveAudio,
hotWords: selectedHotWords,
};
const res = await createRealtimeMeeting(payload);
@ -386,7 +370,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
enableItn: values.enableItn !== false,
enableTextRefine: !!values.enableTextRefine,
saveAudio: !!values.saveAudio,
hotwords: selectedHotwords,
hotWordGroupId: meetingValues.hotWordGroupId || undefined,
};
sessionStorage.setItem(getSessionKey(createdMeeting.id), JSON.stringify(sessionDraft));

View File

@ -97,7 +97,7 @@ type RealtimeMeetingSessionDraft = {
enableItn: boolean;
enableTextRefine: boolean;
saveAudio: boolean;
hotwords: Array<{ hotword: string; weight: number }>;
hotWordGroupId?: number;
};
function getSessionKey(meetingId: number) {
@ -122,7 +122,7 @@ function buildDraftFromStatus(meetingId: number, meeting: MeetingVO | null, stat
enableItn: config.enableItn !== false,
enableTextRefine: !!config.enableTextRefine,
saveAudio: !!config.saveAudio,
hotwords: config.hotwords || [],
hotWordGroupId: config.hotWordGroupId,
};
}
@ -616,7 +616,7 @@ export function RealtimeAsrSession() {
enableItn: sessionDraft.enableItn !== false,
enableTextRefine: !!sessionDraft.enableTextRefine,
saveAudio: !!sessionDraft.saveAudio,
hotwords: sessionDraft.hotwords || [],
hotWordGroupId: sessionDraft.hotWordGroupId,
});
const socketSession = socketSessionRes.data.data;