feat: 增强会议配置解析和日志级别设置
- 在 `MeetingCommandServiceImpl` 中引入 `MeetingRuntimeProfileResolver`,解析并应用运行时配置 - 更新 `application-dev.yml` 和 `application.yml`,添加日志级别配置和新的数据库表dev_na
parent
1c82365e97
commit
b9593324a5
|
|
@ -2,12 +2,24 @@ package com.imeeting.event;
|
||||||
|
|
||||||
public class MeetingCreatedEvent {
|
public class MeetingCreatedEvent {
|
||||||
private final Long meetingId;
|
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.meetingId = meetingId;
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
this.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getMeetingId() {
|
public Long getMeetingId() {
|
||||||
return meetingId;
|
return meetingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,6 @@ public class MeetingTaskDispatchListener {
|
||||||
|
|
||||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
public void onMeetingCreated(MeetingCreatedEvent event) {
|
public void onMeetingCreated(MeetingCreatedEvent event) {
|
||||||
aiTaskService.dispatchTasks(event.getMeetingId());
|
aiTaskService.dispatchTasks(event.getMeetingId(), event.getTenantId(), event.getUserId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.imeeting.common.RedisKeys;
|
import com.imeeting.common.RedisKeys;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.mapper.biz.MeetingMapper;
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
|
import com.imeeting.support.TaskSecurityContextRunner;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
@ -27,15 +28,18 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
||||||
private final MeetingMapper meetingMapper;
|
private final MeetingMapper meetingMapper;
|
||||||
private final AiTaskService aiTaskService;
|
private final AiTaskService aiTaskService;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(ApplicationArguments args) {
|
public void run(ApplicationArguments args) {
|
||||||
log.info("Starting meeting task self-healing check...");
|
log.info("Starting meeting task self-healing check...");
|
||||||
|
|
||||||
// 1. 查询状态为 1(识别中) 或 2(总结中) 且未删除的会议
|
// 1. 查询状态为 1(识别中) 或 2(总结中) 且未删除的会议
|
||||||
List<Meeting> pendingMeetings = meetingMapper.selectList(new LambdaQueryWrapper<Meeting>()
|
List<Meeting> pendingMeetings = taskSecurityContextRunner.callAsPlatformAdmin(() ->
|
||||||
.in(Meeting::getStatus, 1, 2)
|
meetingMapper.selectList(new LambdaQueryWrapper<Meeting>()
|
||||||
.eq(Meeting::getIsDeleted, 0));
|
.in(Meeting::getStatus, 1, 2)
|
||||||
|
.eq(Meeting::getIsDeleted, 0))
|
||||||
|
);
|
||||||
|
|
||||||
if (pendingMeetings.isEmpty()) {
|
if (pendingMeetings.isEmpty()) {
|
||||||
log.info("No pending tasks found. Recovery check completed.");
|
log.info("No pending tasks found. Recovery check completed.");
|
||||||
|
|
@ -59,10 +63,10 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
||||||
// 3. 根据状态重新派发任务 (平滑拉起)
|
// 3. 根据状态重新派发任务 (平滑拉起)
|
||||||
if (meeting.getStatus() == 1) {
|
if (meeting.getStatus() == 1) {
|
||||||
log.info("Resuming ASR task for meeting {}", meeting.getId());
|
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) {
|
} else if (meeting.getStatus() == 2) {
|
||||||
log.info("Resuming Summary task for meeting {}", meeting.getId());
|
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();
|
Meeting update = new Meeting();
|
||||||
update.setId(m.getId());
|
update.setId(m.getId());
|
||||||
update.setStatus(4); // 失败
|
update.setStatus(4); // 失败
|
||||||
meetingMapper.updateById(update);
|
taskSecurityContextRunner.runAsTenantUser(m.getTenantId(), m.getCreatorId(), () -> meetingMapper.updateById(update));
|
||||||
|
|
||||||
// 同步 Redis 进度为失败
|
// 同步 Redis 进度为失败
|
||||||
String progressKey = RedisKeys.meetingProgressKey(m.getId());
|
String progressKey = RedisKeys.meetingProgressKey(m.getId());
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.imeeting.entity.biz.AiTask;
|
import com.imeeting.entity.biz.AiTask;
|
||||||
|
|
||||||
public interface AiTaskService extends IService<AiTask> {
|
public interface AiTaskService extends IService<AiTask> {
|
||||||
void dispatchTasks(Long meetingId);
|
void dispatchTasks(Long meetingId, Long tenantId, Long userId);
|
||||||
void dispatchSummaryTask(Long meetingId);
|
void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.imeeting.entity.biz.MeetingTranscript;
|
||||||
import com.imeeting.mapper.biz.AiTaskMapper;
|
import com.imeeting.mapper.biz.AiTaskMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingMapper;
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||||
|
import com.imeeting.support.TaskSecurityContextRunner;
|
||||||
import com.imeeting.service.biz.AiModelService;
|
import com.imeeting.service.biz.AiModelService;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.HotWordService;
|
import com.imeeting.service.biz.HotWordService;
|
||||||
|
|
@ -57,6 +58,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
private final HotWordService hotWordService;
|
private final HotWordService hotWordService;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final StringRedisTemplate redisTemplate;
|
||||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||||
|
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||||
|
|
||||||
@Value("${unisbase.app.server-base-url}")
|
@Value("${unisbase.app.server-base-url}")
|
||||||
private String serverBaseUrl;
|
private String serverBaseUrl;
|
||||||
|
|
@ -71,7 +73,11 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async
|
@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);
|
String lockKey = RedisKeys.meetingPollingLockKey(meetingId);
|
||||||
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
|
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
|
||||||
if (Boolean.FALSE.equals(acquired)) {
|
if (Boolean.FALSE.equals(acquired)) {
|
||||||
|
|
@ -134,7 +140,11 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async
|
@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);
|
Meeting meeting = meetingMapper.selectById(meetingId);
|
||||||
if (meeting == null) return;
|
if (meeting == null) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -413,7 +423,14 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
|
|
||||||
Long summaryModelId = Long.valueOf(taskRecord.getTaskConfig().get("summaryModelId").toString());
|
Long summaryModelId = Long.valueOf(taskRecord.getTaskConfig().get("summaryModelId").toString());
|
||||||
AiModelVO llmModel = aiModelService.getModelById(summaryModelId, "LLM");
|
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
|
String promptContent = taskRecord.getTaskConfig().get("promptContent") != null
|
||||||
? taskRecord.getTaskConfig().get("promptContent").toString() : "";
|
? taskRecord.getTaskConfig().get("promptContent").toString() : "";
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import com.imeeting.common.RedisKeys;
|
||||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
||||||
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
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.AiTaskService;
|
||||||
import com.imeeting.service.biz.HotWordService;
|
import com.imeeting.service.biz.HotWordService;
|
||||||
import com.imeeting.service.biz.MeetingCommandService;
|
import com.imeeting.service.biz.MeetingCommandService;
|
||||||
|
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
||||||
import com.imeeting.service.biz.MeetingService;
|
import com.imeeting.service.biz.MeetingService;
|
||||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
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 com.imeeting.mapper.biz.MeetingTranscriptMapper transcriptMapper;
|
||||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||||
private final MeetingDomainSupport meetingDomainSupport;
|
private final MeetingDomainSupport meetingDomainSupport;
|
||||||
|
private final MeetingRuntimeProfileResolver meetingRuntimeProfileResolver;
|
||||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||||
private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService;
|
private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
|
@ -57,6 +60,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName) {
|
public MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName) {
|
||||||
|
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
|
||||||
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
||||||
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
|
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
|
||||||
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
||||||
|
|
@ -69,9 +73,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
asrTask.setStatus(0);
|
asrTask.setStatus(0);
|
||||||
|
|
||||||
Map<String, Object> asrConfig = new HashMap<>();
|
Map<String, Object> asrConfig = new HashMap<>();
|
||||||
asrConfig.put("asrModelId", command.getAsrModelId());
|
asrConfig.put("asrModelId", runtimeProfile.getResolvedAsrModelId());
|
||||||
asrConfig.put("useSpkId", command.getUseSpkId() != null ? command.getUseSpkId() : 1);
|
asrConfig.put("useSpkId", runtimeProfile.getResolvedUseSpkId());
|
||||||
asrConfig.put("enableTextRefine", command.getEnableTextRefine() != null ? command.getEnableTextRefine() : false);
|
asrConfig.put("enableTextRefine", runtimeProfile.getResolvedEnableTextRefine());
|
||||||
|
|
||||||
List<String> finalHotWords = command.getHotWords();
|
List<String> finalHotWords = command.getHotWords();
|
||||||
if (finalHotWords == null || finalHotWords.isEmpty()) {
|
if (finalHotWords == null || finalHotWords.isEmpty()) {
|
||||||
|
|
@ -86,10 +90,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
asrTask.setTaskConfig(asrConfig);
|
asrTask.setTaskConfig(asrConfig);
|
||||||
aiTaskService.save(asrTask);
|
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()));
|
meeting.setAudioUrl(meetingDomainSupport.relocateAudioUrl(meeting.getId(), command.getAudioUrl()));
|
||||||
meetingService.updateById(meeting);
|
meetingService.updateById(meeting);
|
||||||
meetingDomainSupport.publishMeetingCreated(meeting.getId());
|
meetingDomainSupport.publishMeetingCreated(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
|
||||||
|
|
||||||
MeetingVO vo = new MeetingVO();
|
MeetingVO vo = new MeetingVO();
|
||||||
meetingDomainSupport.fillMeetingVO(meeting, vo, false);
|
meetingDomainSupport.fillMeetingVO(meeting, vo, false);
|
||||||
|
|
@ -99,14 +103,15 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName) {
|
public MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName) {
|
||||||
|
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
|
||||||
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
||||||
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
|
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
|
||||||
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
||||||
null, tenantId, creatorId, creatorName, hostUserId, hostName, 0);
|
null, tenantId, creatorId, creatorName, hostUserId, hostName, 0);
|
||||||
meetingService.save(meeting);
|
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.initSessionIfAbsent(meeting.getId(), tenantId, creatorId);
|
||||||
realtimeMeetingSessionStateService.rememberResumeConfig(meeting.getId(), buildRealtimeResumeConfig(command, tenantId));
|
realtimeMeetingSessionStateService.rememberResumeConfig(meeting.getId(), buildRealtimeResumeConfig(command, tenantId, runtimeProfile));
|
||||||
|
|
||||||
MeetingVO vo = new MeetingVO();
|
MeetingVO vo = new MeetingVO();
|
||||||
meetingDomainSupport.fillMeetingVO(meeting, vo, false);
|
meetingDomainSupport.fillMeetingVO(meeting, vo, false);
|
||||||
|
|
@ -239,7 +244,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
prepareOfflineReprocessTasks(meetingId, currentStatus);
|
prepareOfflineReprocessTasks(meetingId, currentStatus);
|
||||||
realtimeMeetingSessionStateService.clear(meetingId);
|
realtimeMeetingSessionStateService.clear(meetingId);
|
||||||
updateMeetingProgress(meetingId, 0, "正在转入离线音频识别流程...", 0);
|
updateMeetingProgress(meetingId, 0, "正在转入离线音频识别流程...", 0);
|
||||||
aiTaskService.dispatchTasks(meetingId);
|
aiTaskService.dispatchTasks(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,7 +271,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
meeting.setStatus(2);
|
meeting.setStatus(2);
|
||||||
meetingService.updateById(meeting);
|
meetingService.updateById(meeting);
|
||||||
updateMeetingProgress(meetingId, 90, "正在生成会议总结...", 0);
|
updateMeetingProgress(meetingId, 90, "正在生成会议总结...", 0);
|
||||||
aiTaskService.dispatchSummaryTask(meetingId);
|
aiTaskService.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyRealtimeAudioFinalizeResult(Meeting meeting, RealtimeMeetingAudioStorageService.FinalizeResult result) {
|
private void applyRealtimeAudioFinalizeResult(Meeting meeting, RealtimeMeetingAudioStorageService.FinalizeResult result) {
|
||||||
|
|
@ -450,18 +455,18 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
meetingDomainSupport.createSummaryTask(meetingId, summaryModelId, promptId);
|
meetingDomainSupport.createSummaryTask(meetingId, summaryModelId, promptId);
|
||||||
meeting.setStatus(2);
|
meeting.setStatus(2);
|
||||||
meetingService.updateById(meeting);
|
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()) {
|
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
aiTaskService.dispatchSummaryTask(meetingId);
|
aiTaskService.dispatchSummaryTask(meetingId, tenantId, userId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
@Override
|
@Override
|
||||||
public void afterCommit() {
|
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();
|
RealtimeMeetingResumeConfig resumeConfig = new RealtimeMeetingResumeConfig();
|
||||||
resumeConfig.setAsrModelId(command.getAsrModelId());
|
resumeConfig.setAsrModelId(runtimeProfile.getResolvedAsrModelId());
|
||||||
resumeConfig.setMode(command.getMode() == null || command.getMode().isBlank() ? "2pass" : command.getMode().trim());
|
resumeConfig.setMode(runtimeProfile.getResolvedMode());
|
||||||
resumeConfig.setLanguage(command.getLanguage() == null || command.getLanguage().isBlank() ? "auto" : command.getLanguage().trim());
|
resumeConfig.setLanguage(runtimeProfile.getResolvedLanguage());
|
||||||
resumeConfig.setUseSpkId(command.getUseSpkId() != null ? command.getUseSpkId() : 0);
|
resumeConfig.setUseSpkId(runtimeProfile.getResolvedUseSpkId());
|
||||||
resumeConfig.setEnablePunctuation(command.getEnablePunctuation() != null ? command.getEnablePunctuation() : Boolean.TRUE);
|
resumeConfig.setEnablePunctuation(runtimeProfile.getResolvedEnablePunctuation());
|
||||||
resumeConfig.setEnableItn(command.getEnableItn() != null ? command.getEnableItn() : Boolean.TRUE);
|
resumeConfig.setEnableItn(runtimeProfile.getResolvedEnableItn());
|
||||||
resumeConfig.setEnableTextRefine(Boolean.TRUE.equals(command.getEnableTextRefine()));
|
resumeConfig.setEnableTextRefine(runtimeProfile.getResolvedEnableTextRefine());
|
||||||
resumeConfig.setSaveAudio(Boolean.TRUE.equals(command.getSaveAudio()));
|
resumeConfig.setSaveAudio(runtimeProfile.getResolvedSaveAudio());
|
||||||
resumeConfig.setHotwords(resolveRealtimeHotwords(command.getHotWords(), tenantId));
|
resumeConfig.setHotwords(resolveRealtimeHotwords(runtimeProfile.getResolvedHotWords(), tenantId));
|
||||||
return resumeConfig;
|
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) {
|
private List<Map<String, Object>> resolveRealtimeHotwords(List<String> selectedWords, Long tenantId) {
|
||||||
List<HotWord> tenantHotwords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
List<HotWord> tenantHotwords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||||
.eq(HotWord::getTenantId, tenantId)
|
.eq(HotWord::getTenantId, tenantId)
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,8 @@ public class MeetingDomainSupport {
|
||||||
aiTaskService.save(sumTask);
|
aiTaskService.save(sumTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void publishMeetingCreated(Long meetingId) {
|
public void publishMeetingCreated(Long meetingId, Long tenantId, Long userId) {
|
||||||
eventPublisher.publishEvent(new MeetingCreatedEvent(meetingId));
|
eventPublisher.publishEvent(new MeetingCreatedEvent(meetingId, tenantId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String relocateAudioUrl(Long meetingId, String audioUrl) {
|
public String relocateAudioUrl(Long meetingId, String audioUrl) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
server:
|
server:
|
||||||
port: ${SERVER_PORT:8081}
|
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:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://127.0.0.1:5432/imeeting_db}
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://127.0.0.1:5432/imeeting_db}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ spring:
|
||||||
|
|
||||||
unisbase:
|
unisbase:
|
||||||
security:
|
security:
|
||||||
jwt-secret: ${SECURITY_JWT_SECRET}
|
jwt-secret: ${SECURITY_JWT_SECRET:change-me-dev-jwt-secret-32bytes}
|
||||||
internal-auth:
|
internal-auth:
|
||||||
secret: ${INTERNAL_AUTH_SECRET}
|
secret: ${INTERNAL_AUTH_SECRET:change-me-dev-internal-secret}
|
||||||
app:
|
app:
|
||||||
server-base-url: ${APP_SERVER_BASE_URL}
|
server-base-url: ${APP_SERVER_BASE_URL:http://127.0.0.1:${server.port}}
|
||||||
upload-path: ${APP_UPLOAD_PATH}
|
upload-path: ${APP_UPLOAD_PATH:/data/imeeting/uploads/}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
server:
|
server:
|
||||||
port: ${SERVER_PORT:8080}
|
port: ${SERVER_PORT:8080}
|
||||||
|
|
||||||
|
logging:
|
||||||
|
file:
|
||||||
|
path: ${LOG_PATH:./logs}
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
profiles:
|
profiles:
|
||||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||||
|
|
@ -34,6 +38,9 @@ unisbase:
|
||||||
- biz_ai_tasks
|
- biz_ai_tasks
|
||||||
- biz_meeting_transcripts
|
- biz_meeting_transcripts
|
||||||
- biz_speakers
|
- biz_speakers
|
||||||
|
- biz_llm_models
|
||||||
|
- biz_asr_models
|
||||||
|
- biz_prompt_templates
|
||||||
security:
|
security:
|
||||||
enabled: true
|
enabled: true
|
||||||
mode: embedded
|
mode: embedded
|
||||||
|
|
@ -43,6 +50,7 @@ unisbase:
|
||||||
- /actuator/health
|
- /actuator/health
|
||||||
- /api/static/**
|
- /api/static/**
|
||||||
- /ws/**
|
- /ws/**
|
||||||
|
- /api/android/**
|
||||||
internal-auth:
|
internal-auth:
|
||||||
enabled: true
|
enabled: true
|
||||||
header-name: X-Internal-Secret
|
header-name: X-Internal-Secret
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
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.dto.biz.RealtimeTranscriptItemDTO;
|
||||||
|
import com.imeeting.entity.biz.AiTask;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.HotWordService;
|
import com.imeeting.service.biz.HotWordService;
|
||||||
|
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
||||||
import com.imeeting.service.biz.MeetingService;
|
import com.imeeting.service.biz.MeetingService;
|
||||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
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.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
|
@ -200,6 +205,7 @@ class MeetingCommandServiceImplTest {
|
||||||
transcriptMapper,
|
transcriptMapper,
|
||||||
mock(MeetingSummaryFileService.class),
|
mock(MeetingSummaryFileService.class),
|
||||||
meetingDomainSupport,
|
meetingDomainSupport,
|
||||||
|
mockRuntimeProfileResolver(),
|
||||||
sessionStateService,
|
sessionStateService,
|
||||||
audioStorageService,
|
audioStorageService,
|
||||||
mock(StringRedisTemplate.class),
|
mock(StringRedisTemplate.class),
|
||||||
|
|
@ -212,7 +218,7 @@ class MeetingCommandServiceImplTest {
|
||||||
verify(meetingService).updateById(meetingCaptor.capture());
|
verify(meetingService).updateById(meetingCaptor.capture());
|
||||||
assertEquals("/api/static/meetings/202/source_audio.wav", meetingCaptor.getValue().getAudioUrl());
|
assertEquals("/api/static/meetings/202/source_audio.wav", meetingCaptor.getValue().getAudioUrl());
|
||||||
assertEquals(RealtimeMeetingAudioStorageService.STATUS_SUCCESS, meetingCaptor.getValue().getAudioSaveStatus());
|
assertEquals(RealtimeMeetingAudioStorageService.STATUS_SUCCESS, meetingCaptor.getValue().getAudioSaveStatus());
|
||||||
verify(aiTaskService).dispatchSummaryTask(202L);
|
verify(aiTaskService).dispatchSummaryTask(202L, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -237,6 +243,7 @@ class MeetingCommandServiceImplTest {
|
||||||
transcriptMapper,
|
transcriptMapper,
|
||||||
mock(MeetingSummaryFileService.class),
|
mock(MeetingSummaryFileService.class),
|
||||||
meetingDomainSupport,
|
meetingDomainSupport,
|
||||||
|
mockRuntimeProfileResolver(),
|
||||||
mock(RealtimeMeetingSessionStateService.class),
|
mock(RealtimeMeetingSessionStateService.class),
|
||||||
audioStorageService,
|
audioStorageService,
|
||||||
mock(StringRedisTemplate.class),
|
mock(StringRedisTemplate.class),
|
||||||
|
|
@ -262,6 +269,7 @@ class MeetingCommandServiceImplTest {
|
||||||
transcriptMapper,
|
transcriptMapper,
|
||||||
mock(MeetingSummaryFileService.class),
|
mock(MeetingSummaryFileService.class),
|
||||||
meetingDomainSupport,
|
meetingDomainSupport,
|
||||||
|
mockRuntimeProfileResolver(),
|
||||||
sessionStateService,
|
sessionStateService,
|
||||||
mock(RealtimeMeetingAudioStorageService.class),
|
mock(RealtimeMeetingAudioStorageService.class),
|
||||||
mock(StringRedisTemplate.class),
|
mock(StringRedisTemplate.class),
|
||||||
|
|
@ -300,6 +308,7 @@ class MeetingCommandServiceImplTest {
|
||||||
mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class),
|
mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class),
|
||||||
mock(MeetingSummaryFileService.class),
|
mock(MeetingSummaryFileService.class),
|
||||||
meetingDomainSupport,
|
meetingDomainSupport,
|
||||||
|
mockRuntimeProfileResolver(),
|
||||||
mock(RealtimeMeetingSessionStateService.class),
|
mock(RealtimeMeetingSessionStateService.class),
|
||||||
mock(RealtimeMeetingAudioStorageService.class),
|
mock(RealtimeMeetingAudioStorageService.class),
|
||||||
mock(StringRedisTemplate.class),
|
mock(StringRedisTemplate.class),
|
||||||
|
|
@ -313,16 +322,150 @@ class MeetingCommandServiceImplTest {
|
||||||
verify(meetingDomainSupport).createSummaryTask(301L, 22L, 33L);
|
verify(meetingDomainSupport).createSummaryTask(301L, 22L, 33L);
|
||||||
assertEquals(2, meeting.getStatus());
|
assertEquals(2, meeting.getStatus());
|
||||||
verify(meetingService).updateById(meeting);
|
verify(meetingService).updateById(meeting);
|
||||||
verify(aiTaskService, never()).dispatchSummaryTask(301L);
|
verify(aiTaskService, never()).dispatchSummaryTask(301L, null, null);
|
||||||
|
|
||||||
TransactionSynchronizationUtils.triggerAfterCommit();
|
TransactionSynchronizationUtils.triggerAfterCommit();
|
||||||
|
|
||||||
verify(aiTaskService).dispatchSummaryTask(301L);
|
verify(aiTaskService).dispatchSummaryTask(301L, null, null);
|
||||||
} finally {
|
} finally {
|
||||||
TransactionSynchronizationManager.clearSynchronization();
|
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) {
|
private MeetingCommandServiceImpl newService(MeetingService meetingService, MeetingDomainSupport meetingDomainSupport) {
|
||||||
return new MeetingCommandServiceImpl(
|
return new MeetingCommandServiceImpl(
|
||||||
meetingService,
|
meetingService,
|
||||||
|
|
@ -331,6 +474,7 @@ class MeetingCommandServiceImplTest {
|
||||||
mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class),
|
mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class),
|
||||||
mock(MeetingSummaryFileService.class),
|
mock(MeetingSummaryFileService.class),
|
||||||
meetingDomainSupport,
|
meetingDomainSupport,
|
||||||
|
mockRuntimeProfileResolver(),
|
||||||
mock(RealtimeMeetingSessionStateService.class),
|
mock(RealtimeMeetingSessionStateService.class),
|
||||||
mock(RealtimeMeetingAudioStorageService.class),
|
mock(RealtimeMeetingAudioStorageService.class),
|
||||||
mock(StringRedisTemplate.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) {
|
private void fillHostFieldsFromMeeting(MeetingDomainSupport meetingDomainSupport) {
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
MeetingVO vo = invocation.getArgument(1);
|
MeetingVO vo = invocation.getArgument(1);
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,8 @@ function VirtualPDFViewer({ url, filename }) {
|
||||||
// 使用 useMemo 避免不必要的重新加载
|
// 使用 useMemo 避免不必要的重新加载
|
||||||
const fileConfig = useMemo(() => ({ url }), [url])
|
const fileConfig = useMemo(() => ({ url }), [url])
|
||||||
|
|
||||||
// Memoize PDF.js options to prevent unnecessary reloads
|
// 离线部署环境不依赖 unpkg 等外部静态资源。
|
||||||
const pdfOptions = useMemo(() => ({
|
const pdfOptions = useMemo(() => undefined, [])
|
||||||
cMapUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/cmaps/',
|
|
||||||
cMapPacked: true,
|
|
||||||
standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/standard_fonts/',
|
|
||||||
}), [])
|
|
||||||
|
|
||||||
// 根据 PDF 实际宽高和缩放比例计算页面高度
|
// 根据 PDF 实际宽高和缩放比例计算页面高度
|
||||||
const pageHeight = useMemo(() => {
|
const pageHeight = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,6 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<title>MeetingAI - 智能会议系统</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
tags: meetingValues.tags?.join(",") || "",
|
tags: meetingValues.tags?.join(",") || "",
|
||||||
mode: meetingValues.mode || "2pass",
|
mode: meetingValues.mode || "2pass",
|
||||||
language: meetingValues.language || "auto",
|
language: meetingValues.language || "auto",
|
||||||
useSpkId: meetingValues.useSpkId ? 1 : 0,
|
useSpkId: meetingValues.useSpkId == null ? 1 : (meetingValues.useSpkId ? 1 : 0),
|
||||||
enablePunctuation: meetingValues.enablePunctuation !== false,
|
enablePunctuation: meetingValues.enablePunctuation !== false,
|
||||||
enableItn: meetingValues.enableItn !== false,
|
enableItn: meetingValues.enableItn !== false,
|
||||||
enableTextRefine: !!meetingValues.enableTextRefine,
|
enableTextRefine: !!meetingValues.enableTextRefine,
|
||||||
|
|
@ -229,7 +229,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
asrModelId: selectedAsrModel?.id || values.asrModelId,
|
asrModelId: selectedAsrModel?.id || values.asrModelId,
|
||||||
mode: values.mode || "2pass",
|
mode: values.mode || "2pass",
|
||||||
language: values.language || "auto",
|
language: values.language || "auto",
|
||||||
useSpkId: values.useSpkId ? 1 : 0,
|
useSpkId: values.useSpkId == null ? 1 : (values.useSpkId ? 1 : 0),
|
||||||
enablePunctuation: values.enablePunctuation !== false,
|
enablePunctuation: values.enablePunctuation !== false,
|
||||||
enableItn: values.enableItn !== false,
|
enableItn: values.enableItn !== false,
|
||||||
enableTextRefine: !!values.enableTextRefine,
|
enableTextRefine: !!values.enableTextRefine,
|
||||||
|
|
@ -394,6 +394,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: 'advanced',
|
key: 'advanced',
|
||||||
|
forceRender: true,
|
||||||
label: (
|
label: (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', width: '100%', height: '32px' }}>
|
<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 }}>
|
<div style={{ display: 'flex', alignItems: 'center', fontWeight: 600, color: 'var(--app-text-main)', fontSize: 15 }}>
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,8 @@ function VirtualPDFViewer({ url, filename }) {
|
||||||
// 使用 useMemo 避免不必要的重新加载
|
// 使用 useMemo 避免不必要的重新加载
|
||||||
const fileConfig = useMemo(() => ({ url }), [url])
|
const fileConfig = useMemo(() => ({ url }), [url])
|
||||||
|
|
||||||
// Memoize PDF.js options to prevent unnecessary reloads
|
// 离线部署环境不依赖 unpkg 等外部静态资源。
|
||||||
const pdfOptions = useMemo(() => ({
|
const pdfOptions = useMemo(() => undefined, [])
|
||||||
cMapUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/cmaps/',
|
|
||||||
cMapPacked: true,
|
|
||||||
standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/standard_fonts/',
|
|
||||||
}), [])
|
|
||||||
|
|
||||||
// 根据 PDF 实际宽高和缩放比例计算页面高度
|
// 根据 PDF 实际宽高和缩放比例计算页面高度
|
||||||
const pageHeight = useMemo(() => {
|
const pageHeight = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -228,8 +228,8 @@ export default function Login() {
|
||||||
|
|
||||||
<div className="login-footer">
|
<div className="login-footer">
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
{t("login.demoAccount")} <Text strong className="tabular-nums">admin</Text> / {t("login.password")}{" "}
|
{/*{t("login.demoAccount")} <Text strong className="tabular-nums">admin</Text> / {t("login.password")}{" "}*/}
|
||||||
<Text strong className="tabular-nums">123456</Text>
|
{/*<Text strong className="tabular-nums">123456</Text>*/}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ const HotWords: React.FC = () => {
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
scroll={{ y: "calc(100vh - 340px)" }}
|
scroll={{ y: "calc(100vh - 440px)" }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current,
|
current,
|
||||||
pageSize: size,
|
pageSize: size,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const { Search } = Input;
|
||||||
|
|
||||||
const REG_CONTENT =
|
const REG_CONTENT =
|
||||||
'iMeeting 智能会议系统,助力高效办公,让每一场讨论都有据可查。我正在进行声纹注册,以确保会议识别的准确性。';
|
'iMeeting 智能会议系统,助力高效办公,让每一场讨论都有据可查。我正在进行声纹注册,以确保会议识别的准确性。';
|
||||||
const DEFAULT_DURATION = 10;
|
const DEFAULT_DURATION = 15;
|
||||||
const DEFAULT_PAGE_SIZE = 8;
|
const DEFAULT_PAGE_SIZE = 8;
|
||||||
|
|
||||||
const SpeakerReg: React.FC = () => {
|
const SpeakerReg: React.FC = () => {
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,12 @@
|
||||||
.home-container {
|
.home-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
min-height: 100%; // Changed from height: 100% to avoid height conflicts
|
||||||
padding: clamp(24px, 4vw, 40px) clamp(24px, 5vw, 60px);
|
padding: clamp(40px, 8vh, 80px) clamp(24px, 5vw, 60px) 0; // Top shift down, ZERO bottom padding
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
|
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
|
||||||
color: @home-text-main;
|
color: @home-text-main;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -60,23 +59,24 @@
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
flex: 1; // 占满剩余高度
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
gap: clamp(24px, 5vh, 48px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-hero {
|
.home-hero {
|
||||||
margin-bottom: 56px;
|
margin-bottom: 0;
|
||||||
padding-top: 32px;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-title {
|
.home-title {
|
||||||
font-size: clamp(40px, 5vw, 64px) !important;
|
font-size: clamp(32px, 4.5vw, 56px) !important; // Restored size
|
||||||
font-weight: 800 !important;
|
font-weight: 800 !important;
|
||||||
margin-bottom: 56px !important;
|
margin-bottom: clamp(32px, 6vh, 64px) !important; // Restored margin
|
||||||
color: @home-text-main !important;
|
color: @home-text-main !important;
|
||||||
letter-spacing: -0.02em; // Tighter tracking
|
letter-spacing: -0.02em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 1.2em; /* fixed height for scroller */
|
height: 1.2em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
@ -99,10 +99,10 @@
|
||||||
.home-title-accent {
|
.home-title-accent {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 1.2em; /* strictly matched to wrapper height */
|
height: 1.2em;
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
white-space: nowrap;
|
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-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
|
|
@ -112,11 +112,11 @@
|
||||||
|
|
||||||
.home-quick-actions {
|
.home-quick-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 64px;
|
gap: clamp(24px, 4vw, 48px);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
gap: 24px;
|
gap: 16px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
padding: 32px 24px;
|
padding: 32px 24px; // Restored original padding
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: var(--action-bg-gradient);
|
background: var(--action-bg-gradient);
|
||||||
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.4s ease;
|
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 {
|
.home-action-icon-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 72px;
|
width: 72px; // Restored original size
|
||||||
height: 72px;
|
height: 72px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px; // Restored margin
|
||||||
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,7 +259,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-action-title {
|
.home-action-title {
|
||||||
font-size: 24px !important;
|
font-size: 24px !important; // Restored original size
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
margin-bottom: 16px !important;
|
margin-bottom: 16px !important;
|
||||||
color: #1e1e38 !important;
|
color: #1e1e38 !important;
|
||||||
|
|
@ -273,14 +273,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-action-line {
|
.home-action-line {
|
||||||
font-size: 15px;
|
font-size: 15px; // Restored original size
|
||||||
color: #5a5a72;
|
color: #5a5a72;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-recent-section {
|
.home-recent-section {
|
||||||
margin-top: auto;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-section-header {
|
.home-section-header {
|
||||||
|
|
@ -325,8 +325,8 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 140px;
|
min-height: 140px; // Restored original height
|
||||||
padding: 20px 24px 20px;
|
padding: 20px 24px; // Restored original padding
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
|
@ -421,12 +421,12 @@
|
||||||
.home-recent-card-title {
|
.home-recent-card-title {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
color: #2d2c59 !important;
|
color: #2d2c59 !important;
|
||||||
font-size: 17px !important;
|
font-size: 17px !important; // Restored original size
|
||||||
line-height: 1.4 !important;
|
line-height: 1.4 !important;
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2; // Restored original lines
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ function buildRecentCards(tasks: MeetingVO[]): RecentCard[] {
|
||||||
return fallbackRecentCards;
|
return fallbackRecentCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tasks.slice(0, 3).map((task, index) => ({
|
return tasks.slice(0, 4).map((task, index) => ({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
title: task.title,
|
title: task.title,
|
||||||
duration: `0${index + 1}:${10 + index * 12}`,
|
duration: `0${index + 1}:${10 + index * 12}`,
|
||||||
|
|
@ -142,7 +142,7 @@ export default function HomePage() {
|
||||||
description: ["音视频转文字", "区分发言人,一键导出"],
|
description: ["音视频转文字", "区分发言人,一键导出"],
|
||||||
accent: "cyan",
|
accent: "cyan",
|
||||||
badge: "iMeeting",
|
badge: "iMeeting",
|
||||||
onClick: () => navigate("/meetings?create=true")
|
onClick: () => navigate("/meetings?action=create&type=upload")
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[navigate]
|
[navigate]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue