feat: 增强会议配置解析和日志级别设置

- 在 `MeetingCommandServiceImpl` 中引入 `MeetingRuntimeProfileResolver`,解析并应用运行时配置
- 更新 `application-dev.yml` 和 `application.yml`,添加日志级别配置和新的数据库表
dev_na
chenhao 2026-04-10 09:14:00 +08:00
parent 1c82365e97
commit b9593324a5
20 changed files with 342 additions and 95 deletions

View File

@ -2,12 +2,24 @@ package com.imeeting.event;
public class MeetingCreatedEvent {
private final Long meetingId;
private final Long tenantId;
private final Long userId;
public MeetingCreatedEvent(Long meetingId) {
public MeetingCreatedEvent(Long meetingId, Long tenantId, Long userId) {
this.meetingId = meetingId;
this.tenantId = tenantId;
this.userId = userId;
}
public Long getMeetingId() {
return meetingId;
}
public Long getTenantId() {
return tenantId;
}
public Long getUserId() {
return userId;
}
}

View File

@ -15,6 +15,6 @@ public class MeetingTaskDispatchListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onMeetingCreated(MeetingCreatedEvent event) {
aiTaskService.dispatchTasks(event.getMeetingId());
aiTaskService.dispatchTasks(event.getMeetingId(), event.getTenantId(), event.getUserId());
}
}

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.imeeting.common.RedisKeys;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.mapper.biz.MeetingMapper;
import com.imeeting.support.TaskSecurityContextRunner;
import com.imeeting.service.biz.AiTaskService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -27,15 +28,18 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner {
private final MeetingMapper meetingMapper;
private final AiTaskService aiTaskService;
private final StringRedisTemplate redisTemplate;
private final TaskSecurityContextRunner taskSecurityContextRunner;
@Override
public void run(ApplicationArguments args) {
log.info("Starting meeting task self-healing check...");
// 1. 查询状态为 1(识别中) 或 2(总结中) 且未删除的会议
List<Meeting> pendingMeetings = meetingMapper.selectList(new LambdaQueryWrapper<Meeting>()
.in(Meeting::getStatus, 1, 2)
.eq(Meeting::getIsDeleted, 0));
List<Meeting> pendingMeetings = taskSecurityContextRunner.callAsPlatformAdmin(() ->
meetingMapper.selectList(new LambdaQueryWrapper<Meeting>()
.in(Meeting::getStatus, 1, 2)
.eq(Meeting::getIsDeleted, 0))
);
if (pendingMeetings.isEmpty()) {
log.info("No pending tasks found. Recovery check completed.");
@ -59,10 +63,10 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner {
// 3. 根据状态重新派发任务 (平滑拉起)
if (meeting.getStatus() == 1) {
log.info("Resuming ASR task for meeting {}", meeting.getId());
aiTaskService.dispatchTasks(meeting.getId());
aiTaskService.dispatchTasks(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
} else if (meeting.getStatus() == 2) {
log.info("Resuming Summary task for meeting {}", meeting.getId());
aiTaskService.dispatchSummaryTask(meeting.getId());
aiTaskService.dispatchSummaryTask(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
}
// 增加小延迟防止惊群效应
@ -80,7 +84,7 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner {
Meeting update = new Meeting();
update.setId(m.getId());
update.setStatus(4); // 失败
meetingMapper.updateById(update);
taskSecurityContextRunner.runAsTenantUser(m.getTenantId(), m.getCreatorId(), () -> meetingMapper.updateById(update));
// 同步 Redis 进度为失败
String progressKey = RedisKeys.meetingProgressKey(m.getId());

View File

@ -4,6 +4,6 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.imeeting.entity.biz.AiTask;
public interface AiTaskService extends IService<AiTask> {
void dispatchTasks(Long meetingId);
void dispatchSummaryTask(Long meetingId);
void dispatchTasks(Long meetingId, Long tenantId, Long userId);
void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId);
}

View File

@ -14,6 +14,7 @@ import com.imeeting.entity.biz.MeetingTranscript;
import com.imeeting.mapper.biz.AiTaskMapper;
import com.imeeting.mapper.biz.MeetingMapper;
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
import com.imeeting.support.TaskSecurityContextRunner;
import com.imeeting.service.biz.AiModelService;
import com.imeeting.service.biz.AiTaskService;
import com.imeeting.service.biz.HotWordService;
@ -57,6 +58,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
private final HotWordService hotWordService;
private final StringRedisTemplate redisTemplate;
private final MeetingSummaryFileService meetingSummaryFileService;
private final TaskSecurityContextRunner taskSecurityContextRunner;
@Value("${unisbase.app.server-base-url}")
private String serverBaseUrl;
@ -71,7 +73,11 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
@Override
@Async
public void dispatchTasks(Long meetingId) {
public void dispatchTasks(Long meetingId, Long tenantId, Long userId) {
taskSecurityContextRunner.runAsTenantUser(tenantId, userId, () -> doDispatchTasks(meetingId));
}
private void doDispatchTasks(Long meetingId) {
String lockKey = RedisKeys.meetingPollingLockKey(meetingId);
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(acquired)) {
@ -134,7 +140,11 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
@Override
@Async
public void dispatchSummaryTask(Long meetingId) {
public void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId) {
taskSecurityContextRunner.runAsTenantUser(tenantId, userId, () -> doDispatchSummaryTask(meetingId));
}
private void doDispatchSummaryTask(Long meetingId) {
Meeting meeting = meetingMapper.selectById(meetingId);
if (meeting == null) return;
try {
@ -413,7 +423,14 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
Long summaryModelId = Long.valueOf(taskRecord.getTaskConfig().get("summaryModelId").toString());
AiModelVO llmModel = aiModelService.getModelById(summaryModelId, "LLM");
if (llmModel == null) return;
if (llmModel == null) {
updateAiTaskFail(taskRecord, "LLM model not found: " + summaryModelId);
throw new RuntimeException("LLM模型配置不存在");
}
if (!Integer.valueOf(1).equals(llmModel.getStatus())) {
updateAiTaskFail(taskRecord, "LLM model disabled: " + summaryModelId);
throw new RuntimeException("LLM模型未启用");
}
String promptContent = taskRecord.getTaskConfig().get("promptContent") != null
? taskRecord.getTaskConfig().get("promptContent").toString() : "";

View File

@ -7,6 +7,7 @@ import com.imeeting.common.RedisKeys;
import com.imeeting.dto.biz.CreateMeetingCommand;
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
@ -19,6 +20,7 @@ import com.imeeting.entity.biz.MeetingTranscript;
import com.imeeting.service.biz.AiTaskService;
import com.imeeting.service.biz.HotWordService;
import com.imeeting.service.biz.MeetingCommandService;
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
import com.imeeting.service.biz.MeetingService;
import com.imeeting.service.biz.MeetingSummaryFileService;
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
@ -49,6 +51,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
private final com.imeeting.mapper.biz.MeetingTranscriptMapper transcriptMapper;
private final MeetingSummaryFileService meetingSummaryFileService;
private final MeetingDomainSupport meetingDomainSupport;
private final MeetingRuntimeProfileResolver meetingRuntimeProfileResolver;
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService;
private final StringRedisTemplate redisTemplate;
@ -57,6 +60,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
@Override
@Transactional(rollbackFor = Exception.class)
public MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName) {
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
@ -69,9 +73,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
asrTask.setStatus(0);
Map<String, Object> asrConfig = new HashMap<>();
asrConfig.put("asrModelId", command.getAsrModelId());
asrConfig.put("useSpkId", command.getUseSpkId() != null ? command.getUseSpkId() : 1);
asrConfig.put("enableTextRefine", command.getEnableTextRefine() != null ? command.getEnableTextRefine() : false);
asrConfig.put("asrModelId", runtimeProfile.getResolvedAsrModelId());
asrConfig.put("useSpkId", runtimeProfile.getResolvedUseSpkId());
asrConfig.put("enableTextRefine", runtimeProfile.getResolvedEnableTextRefine());
List<String> finalHotWords = command.getHotWords();
if (finalHotWords == null || finalHotWords.isEmpty()) {
@ -86,10 +90,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
asrTask.setTaskConfig(asrConfig);
aiTaskService.save(asrTask);
meetingDomainSupport.createSummaryTask(meeting.getId(), command.getSummaryModelId(), command.getPromptId());
meetingDomainSupport.createSummaryTask(meeting.getId(), runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId());
meeting.setAudioUrl(meetingDomainSupport.relocateAudioUrl(meeting.getId(), command.getAudioUrl()));
meetingService.updateById(meeting);
meetingDomainSupport.publishMeetingCreated(meeting.getId());
meetingDomainSupport.publishMeetingCreated(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
MeetingVO vo = new MeetingVO();
meetingDomainSupport.fillMeetingVO(meeting, vo, false);
@ -99,14 +103,15 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
@Override
@Transactional(rollbackFor = Exception.class)
public MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName) {
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
null, tenantId, creatorId, creatorName, hostUserId, hostName, 0);
meetingService.save(meeting);
meetingDomainSupport.createSummaryTask(meeting.getId(), command.getSummaryModelId(), command.getPromptId());
meetingDomainSupport.createSummaryTask(meeting.getId(), runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId());
realtimeMeetingSessionStateService.initSessionIfAbsent(meeting.getId(), tenantId, creatorId);
realtimeMeetingSessionStateService.rememberResumeConfig(meeting.getId(), buildRealtimeResumeConfig(command, tenantId));
realtimeMeetingSessionStateService.rememberResumeConfig(meeting.getId(), buildRealtimeResumeConfig(command, tenantId, runtimeProfile));
MeetingVO vo = new MeetingVO();
meetingDomainSupport.fillMeetingVO(meeting, vo, false);
@ -239,7 +244,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
prepareOfflineReprocessTasks(meetingId, currentStatus);
realtimeMeetingSessionStateService.clear(meetingId);
updateMeetingProgress(meetingId, 0, "正在转入离线音频识别流程...", 0);
aiTaskService.dispatchTasks(meetingId);
aiTaskService.dispatchTasks(meetingId, meeting.getTenantId(), meeting.getCreatorId());
return;
}
@ -266,7 +271,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
meeting.setStatus(2);
meetingService.updateById(meeting);
updateMeetingProgress(meetingId, 90, "正在生成会议总结...", 0);
aiTaskService.dispatchSummaryTask(meetingId);
aiTaskService.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
}
private void applyRealtimeAudioFinalizeResult(Meeting meeting, RealtimeMeetingAudioStorageService.FinalizeResult result) {
@ -450,18 +455,18 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
meetingDomainSupport.createSummaryTask(meetingId, summaryModelId, promptId);
meeting.setStatus(2);
meetingService.updateById(meeting);
dispatchSummaryTaskAfterCommit(meetingId);
dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
}
private void dispatchSummaryTaskAfterCommit(Long meetingId) {
private void dispatchSummaryTaskAfterCommit(Long meetingId, Long tenantId, Long userId) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
aiTaskService.dispatchSummaryTask(meetingId);
aiTaskService.dispatchSummaryTask(meetingId, tenantId, userId);
return;
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
aiTaskService.dispatchSummaryTask(meetingId);
aiTaskService.dispatchSummaryTask(meetingId, tenantId, userId);
}
});
}
@ -484,20 +489,56 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
}
}
private RealtimeMeetingResumeConfig buildRealtimeResumeConfig(CreateRealtimeMeetingCommand command, Long tenantId) {
private RealtimeMeetingResumeConfig buildRealtimeResumeConfig(CreateRealtimeMeetingCommand command,
Long tenantId,
RealtimeMeetingRuntimeProfile runtimeProfile) {
RealtimeMeetingResumeConfig resumeConfig = new RealtimeMeetingResumeConfig();
resumeConfig.setAsrModelId(command.getAsrModelId());
resumeConfig.setMode(command.getMode() == null || command.getMode().isBlank() ? "2pass" : command.getMode().trim());
resumeConfig.setLanguage(command.getLanguage() == null || command.getLanguage().isBlank() ? "auto" : command.getLanguage().trim());
resumeConfig.setUseSpkId(command.getUseSpkId() != null ? command.getUseSpkId() : 0);
resumeConfig.setEnablePunctuation(command.getEnablePunctuation() != null ? command.getEnablePunctuation() : Boolean.TRUE);
resumeConfig.setEnableItn(command.getEnableItn() != null ? command.getEnableItn() : Boolean.TRUE);
resumeConfig.setEnableTextRefine(Boolean.TRUE.equals(command.getEnableTextRefine()));
resumeConfig.setSaveAudio(Boolean.TRUE.equals(command.getSaveAudio()));
resumeConfig.setHotwords(resolveRealtimeHotwords(command.getHotWords(), tenantId));
resumeConfig.setAsrModelId(runtimeProfile.getResolvedAsrModelId());
resumeConfig.setMode(runtimeProfile.getResolvedMode());
resumeConfig.setLanguage(runtimeProfile.getResolvedLanguage());
resumeConfig.setUseSpkId(runtimeProfile.getResolvedUseSpkId());
resumeConfig.setEnablePunctuation(runtimeProfile.getResolvedEnablePunctuation());
resumeConfig.setEnableItn(runtimeProfile.getResolvedEnableItn());
resumeConfig.setEnableTextRefine(runtimeProfile.getResolvedEnableTextRefine());
resumeConfig.setSaveAudio(runtimeProfile.getResolvedSaveAudio());
resumeConfig.setHotwords(resolveRealtimeHotwords(runtimeProfile.getResolvedHotWords(), tenantId));
return resumeConfig;
}
private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateMeetingCommand command, Long tenantId) {
return meetingRuntimeProfileResolver.resolve(
tenantId,
command.getAsrModelId(),
command.getSummaryModelId(),
command.getPromptId(),
null,
null,
command.getUseSpkId(),
null,
null,
command.getEnableTextRefine(),
null,
command.getHotWords()
);
}
private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateRealtimeMeetingCommand command, Long tenantId) {
return meetingRuntimeProfileResolver.resolve(
tenantId,
command.getAsrModelId(),
command.getSummaryModelId(),
command.getPromptId(),
command.getMode(),
command.getLanguage(),
command.getUseSpkId(),
command.getEnablePunctuation(),
command.getEnableItn(),
command.getEnableTextRefine(),
command.getSaveAudio(),
command.getHotWords()
);
}
private List<Map<String, Object>> resolveRealtimeHotwords(List<String> selectedWords, Long tenantId) {
List<HotWord> tenantHotwords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
.eq(HotWord::getTenantId, tenantId)

View File

@ -86,8 +86,8 @@ public class MeetingDomainSupport {
aiTaskService.save(sumTask);
}
public void publishMeetingCreated(Long meetingId) {
eventPublisher.publishEvent(new MeetingCreatedEvent(meetingId));
public void publishMeetingCreated(Long meetingId, Long tenantId, Long userId) {
eventPublisher.publishEvent(new MeetingCreatedEvent(meetingId, tenantId, userId));
}
public String relocateAudioUrl(Long meetingId, String audioUrl) {

View File

@ -1,6 +1,14 @@
server:
port: ${SERVER_PORT:8081}
logging:
level:
root: info
io.grpc: debug
io.grpc.netty.shaded.io.grpc.netty: debug
com.imeeting.config.grpc: debug
com.imeeting.grpc: debug
com.imeeting.service.realtime.impl.RealtimeMeetingGrpcSessionServiceImpl: debug
com.imeeting.service.realtime.impl.AsrUpstreamBridgeServiceImpl: debug
spring:
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://127.0.0.1:5432/imeeting_db}

View File

@ -15,9 +15,9 @@ spring:
unisbase:
security:
jwt-secret: ${SECURITY_JWT_SECRET}
jwt-secret: ${SECURITY_JWT_SECRET:change-me-dev-jwt-secret-32bytes}
internal-auth:
secret: ${INTERNAL_AUTH_SECRET}
secret: ${INTERNAL_AUTH_SECRET:change-me-dev-internal-secret}
app:
server-base-url: ${APP_SERVER_BASE_URL}
upload-path: ${APP_UPLOAD_PATH}
server-base-url: ${APP_SERVER_BASE_URL:http://127.0.0.1:${server.port}}
upload-path: ${APP_UPLOAD_PATH:/data/imeeting/uploads/}

View File

@ -1,6 +1,10 @@
server:
port: ${SERVER_PORT:8080}
logging:
file:
path: ${LOG_PATH:./logs}
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
@ -34,6 +38,9 @@ unisbase:
- biz_ai_tasks
- biz_meeting_transcripts
- biz_speakers
- biz_llm_models
- biz_asr_models
- biz_prompt_templates
security:
enabled: true
mode: embedded
@ -43,6 +50,7 @@ unisbase:
- /actuator/health
- /api/static/**
- /ws/**
- /api/android/**
internal-auth:
enabled: true
header-name: X-Internal-Secret

View File

@ -4,10 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.dto.biz.CreateMeetingCommand;
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.service.biz.AiTaskService;
import com.imeeting.service.biz.HotWordService;
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
import com.imeeting.service.biz.MeetingService;
import com.imeeting.service.biz.MeetingSummaryFileService;
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
@ -22,6 +26,7 @@ import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
@ -200,6 +205,7 @@ class MeetingCommandServiceImplTest {
transcriptMapper,
mock(MeetingSummaryFileService.class),
meetingDomainSupport,
mockRuntimeProfileResolver(),
sessionStateService,
audioStorageService,
mock(StringRedisTemplate.class),
@ -212,7 +218,7 @@ class MeetingCommandServiceImplTest {
verify(meetingService).updateById(meetingCaptor.capture());
assertEquals("/api/static/meetings/202/source_audio.wav", meetingCaptor.getValue().getAudioUrl());
assertEquals(RealtimeMeetingAudioStorageService.STATUS_SUCCESS, meetingCaptor.getValue().getAudioSaveStatus());
verify(aiTaskService).dispatchSummaryTask(202L);
verify(aiTaskService).dispatchSummaryTask(202L, null, null);
}
@Test
@ -237,6 +243,7 @@ class MeetingCommandServiceImplTest {
transcriptMapper,
mock(MeetingSummaryFileService.class),
meetingDomainSupport,
mockRuntimeProfileResolver(),
mock(RealtimeMeetingSessionStateService.class),
audioStorageService,
mock(StringRedisTemplate.class),
@ -262,6 +269,7 @@ class MeetingCommandServiceImplTest {
transcriptMapper,
mock(MeetingSummaryFileService.class),
meetingDomainSupport,
mockRuntimeProfileResolver(),
sessionStateService,
mock(RealtimeMeetingAudioStorageService.class),
mock(StringRedisTemplate.class),
@ -300,6 +308,7 @@ class MeetingCommandServiceImplTest {
mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class),
mock(MeetingSummaryFileService.class),
meetingDomainSupport,
mockRuntimeProfileResolver(),
mock(RealtimeMeetingSessionStateService.class),
mock(RealtimeMeetingAudioStorageService.class),
mock(StringRedisTemplate.class),
@ -313,16 +322,150 @@ class MeetingCommandServiceImplTest {
verify(meetingDomainSupport).createSummaryTask(301L, 22L, 33L);
assertEquals(2, meeting.getStatus());
verify(meetingService).updateById(meeting);
verify(aiTaskService, never()).dispatchSummaryTask(301L);
verify(aiTaskService, never()).dispatchSummaryTask(301L, null, null);
TransactionSynchronizationUtils.triggerAfterCommit();
verify(aiTaskService).dispatchSummaryTask(301L);
verify(aiTaskService).dispatchSummaryTask(301L, null, null);
} finally {
TransactionSynchronizationManager.clearSynchronization();
}
}
@Test
void createMeetingShouldPersistResolvedRuntimeProfile() {
MeetingService meetingService = mock(MeetingService.class);
MeetingDomainSupport meetingDomainSupport = mock(MeetingDomainSupport.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingRuntimeProfileResolver runtimeProfileResolver = mockRuntimeProfileResolver(101L, 202L, 303L);
Meeting meeting = new Meeting();
meeting.setId(808L);
meeting.setTenantId(1L);
meeting.setHostUserId(7L);
meeting.setHostName("creator");
when(meetingDomainSupport.initMeeting(
eq("Resolved Meeting"),
any(LocalDateTime.class),
eq("1,2"),
eq("tagA"),
eq("/audio/demo.wav"),
eq(1L),
eq(7L),
eq("creator"),
eq(7L),
eq("creator"),
eq(0)
)).thenReturn(meeting);
when(meetingDomainSupport.relocateAudioUrl(808L, "/audio/demo.wav")).thenReturn("/audio/demo.wav");
fillHostFieldsFromMeeting(meetingDomainSupport);
MeetingCommandServiceImpl service = new MeetingCommandServiceImpl(
meetingService,
aiTaskService,
mock(HotWordService.class),
mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class),
mock(MeetingSummaryFileService.class),
meetingDomainSupport,
runtimeProfileResolver,
mock(RealtimeMeetingSessionStateService.class),
mock(RealtimeMeetingAudioStorageService.class),
mock(StringRedisTemplate.class),
new ObjectMapper()
);
CreateMeetingCommand command = new CreateMeetingCommand();
command.setTitle("Resolved Meeting");
command.setMeetingTime(LocalDateTime.of(2026, 4, 3, 19, 0));
command.setParticipants("1,2");
command.setTags("tagA");
command.setAudioUrl("/audio/demo.wav");
command.setAsrModelId(11L);
command.setSummaryModelId(22L);
command.setPromptId(33L);
service.createMeeting(command, 1L, 7L, "creator");
verify(aiTaskService).save(argThat(task -> {
if (!"ASR".equals(task.getTaskType())) {
return false;
}
Object asrModelId = task.getTaskConfig().get("asrModelId");
return Long.valueOf(101L).equals(asrModelId);
}));
verify(meetingDomainSupport).createSummaryTask(808L, 202L, 303L);
}
@Test
void createRealtimeMeetingShouldPersistResolvedResumeProfile() {
MeetingService meetingService = mock(MeetingService.class);
MeetingDomainSupport meetingDomainSupport = mock(MeetingDomainSupport.class);
MeetingRuntimeProfileResolver runtimeProfileResolver = mockRuntimeProfileResolver(111L, 222L, 333L);
RealtimeMeetingSessionStateService sessionStateService = mock(RealtimeMeetingSessionStateService.class);
Meeting meeting = new Meeting();
meeting.setId(909L);
meeting.setHostUserId(7L);
meeting.setHostName("creator");
when(meetingDomainSupport.initMeeting(
eq("Realtime Resolved"),
any(LocalDateTime.class),
eq("1,2"),
eq("tagB"),
isNull(),
eq(1L),
eq(7L),
eq("creator"),
eq(7L),
eq("creator"),
eq(0)
)).thenReturn(meeting);
fillHostFieldsFromMeeting(meetingDomainSupport);
MeetingCommandServiceImpl service = new MeetingCommandServiceImpl(
meetingService,
mock(AiTaskService.class),
mock(HotWordService.class),
mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class),
mock(MeetingSummaryFileService.class),
meetingDomainSupport,
runtimeProfileResolver,
sessionStateService,
mock(RealtimeMeetingAudioStorageService.class),
mock(StringRedisTemplate.class),
new ObjectMapper()
);
CreateRealtimeMeetingCommand command = new CreateRealtimeMeetingCommand();
command.setTitle("Realtime Resolved");
command.setMeetingTime(LocalDateTime.of(2026, 4, 3, 19, 0));
command.setParticipants("1,2");
command.setTags("tagB");
command.setAsrModelId(11L);
command.setSummaryModelId(22L);
command.setPromptId(33L);
command.setMode("online");
command.setLanguage("zh");
command.setEnablePunctuation(false);
command.setEnableItn(false);
command.setEnableTextRefine(true);
command.setSaveAudio(true);
service.createRealtimeMeeting(command, 1L, 7L, "creator");
verify(meetingDomainSupport).createSummaryTask(909L, 222L, 333L);
verify(sessionStateService).rememberResumeConfig(eq(909L), argThat(config ->
Long.valueOf(111L).equals(config.getAsrModelId())
&& "online".equals(config.getMode())
&& "zh".equals(config.getLanguage())
&& Integer.valueOf(1).equals(config.getUseSpkId())
&& Boolean.FALSE.equals(config.getEnablePunctuation())
&& Boolean.FALSE.equals(config.getEnableItn())
&& Boolean.TRUE.equals(config.getEnableTextRefine())
&& Boolean.TRUE.equals(config.getSaveAudio())
));
}
private MeetingCommandServiceImpl newService(MeetingService meetingService, MeetingDomainSupport meetingDomainSupport) {
return new MeetingCommandServiceImpl(
meetingService,
@ -331,6 +474,7 @@ class MeetingCommandServiceImplTest {
mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class),
mock(MeetingSummaryFileService.class),
meetingDomainSupport,
mockRuntimeProfileResolver(),
mock(RealtimeMeetingSessionStateService.class),
mock(RealtimeMeetingAudioStorageService.class),
mock(StringRedisTemplate.class),
@ -338,6 +482,29 @@ class MeetingCommandServiceImplTest {
);
}
private MeetingRuntimeProfileResolver mockRuntimeProfileResolver() {
return mockRuntimeProfileResolver(11L, 22L, 33L);
}
private MeetingRuntimeProfileResolver mockRuntimeProfileResolver(Long asrModelId, Long summaryModelId, Long promptId) {
MeetingRuntimeProfileResolver resolver = mock(MeetingRuntimeProfileResolver.class);
RealtimeMeetingRuntimeProfile profile = new RealtimeMeetingRuntimeProfile();
profile.setResolvedAsrModelId(asrModelId);
profile.setResolvedSummaryModelId(summaryModelId);
profile.setResolvedPromptId(promptId);
profile.setResolvedMode("online");
profile.setResolvedLanguage("zh");
profile.setResolvedUseSpkId(1);
profile.setResolvedEnablePunctuation(Boolean.FALSE);
profile.setResolvedEnableItn(Boolean.FALSE);
profile.setResolvedEnableTextRefine(Boolean.TRUE);
profile.setResolvedSaveAudio(Boolean.TRUE);
profile.setResolvedHotWords(java.util.List.of());
when(resolver.resolve(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()))
.thenReturn(profile);
return resolver;
}
private void fillHostFieldsFromMeeting(MeetingDomainSupport meetingDomainSupport) {
doAnswer(invocation -> {
MeetingVO vo = invocation.getArgument(1);

View File

@ -27,12 +27,8 @@ function VirtualPDFViewer({ url, filename }) {
// 使 useMemo
const fileConfig = useMemo(() => ({ url }), [url])
// Memoize PDF.js options to prevent unnecessary reloads
const pdfOptions = useMemo(() => ({
cMapUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/cmaps/',
cMapPacked: true,
standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/standard_fonts/',
}), [])
// 线 unpkg
const pdfOptions = useMemo(() => undefined, [])
// PDF
const pageHeight = useMemo(() => {

View File

@ -4,9 +4,6 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Calistoga&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<title>MeetingAI - 智能会议系统</title>
</head>
<body>

View File

@ -211,7 +211,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
tags: meetingValues.tags?.join(",") || "",
mode: meetingValues.mode || "2pass",
language: meetingValues.language || "auto",
useSpkId: meetingValues.useSpkId ? 1 : 0,
useSpkId: meetingValues.useSpkId == null ? 1 : (meetingValues.useSpkId ? 1 : 0),
enablePunctuation: meetingValues.enablePunctuation !== false,
enableItn: meetingValues.enableItn !== false,
enableTextRefine: !!meetingValues.enableTextRefine,
@ -229,7 +229,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
asrModelId: selectedAsrModel?.id || values.asrModelId,
mode: values.mode || "2pass",
language: values.language || "auto",
useSpkId: values.useSpkId ? 1 : 0,
useSpkId: values.useSpkId == null ? 1 : (values.useSpkId ? 1 : 0),
enablePunctuation: values.enablePunctuation !== false,
enableItn: values.enableItn !== false,
enableTextRefine: !!values.enableTextRefine,
@ -394,6 +394,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
items={[
{
key: 'advanced',
forceRender: true,
label: (
<div style={{ display: 'flex', alignItems: 'center', width: '100%', height: '32px' }}>
<div style={{ display: 'flex', alignItems: 'center', fontWeight: 600, color: 'var(--app-text-main)', fontSize: 15 }}>

View File

@ -28,12 +28,8 @@ function VirtualPDFViewer({ url, filename }) {
// 使 useMemo
const fileConfig = useMemo(() => ({ url }), [url])
// Memoize PDF.js options to prevent unnecessary reloads
const pdfOptions = useMemo(() => ({
cMapUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/cmaps/',
cMapPacked: true,
standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/standard_fonts/',
}), [])
// 线 unpkg
const pdfOptions = useMemo(() => undefined, [])
// PDF
const pageHeight = useMemo(() => {

View File

@ -228,8 +228,8 @@ export default function Login() {
<div className="login-footer">
<Text type="secondary">
{t("login.demoAccount")} <Text strong className="tabular-nums">admin</Text> / {t("login.password")}{" "}
<Text strong className="tabular-nums">123456</Text>
{/*{t("login.demoAccount")} <Text strong className="tabular-nums">admin</Text> / {t("login.password")}{" "}*/}
{/*<Text strong className="tabular-nums">123456</Text>*/}
</Text>
</div>
</div>

View File

@ -302,7 +302,7 @@ const HotWords: React.FC = () => {
dataSource={data}
rowKey="id"
loading={loading}
scroll={{ y: "calc(100vh - 340px)" }}
scroll={{ y: "calc(100vh - 440px)" }}
pagination={{
current,
pageSize: size,

View File

@ -27,7 +27,7 @@ const { Search } = Input;
const REG_CONTENT =
'iMeeting 智能会议系统,助力高效办公,让每一场讨论都有据可查。我正在进行声纹注册,以确保会议识别的准确性。';
const DEFAULT_DURATION = 10;
const DEFAULT_DURATION = 15;
const DEFAULT_PAGE_SIZE = 8;
const SpeakerReg: React.FC = () => {

View File

@ -6,13 +6,12 @@
.home-container {
position: relative;
flex: 1;
height: 100%;
padding: clamp(24px, 4vw, 40px) clamp(24px, 5vw, 60px);
min-height: 100%; // Changed from height: 100% to avoid height conflicts
padding: clamp(40px, 8vh, 80px) clamp(24px, 5vw, 60px) 0; // Top shift down, ZERO bottom padding
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
color: @home-text-main;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
overflow-x: hidden;
overflow-y: auto;
z-index: 1;
display: flex;
flex-direction: column;
@ -60,23 +59,24 @@
max-width: 1400px;
width: 100%;
margin: 0 auto;
flex: 1; // 占满剩余高度
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: clamp(24px, 5vh, 48px);
}
.home-hero {
margin-bottom: 56px;
padding-top: 32px;
margin-bottom: 0;
padding-top: 0;
}
.home-title {
font-size: clamp(40px, 5vw, 64px) !important;
font-size: clamp(32px, 4.5vw, 56px) !important; // Restored size
font-weight: 800 !important;
margin-bottom: 56px !important;
margin-bottom: clamp(32px, 6vh, 64px) !important; // Restored margin
color: @home-text-main !important;
letter-spacing: -0.02em; // Tighter tracking
letter-spacing: -0.02em;
display: flex;
align-items: center;
flex-wrap: wrap;
@ -85,7 +85,7 @@
position: relative;
display: inline-flex;
flex-direction: column;
height: 1.2em; /* fixed height for scroller */
height: 1.2em;
overflow: hidden;
vertical-align: bottom;
}
@ -99,10 +99,10 @@
.home-title-accent {
display: flex;
align-items: center;
height: 1.2em; /* strictly matched to wrapper height */
height: 1.2em;
line-height: 1.2em;
white-space: nowrap;
background: linear-gradient(90deg, #4f46e5, #7c3aed, #2563eb); // Deeper, more elegant gradient
background: linear-gradient(90deg, #4f46e5, #7c3aed, #2563eb);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@ -112,11 +112,11 @@
.home-quick-actions {
display: flex;
gap: 64px;
gap: clamp(24px, 4vw, 48px);
flex-wrap: wrap;
@media (max-width: 768px) {
gap: 24px;
gap: 16px;
flex-direction: column;
align-items: stretch;
@ -136,7 +136,7 @@
max-width: 280px;
flex: 1;
min-width: 240px;
padding: 32px 24px;
padding: 32px 24px; // Restored original padding
border-radius: 20px;
background: var(--action-bg-gradient);
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.4s ease;
@ -195,9 +195,9 @@
.home-action-icon-wrapper {
position: relative;
width: 72px;
width: 72px; // Restored original size
height: 72px;
margin-bottom: 32px;
margin-bottom: 32px; // Restored margin
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@ -259,7 +259,7 @@
}
.home-action-title {
font-size: 24px !important;
font-size: 24px !important; // Restored original size
font-weight: 700 !important;
margin-bottom: 16px !important;
color: #1e1e38 !important;
@ -273,14 +273,14 @@
}
.home-action-line {
font-size: 15px;
font-size: 15px; // Restored original size
color: #5a5a72;
line-height: 1.5;
}
}
.home-recent-section {
margin-top: auto;
margin-top: 0;
}
.home-section-header {
@ -325,8 +325,8 @@
position: relative;
display: flex;
flex-direction: column;
min-height: 140px;
padding: 20px 24px 20px;
min-height: 140px; // Restored original height
padding: 20px 24px; // Restored original padding
cursor: pointer;
overflow: hidden;
border-radius: 20px;
@ -421,12 +421,12 @@
.home-recent-card-title {
margin: 0 !important;
color: #2d2c59 !important;
font-size: 17px !important;
font-size: 17px !important; // Restored original size
line-height: 1.4 !important;
font-weight: 700 !important;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-line-clamp: 2; // Restored original lines
-webkit-box-orient: vertical;
}

View File

@ -62,7 +62,7 @@ function buildRecentCards(tasks: MeetingVO[]): RecentCard[] {
return fallbackRecentCards;
}
return tasks.slice(0, 3).map((task, index) => ({
return tasks.slice(0, 4).map((task, index) => ({
id: task.id,
title: task.title,
duration: `0${index + 1}:${10 + index * 12}`,
@ -142,7 +142,7 @@ export default function HomePage() {
description: ["音视频转文字", "区分发言人,一键导出"],
accent: "cyan",
badge: "iMeeting",
onClick: () => navigate("/meetings?create=true")
onClick: () => navigate("/meetings?action=create&type=upload")
}
],
[navigate]