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 { 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;
}
} }

View File

@ -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());
} }
} }

View File

@ -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());

View File

@ -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);
} }

View File

@ -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() : "";

View File

@ -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)

View File

@ -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) {

View File

@ -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}
@ -24,4 +32,4 @@ unisbase:
secret: ${INTERNAL_AUTH_SECRET:change-me-dev-internal-secret} secret: ${INTERNAL_AUTH_SECRET:change-me-dev-internal-secret}
app: app:
server-base-url: ${APP_SERVER_BASE_URL:http://127.0.0.1:${server.port}} server-base-url: ${APP_SERVER_BASE_URL:http://127.0.0.1:${server.port}}
upload-path: ${APP_UPLOAD_PATH:D:/data/imeeting/uploads/} upload-path: ${APP_UPLOAD_PATH:D:/data/imeeting/uploads/}

View File

@ -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/}

View File

@ -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

View File

@ -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);

View File

@ -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(() => {

View File

@ -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>

View File

@ -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 }}>

View File

@ -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(() => {

View File

@ -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>

View File

@ -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,

View File

@ -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 = () => {

View File

@ -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;
} }

View File

@ -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]