feat: 添加 AI 目录功能和相关逻辑
- 在 `MeetingUnifiedStatusServiceImpl`、`MeetingCommandServiceImpl` 和 `MeetingDomainSupport` 中添加 `resolveAiCatalogEnabled` 方法,用于检查 AI 目录是否启用 - 更新 `MeetingVO` 和 `MeetingCreateConfigVO`,添加 `aiCatalogEnabled` 字段 - 在 `MeetingController` 中添加 `aiCatalogEnabled` 参数,并更新响应构建逻辑 - 在前端页面中添加对 `aiCatalogEnabled` 的处理,包括 `MeetingDetail`、`MeetingPreview` 和 `MeetingPreviewView` 页面 - 在 `sys-params/index.tsx` 中添加 `MEETING_AI_CATALOG_ENABLED` 系统参数配置 - 更新 `AndroidPushGrpcService` 中的平台枚举,增加新的平台类型 - 优化 `AiTaskServiceImpl` 中的任务调度逻辑,支持并行和串行模式dev_na
parent
97065c68a6
commit
2bab042ca0
|
|
@ -10,6 +10,10 @@ public final class SysParamKeys {
|
|||
public static final String CAPTCHA_ENABLED = "security.captcha.enabled";
|
||||
/** AI 会议总结使用的系统提示词。 */
|
||||
public static final String MEETING_SUMMARY_SYSTEM_PROMPT = "meeting.summary.system_prompt";
|
||||
/** 是否启用 AI 目录。 */
|
||||
public static final String MEETING_AI_CATALOG_ENABLED = "meeting.ai_catalog.enabled";
|
||||
/** 会议总结派发模式:PARALLEL / SERIAL。 */
|
||||
public static final String MEETING_SUMMARY_DISPATCH_MODE = "meeting.summary.dispatch_mode";
|
||||
/** 离线会议音频上传大小上限,单位 MB。 */
|
||||
public static final String MEETING_OFFLINE_AUDIO_MAX_SIZE_MB = "meeting.offline_audio.max_size_mb";
|
||||
/** 是否允许创建离线会议。 */
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ public class MeetingController {
|
|||
MeetingCreateConfigVO vo = new MeetingCreateConfigVO();
|
||||
vo.setOfflineEnabled(resolveBooleanParam(SysParamKeys.MEETING_CREATE_OFFLINE_ENABLED, true));
|
||||
vo.setRealtimeEnabled(resolveBooleanParam(SysParamKeys.MEETING_CREATE_REALTIME_ENABLED, true));
|
||||
vo.setAiCatalogEnabled(resolveBooleanParam(SysParamKeys.MEETING_AI_CATALOG_ENABLED, false));
|
||||
vo.setOfflineAudioMaxSizeMb(resolveLongParam(SysParamKeys.MEETING_OFFLINE_AUDIO_MAX_SIZE_MB, 1024L));
|
||||
return ApiResponse.ok(vo);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ public class MeetingCreateConfigVO {
|
|||
@Schema(description = "是否启用实时会议")
|
||||
private Boolean realtimeEnabled;
|
||||
|
||||
@Schema(description = "是否启用 AI 目录")
|
||||
private Boolean aiCatalogEnabled;
|
||||
|
||||
@Schema(description = "离线音频上传大小上限,单位 MB")
|
||||
private Long offlineAudioMaxSizeMb;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ public class MeetingVO {
|
|||
@Schema(description = "总结模板ID")
|
||||
private Long promptId;
|
||||
|
||||
@Schema(description = "是否启用 AI 目录")
|
||||
private Boolean aiCatalogEnabled;
|
||||
|
||||
@Schema(description = "音频保存状态")
|
||||
private String audioSaveStatus;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.common.MeetingProgressStage;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.dto.biz.MeetingSummarySource;
|
||||
|
|
@ -66,6 +67,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
|
||||
private static final Duration ASR_SUBMIT_REQUEST_TIMEOUT = Duration.ofSeconds(30);
|
||||
private static final Duration ASR_QUERY_REQUEST_TIMEOUT = Duration.ofSeconds(30);
|
||||
private static final String DISPATCH_MODE_PARALLEL = "PARALLEL";
|
||||
private static final String DISPATCH_MODE_SERIAL = "SERIAL";
|
||||
|
||||
private final MeetingMapper meetingMapper;
|
||||
private final MeetingTranscriptMapper transcriptMapper;
|
||||
|
|
@ -288,8 +291,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
sumTask == null ? null : sumTask.getId());
|
||||
meetingProgressService.markStage(meetingId, asrTask, 1, MeetingProgressStage.ASR_COMPLETED, 80, "转写完成,准备生成总结", 0);
|
||||
scheduleQueuedAsrTasks();
|
||||
self.dispatchChapterTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
self.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
dispatchPostAsrTasks(meeting, chapterTask, sumTask);
|
||||
return;
|
||||
}
|
||||
if (sumTask != null && canExecuteTask(sumTask)) {
|
||||
|
|
@ -343,8 +345,20 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
return;
|
||||
}
|
||||
executeChapterFlow(meeting, chapterTask);
|
||||
if (shouldRunSummaryAfterChapter(meeting, chapterTask)) {
|
||||
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
|
||||
if (summaryTask != null && canExecuteTask(summaryTask)) {
|
||||
self.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
reconcileMeetingStatus(meetingId);
|
||||
androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.COMPLETED.getCode());
|
||||
androidMeetingPushService.pushMeetingStatusChanged(
|
||||
meetingId,
|
||||
MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.FAILED)
|
||||
? UnifiedMeetingStatusStage.FAILED_SUMMARIZING.getCode()
|
||||
: UnifiedMeetingStatusStage.COMPLETED.getCode()
|
||||
);
|
||||
log.info("[CHAPTER-FLOW] 章节任务流程结束: meetingId={}, chapterTaskId={}, costMs={}",
|
||||
meetingId, chapterTask.getId(), System.currentTimeMillis() - startMillis);
|
||||
}
|
||||
|
|
@ -1178,7 +1192,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
log.info("[SUMMARY-EXEC] 已获取总结锁,开始构建总结来源: meetingId={}, sumTaskId={}",
|
||||
meeting.getId(), sumTask == null ? null : sumTask.getId());
|
||||
try {
|
||||
MeetingSummarySource summarySource = buildRawTranscriptSummarySource(meeting);
|
||||
MeetingSummarySource summarySource = buildSummarySourceForExecution(meeting, sumTask);
|
||||
if (summarySource.getText() == null || summarySource.getText().isBlank()) {
|
||||
log.warn("[SUMMARY-EXEC] 无转录内容,无法生成总结: meetingId={}, sumTaskId={}",
|
||||
meeting.getId(), sumTask == null ? null : sumTask.getId());
|
||||
|
|
@ -1208,6 +1222,85 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
.build();
|
||||
}
|
||||
|
||||
private MeetingSummarySource buildSummarySourceForExecution(Meeting meeting, AiTask sumTask) {
|
||||
if (shouldUseChapterBackedSummarySource()) {
|
||||
return meetingTranscriptChapterService.resolveSummarySource(meeting, sumTask);
|
||||
}
|
||||
return buildRawTranscriptSummarySource(meeting);
|
||||
}
|
||||
|
||||
private void dispatchPostAsrTasks(Meeting meeting, AiTask chapterTask, AiTask summaryTask) {
|
||||
if (meeting == null) {
|
||||
return;
|
||||
}
|
||||
if (!resolveAiCatalogEnabled()) {
|
||||
if (summaryTask != null && canExecuteTask(summaryTask)) {
|
||||
self.dispatchSummaryTask(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isSerialDispatchMode()) {
|
||||
if (chapterTask != null && canExecuteTask(chapterTask)) {
|
||||
self.dispatchChapterTask(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
|
||||
} else {
|
||||
log.warn("[ASR-FLOW] 串行模式下缺少可执行章节任务,跳过总结派发: meetingId={}, chapterTaskId={}, chapterStatus={}",
|
||||
meeting.getId(),
|
||||
chapterTask == null ? null : chapterTask.getId(),
|
||||
chapterTask == null ? null : chapterTask.getStatus());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (chapterTask != null && canExecuteTask(chapterTask)) {
|
||||
self.dispatchChapterTask(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
|
||||
}
|
||||
if (summaryTask != null && canExecuteTask(summaryTask)) {
|
||||
self.dispatchSummaryTask(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldRunSummaryAfterChapter(Meeting meeting, AiTask chapterTask) {
|
||||
return meeting != null
|
||||
&& resolveAiCatalogEnabled()
|
||||
&& isSerialDispatchMode()
|
||||
&& isTaskCompleted(chapterTask)
|
||||
&& !MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.FAILED);
|
||||
}
|
||||
|
||||
private boolean shouldUseChapterBackedSummarySource() {
|
||||
return resolveAiCatalogEnabled() && isSerialDispatchMode();
|
||||
}
|
||||
|
||||
private boolean resolveAiCatalogEnabled() {
|
||||
if (sysParamService == null) {
|
||||
return false;
|
||||
}
|
||||
String rawValue = sysParamService.getCachedParamValue(SysParamKeys.MEETING_AI_CATALOG_ENABLED, "false");
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String normalized = rawValue.trim().toLowerCase();
|
||||
return "1".equals(normalized)
|
||||
|| "true".equals(normalized)
|
||||
|| "yes".equals(normalized)
|
||||
|| "on".equals(normalized);
|
||||
}
|
||||
|
||||
private boolean isSerialDispatchMode() {
|
||||
return DISPATCH_MODE_SERIAL.equals(resolveSummaryDispatchMode());
|
||||
}
|
||||
|
||||
private String resolveSummaryDispatchMode() {
|
||||
if (sysParamService == null) {
|
||||
return DISPATCH_MODE_PARALLEL;
|
||||
}
|
||||
String rawValue = sysParamService.getCachedParamValue(SysParamKeys.MEETING_SUMMARY_DISPATCH_MODE, DISPATCH_MODE_PARALLEL);
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return DISPATCH_MODE_PARALLEL;
|
||||
}
|
||||
String normalized = rawValue.trim().toUpperCase(Locale.ROOT);
|
||||
return DISPATCH_MODE_SERIAL.equals(normalized) ? DISPATCH_MODE_SERIAL : DISPATCH_MODE_PARALLEL;
|
||||
}
|
||||
|
||||
private AiTask findLatestTask(Long meetingId, String taskType) {
|
||||
return this.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.dto.android.AndroidPendingMeetingDraft;
|
||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||
|
|
@ -48,6 +49,7 @@ import com.imeeting.support.redis.MeetingAsrPermitCache;
|
|||
import com.imeeting.support.redis.MeetingLockCache;
|
||||
import com.unisbase.common.exception.BusinessException;
|
||||
import com.unisbase.common.exception.ErrorCodeEnum;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
|
@ -90,6 +92,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
private final AndroidPendingMeetingDraftService androidPendingMeetingDraftService;
|
||||
private final MeetingLockCache meetingLockCache;
|
||||
private final MeetingAsrPermitCache meetingAsrPermitCache;
|
||||
private final SysParamService sysParamService;
|
||||
|
||||
@Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}")
|
||||
private String summaryOrchestrationMode;
|
||||
|
|
@ -115,7 +118,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
AndroidPushMessageService androidPushMessageService,
|
||||
AndroidPendingMeetingDraftService androidPendingMeetingDraftService,
|
||||
MeetingLockCache meetingLockCache,
|
||||
MeetingAsrPermitCache meetingAsrPermitCache) {
|
||||
MeetingAsrPermitCache meetingAsrPermitCache,
|
||||
SysParamService sysParamService) {
|
||||
this.meetingService = meetingService;
|
||||
this.aiTaskService = aiTaskService;
|
||||
this.hotWordService = hotWordService;
|
||||
|
|
@ -137,6 +141,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
this.androidPendingMeetingDraftService = androidPendingMeetingDraftService;
|
||||
this.meetingLockCache = meetingLockCache;
|
||||
this.meetingAsrPermitCache = meetingAsrPermitCache;
|
||||
this.sysParamService = sysParamService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -179,7 +184,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
aiTaskService.save(asrTask);
|
||||
|
||||
Long chapterModelId = command.getChapterModelId() != null ? command.getChapterModelId() : runtimeProfile.getResolvedSummaryModelId();
|
||||
meetingDomainSupport.createChapterTask(
|
||||
createChapterTaskIfEnabled(
|
||||
meeting.getId(),
|
||||
runtimeProfile.getResolvedSummaryModelId(),
|
||||
chapterModelId,
|
||||
|
|
@ -231,7 +236,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
hostUserId, hostName, runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId(), summaryDetailLevel, 0);
|
||||
meetingService.save(meeting);
|
||||
Long chapterModelId = command.getChapterModelId() != null ? command.getChapterModelId() : runtimeProfile.getResolvedSummaryModelId();
|
||||
meetingDomainSupport.createChapterTask(
|
||||
createChapterTaskIfEnabled(
|
||||
meeting.getId(),
|
||||
runtimeProfile.getResolvedSummaryModelId(),
|
||||
chapterModelId,
|
||||
|
|
@ -497,10 +502,16 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
realtimeMeetingSessionStateService.clear(meetingId);
|
||||
meeting.setStatus(MeetingStatusEnum.SUMMARIZING.getCode());
|
||||
meetingService.updateById(meeting);
|
||||
updateMeetingProgress(meetingId, 85, "正在生成 AI 目录与总结...", 0);
|
||||
updateMeetingProgress(meetingId, resolveAiCatalogEnabled() ? 85 : 90, resolveAiCatalogEnabled() ? "正在生成 AI 目录与总结..." : "正在生成会议总结...", 0);
|
||||
meetingDomainSupport.prewarmPlaybackAudioAfterCommit(meeting.getAudioUrl());
|
||||
aiTaskService.dispatchChapterTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
aiTaskService.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
if (!resolveAiCatalogEnabled()) {
|
||||
aiTaskService.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
} else if (isParallelDispatchMode()) {
|
||||
aiTaskService.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
aiTaskService.dispatchChapterTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
} else {
|
||||
aiTaskService.dispatchChapterTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -786,6 +797,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
@Transactional(rollbackFor = Exception.class)
|
||||
public MeetingTranscriptChapterImportResultVO importTranscriptChapters(MeetingTranscriptChapterImportDTO command) {
|
||||
ensureExternalSummaryModeEnabled();
|
||||
ensureAiCatalogEnabled();
|
||||
if (command == null || command.getMeetingId() == null) {
|
||||
throw new RuntimeException("缺少会议ID,无法导入章节");
|
||||
}
|
||||
|
|
@ -806,6 +818,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
.last("LIMIT 1"));
|
||||
|
||||
if (latestChapterTask == null) {
|
||||
ensureAiCatalogEnabled();
|
||||
Long summaryModelId = resolveSummaryModelId(command, latestSummaryTask);
|
||||
Long chapterModelId = resolveChapterModelId(command, latestSummaryTask, summaryModelId);
|
||||
Long promptId = resolvePromptId(command, latestSummaryTask);
|
||||
|
|
@ -1139,7 +1152,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
.eq(AiTask::getTaskType, "CHAPTER")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
if (chapterTask == null) {
|
||||
if (resolveAiCatalogEnabled() && chapterTask == null) {
|
||||
chapterTask = meetingDomainSupport.createChapterTask(
|
||||
meetingId,
|
||||
effectiveSummaryModelId,
|
||||
|
|
@ -1148,7 +1161,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
effectiveUserPrompt,
|
||||
effectiveSummaryDetailLevel
|
||||
);
|
||||
} else {
|
||||
} else if (resolveAiCatalogEnabled()) {
|
||||
resetAiTask(chapterTask, meetingSummaryPromptAssembler.buildTaskConfig(
|
||||
effectiveSummaryModelId,
|
||||
effectiveChapterModelId,
|
||||
|
|
@ -1204,6 +1217,16 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
if (!Integer.valueOf(3).equals(summaryTask.getStatus())) {
|
||||
throw new RuntimeException("当前总结环节未失败,无需重试");
|
||||
}
|
||||
if (resolveAiCatalogEnabled() && isSerialDispatchMode()) {
|
||||
AiTask chapterTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
.eq(AiTask::getTaskType, "CHAPTER")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
if (chapterTask == null || !Integer.valueOf(2).equals(chapterTask.getStatus())) {
|
||||
throw new RuntimeException("串行模式下缺少成功的 AI 目录产物,无法重试总结");
|
||||
}
|
||||
}
|
||||
Long effectiveSummaryModelId = resolveMeetingSummaryModelId(meeting, summaryTask);
|
||||
resetAiTask(summaryTask, buildSummaryTaskConfigForRetry(
|
||||
summaryTask,
|
||||
|
|
@ -1226,6 +1249,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void retryChapter(Long meetingId) {
|
||||
ensureAiCatalogEnabled();
|
||||
Meeting meeting = meetingService.getById(meetingId);
|
||||
if (meeting == null) {
|
||||
throw new RuntimeException("会议不存在");
|
||||
|
|
@ -1267,6 +1291,25 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
dispatchChapterTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
}
|
||||
|
||||
private AiTask createChapterTaskIfEnabled(Long meetingId,
|
||||
Long summaryModelId,
|
||||
Long chapterModelId,
|
||||
Long promptId,
|
||||
String userPrompt,
|
||||
String summaryDetailLevel) {
|
||||
if (!resolveAiCatalogEnabled()) {
|
||||
return null;
|
||||
}
|
||||
return meetingDomainSupport.createChapterTask(
|
||||
meetingId,
|
||||
summaryModelId,
|
||||
chapterModelId,
|
||||
promptId,
|
||||
userPrompt,
|
||||
summaryDetailLevel
|
||||
);
|
||||
}
|
||||
|
||||
private void clearLegacyDispatchState(Long meetingId) {
|
||||
if (meetingId == null) {
|
||||
return;
|
||||
|
|
@ -1605,4 +1648,45 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
}
|
||||
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||
}
|
||||
|
||||
private void ensureAiCatalogEnabled() {
|
||||
if (!resolveAiCatalogEnabled()) {
|
||||
throw new RuntimeException("AI目录功能未开启");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean resolveAiCatalogEnabled() {
|
||||
if (sysParamService == null) {
|
||||
return false;
|
||||
}
|
||||
String rawValue = sysParamService.getCachedParamValue(SysParamKeys.MEETING_AI_CATALOG_ENABLED, "false");
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String normalized = rawValue.trim().toLowerCase();
|
||||
return "1".equals(normalized)
|
||||
|| "true".equals(normalized)
|
||||
|| "yes".equals(normalized)
|
||||
|| "on".equals(normalized);
|
||||
}
|
||||
|
||||
private boolean isSerialDispatchMode() {
|
||||
return "SERIAL".equals(resolveSummaryDispatchMode());
|
||||
}
|
||||
|
||||
private boolean isParallelDispatchMode() {
|
||||
return "PARALLEL".equals(resolveSummaryDispatchMode());
|
||||
}
|
||||
|
||||
private String resolveSummaryDispatchMode() {
|
||||
if (sysParamService == null) {
|
||||
return "PARALLEL";
|
||||
}
|
||||
String rawValue = sysParamService.getCachedParamValue(SysParamKeys.MEETING_SUMMARY_DISPATCH_MODE, "PARALLEL");
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return "PARALLEL";
|
||||
}
|
||||
String normalized = rawValue.trim().toUpperCase();
|
||||
return "SERIAL".equals(normalized) ? "SERIAL" : "PARALLEL";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.imeeting.service.biz.impl;
|
|||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.entity.biz.MeetingTranscript;
|
||||
|
|
@ -13,6 +14,7 @@ import com.imeeting.service.biz.MeetingSummaryFileService;
|
|||
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
|
@ -54,6 +56,7 @@ public class MeetingDomainSupport {
|
|||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||
private final MeetingPlaybackAudioResolver meetingPlaybackAudioResolver;
|
||||
private final SysParamService sysParamService;
|
||||
|
||||
@Value("${unisbase.app.upload-path}")
|
||||
private String uploadPath;
|
||||
|
|
@ -399,6 +402,7 @@ public class MeetingDomainSupport {
|
|||
vo.setOfflineRecordingStatus(meeting.getOfflineRecordingStatus());
|
||||
vo.setSummaryModelId(meeting.getSummaryModelId());
|
||||
vo.setPromptId(meeting.getPromptId());
|
||||
vo.setAiCatalogEnabled(resolveAiCatalogEnabled());
|
||||
vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel()));
|
||||
vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
|
||||
vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
|
||||
|
|
@ -648,6 +652,21 @@ public class MeetingDomainSupport {
|
|||
return path != null && path.contains("\\");
|
||||
}
|
||||
|
||||
public boolean resolveAiCatalogEnabled() {
|
||||
if (sysParamService == null) {
|
||||
return false;
|
||||
}
|
||||
String rawValue = sysParamService.getCachedParamValue(SysParamKeys.MEETING_AI_CATALOG_ENABLED, "false");
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String normalized = rawValue.trim().toLowerCase();
|
||||
return "1".equals(normalized)
|
||||
|| "true".equals(normalized)
|
||||
|| "yes".equals(normalized)
|
||||
|| "on".equals(normalized);
|
||||
}
|
||||
|
||||
private Path resolvePublicAudioPath(String audioUrl) {
|
||||
if (audioUrl == null || audioUrl.isBlank()) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.imeeting.service.biz.impl;
|
|||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.dto.biz.MeetingProgressSnapshot;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.dto.biz.UnifiedMeetingStatusStage;
|
||||
|
|
@ -17,6 +18,7 @@ import com.imeeting.mapper.biz.MeetingTranscriptChapterVersionMapper;
|
|||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
import com.imeeting.service.biz.MeetingUnifiedStatusService;
|
||||
import com.imeeting.support.redis.MeetingProgressCache;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
|
@ -31,6 +33,7 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ
|
|||
private final MeetingTranscriptMapper meetingTranscriptMapper;
|
||||
private final MeetingTranscriptChapterVersionMapper chapterVersionMapper;
|
||||
private final MeetingProgressCache meetingProgressCache;
|
||||
private final SysParamService sysParamService;
|
||||
|
||||
@Override
|
||||
public UnifiedMeetingStatusVO resolve(MeetingVO meeting, MeetingProgressSnapshot snapshot) {
|
||||
|
|
@ -238,6 +241,9 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ
|
|||
}
|
||||
|
||||
private boolean canViewAiChapters(Long meetingId) {
|
||||
if (!resolveAiCatalogEnabled()) {
|
||||
return false;
|
||||
}
|
||||
return meetingId != null && chapterVersionMapper.selectCount(new LambdaQueryWrapper<MeetingTranscriptChapterVersion>()
|
||||
.eq(MeetingTranscriptChapterVersion::getMeetingId, meetingId)
|
||||
.eq(MeetingTranscriptChapterVersion::getIsCurrent, 1)
|
||||
|
|
@ -290,6 +296,7 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ
|
|||
vo.setSourceDeviceCode(meeting.getSourceDeviceCode());
|
||||
vo.setSourceDeviceMode(meeting.getSourceDeviceMode());
|
||||
vo.setOfflineRecordingStatus(meeting.getOfflineRecordingStatus());
|
||||
vo.setAiCatalogEnabled(resolveAiCatalogEnabled());
|
||||
vo.setSummaryDetailLevel(meeting.getSummaryDetailLevel());
|
||||
vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
|
||||
vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
|
||||
|
|
@ -300,6 +307,21 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ
|
|||
return vo;
|
||||
}
|
||||
|
||||
private boolean resolveAiCatalogEnabled() {
|
||||
if (sysParamService == null) {
|
||||
return false;
|
||||
}
|
||||
String rawValue = sysParamService.getCachedParamValue(SysParamKeys.MEETING_AI_CATALOG_ENABLED, "false");
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String normalized = rawValue.trim().toLowerCase();
|
||||
return "1".equals(normalized)
|
||||
|| "true".equals(normalized)
|
||||
|| "yes".equals(normalized)
|
||||
|| "on".equals(normalized);
|
||||
}
|
||||
|
||||
private record MeetingUnifiedStageContext(AiTask asrTask,
|
||||
AiTask chapterTask,
|
||||
AiTask summaryTask,
|
||||
|
|
|
|||
|
|
@ -11,8 +11,23 @@ option java_outer_classname = "PushProto";
|
|||
// =========================
|
||||
enum Platform {
|
||||
PLATFORM_UNKNOWN = 0;
|
||||
|
||||
// Mobile
|
||||
ANDROID = 1;
|
||||
IOS = 2;
|
||||
HARMONY_MOBILE = 3;
|
||||
|
||||
// Desktop
|
||||
WINDOWS = 10;
|
||||
MACOS = 11;
|
||||
LINUX = 12;
|
||||
|
||||
// Linux发行版(可选)
|
||||
KYLIN = 20;
|
||||
UOS = 21;
|
||||
|
||||
// Harmony PC
|
||||
HARMONY_PC = 30;
|
||||
}
|
||||
|
||||
// =========================
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export type SummaryDetailLevel = "DETAILED" | "STANDARD" | "BRIEF";
|
|||
export interface MeetingCreateConfig {
|
||||
offlineEnabled: boolean;
|
||||
realtimeEnabled: boolean;
|
||||
aiCatalogEnabled?: boolean;
|
||||
offlineAudioMaxSizeMb: number;
|
||||
chunkUploadEnabled?: boolean;
|
||||
chunkDurationSeconds?: number;
|
||||
|
|
@ -35,6 +36,7 @@ export interface MeetingVO {
|
|||
summaryDetailLevel?: SummaryDetailLevel;
|
||||
summaryModelId: number;
|
||||
promptId?: number;
|
||||
aiCatalogEnabled?: boolean;
|
||||
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
|
||||
audioSaveMessage?: string;
|
||||
accessPassword?: string;
|
||||
|
|
|
|||
|
|
@ -1226,7 +1226,7 @@ const MeetingDetail: React.FC = () => {
|
|||
const [expandKeywords, setExpandKeywords] = useState(false);
|
||||
const [expandSummary, setExpandSummary] = useState(false);
|
||||
const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
|
||||
const [workspaceTab, setWorkspaceTab] = useState<WorkspaceTab>('catalog');
|
||||
const [workspaceTab, setWorkspaceTab] = useState<WorkspaceTab>('transcript');
|
||||
const [addingHotwords, setAddingHotwords] = useState(false);
|
||||
const [editingTranscriptId, setEditingTranscriptId] = useState<number | null>(null);
|
||||
const [savingTranscriptId, setSavingTranscriptId] = useState<number | null>(null);
|
||||
|
|
@ -1402,15 +1402,16 @@ const MeetingDetail: React.FC = () => {
|
|||
return false;
|
||||
}, [meeting]);
|
||||
|
||||
const aiCatalogEnabled = meeting?.aiCatalogEnabled !== false;
|
||||
const canRetrySummary = isOwner
|
||||
&& transcripts.length > 0
|
||||
&& meeting?.status !== 1
|
||||
&& meeting?.status !== 2
|
||||
&& meeting?.latestSummaryAttemptStatus !== 3
|
||||
&& meeting?.latestChapterAttemptStatus !== 3;
|
||||
&& (!aiCatalogEnabled || meeting?.latestChapterAttemptStatus !== 3);
|
||||
const canRetryTranscription = isOwner && meeting?.status === 4 && transcripts.length === 0 && !!meeting?.audioUrl;
|
||||
const canRetryFailedSummaryTask = isOwner && meeting?.latestSummaryAttemptStatus === 3 && meeting?.status !== 2;
|
||||
const canRetryFailedChapterTask = isOwner && meeting?.latestChapterAttemptStatus === 3 && meeting?.status !== 2;
|
||||
const canRetryFailedChapterTask = aiCatalogEnabled && isOwner && meeting?.latestChapterAttemptStatus === 3 && meeting?.status !== 2;
|
||||
|
||||
const canRetrySchedule = isOwner && meeting?.status === 0 && (!generationProgress || generationProgress.percent <= 0) && !!generationProgress?.queuedAt && dayjs().diff(dayjs(generationProgress.queuedAt)) >= QUEUED_RETRY_THRESHOLD_MS;
|
||||
|
||||
|
|
@ -1433,14 +1434,14 @@ const MeetingDetail: React.FC = () => {
|
|||
};
|
||||
|
||||
const hasSummaryContent = Boolean(meeting?.summaryContent?.trim());
|
||||
const hasCatalogContent = catalogChapterLinks.length > 0;
|
||||
const hasCatalogContent = aiCatalogEnabled && catalogChapterLinks.length > 0;
|
||||
const generationFailureNotice = useMemo<MeetingStateNotice | null>(() => {
|
||||
if (!meeting || meeting.status !== 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasFallbackContent = hasSummaryContent || hasCatalogContent;
|
||||
if (meeting.latestChapterAttemptStatus === 3) {
|
||||
if (aiCatalogEnabled && meeting.latestChapterAttemptStatus === 3) {
|
||||
const detail = meeting.latestChapterAttemptErrorMsg || '章节生成失败';
|
||||
return {
|
||||
title: hasFallbackContent ? '历史内容仍可查看' : '本次 AI 目录生成失败',
|
||||
|
|
@ -1467,45 +1468,13 @@ const MeetingDetail: React.FC = () => {
|
|||
}
|
||||
|
||||
return {
|
||||
title: hasFallbackContent ? '历史内容仍可查看' : '会议处理异常',
|
||||
description: hasFallbackContent
|
||||
? '最近一次处理未成功,当前展示的是最近一次成功生成的内容。你可以继续查看,或重新发起识别/总结。'
|
||||
: '会议在处理中遇到问题,暂时没有可展示的总结内容。你可以重新发起识别或总结。',
|
||||
title: '会议处理异常',
|
||||
description: '会议在处理过程中遇到了问题。您可以尝试重新发起识别或总结。',
|
||||
type: hasFallbackContent ? 'info' : 'warning',
|
||||
hasFallbackContent,
|
||||
scope: 'global',
|
||||
};
|
||||
if (meeting.latestChapterAttemptStatus === 3) {
|
||||
const detail = meeting.latestChapterAttemptErrorMsg || '章节生成失败';
|
||||
return {
|
||||
key: `chapter-${meeting.latestChapterAttemptTaskId ?? 'latest'}`,
|
||||
title: '本次重新总结失败',
|
||||
description: hasFallbackContent
|
||||
? `章节生成失败,当前展示的是上一次成功的摘要和 AI 目录。失败原因:${detail}`
|
||||
: `章节生成失败,且当前没有可展示的历史摘要或 AI 目录。失败原因:${detail}`,
|
||||
hasFallbackContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (meeting.latestSummaryAttemptStatus === 3) {
|
||||
const detail = meeting.latestSummaryAttemptErrorMsg || '总结生成失败';
|
||||
return {
|
||||
key: `summary-${meeting.latestSummaryAttemptTaskId ?? 'latest'}`,
|
||||
title: '本次重新总结失败',
|
||||
description: hasFallbackContent
|
||||
? `总结生成失败,当前展示的是上一次成功的摘要和 AI 目录。失败原因:${detail}`
|
||||
: `总结生成失败,且当前没有可展示的历史摘要或 AI 目录。失败原因:${detail}`,
|
||||
hasFallbackContent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'general-failure',
|
||||
title: '会议处理异常',
|
||||
description: '会议在处理过程中遇到了问题。您可以尝试重新发起识别或总结。',
|
||||
hasFallbackContent,
|
||||
};
|
||||
}, [hasCatalogContent, hasSummaryContent, meeting]);
|
||||
}, [aiCatalogEnabled, hasCatalogContent, hasSummaryContent, meeting]);
|
||||
const summaryPanelNotice = useMemo<MeetingStateNotice | null>(() => {
|
||||
if (!meeting || !hasSummaryContent) {
|
||||
return null;
|
||||
|
|
@ -1522,7 +1491,7 @@ const MeetingDetail: React.FC = () => {
|
|||
return null;
|
||||
}, [generationFailureNotice, hasSummaryContent, meeting]);
|
||||
const catalogPanelNotice = useMemo<MeetingStateNotice | null>(() => {
|
||||
if (!generationFailureNotice || generationFailureNotice.scope !== 'catalog') {
|
||||
if (!aiCatalogEnabled || !generationFailureNotice || generationFailureNotice.scope !== 'catalog') {
|
||||
return null;
|
||||
}
|
||||
if (generationFailureNotice.hasFallbackContent && hasCatalogContent) {
|
||||
|
|
@ -1535,7 +1504,7 @@ const MeetingDetail: React.FC = () => {
|
|||
};
|
||||
}
|
||||
return generationFailureNotice;
|
||||
}, [generationFailureNotice, hasCatalogContent]);
|
||||
}, [aiCatalogEnabled, generationFailureNotice, hasCatalogContent]);
|
||||
const emptyTranscriptFailureNotice = useMemo(() => {
|
||||
if (!meeting || meeting.status !== 4 || transcripts.length > 0) {
|
||||
return null;
|
||||
|
|
@ -1557,12 +1526,18 @@ const MeetingDetail: React.FC = () => {
|
|||
}
|
||||
}, [meeting?.id, meeting?.status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!aiCatalogEnabled && workspaceTab === 'catalog') {
|
||||
setWorkspaceTab('transcript');
|
||||
}
|
||||
}, [aiCatalogEnabled, workspaceTab]);
|
||||
|
||||
useEffect(() => {
|
||||
const attemptKey = String(meeting?.latestChapterAttemptTaskId ?? meeting?.latestSummaryAttemptTaskId ?? '');
|
||||
if (meeting?.status !== 2 || !attemptKey) {
|
||||
return;
|
||||
}
|
||||
if ((generationProgress?.percent ?? 0) < 88 || catalogChapterLinks.length === 0) {
|
||||
if (!aiCatalogEnabled || (generationProgress?.percent ?? 0) < 88 || catalogChapterLinks.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (autoOpenedCatalogAttemptRef.current === attemptKey) {
|
||||
|
|
@ -1571,6 +1546,7 @@ const MeetingDetail: React.FC = () => {
|
|||
autoOpenedCatalogAttemptRef.current = attemptKey;
|
||||
setWorkspaceTab('catalog');
|
||||
}, [
|
||||
aiCatalogEnabled,
|
||||
catalogChapterLinks.length,
|
||||
generationProgress?.percent,
|
||||
meeting?.latestChapterAttemptTaskId,
|
||||
|
|
@ -2766,13 +2742,15 @@ const MeetingDetail: React.FC = () => {
|
|||
)}
|
||||
|
||||
<div className="transcript-stage-tabs">
|
||||
<button
|
||||
type="button"
|
||||
className={workspaceTab === 'catalog' ? 'active' : ''}
|
||||
onClick={() => setWorkspaceTab('catalog')}
|
||||
>
|
||||
AI目录
|
||||
</button>
|
||||
{aiCatalogEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
className={workspaceTab === 'catalog' ? 'active' : ''}
|
||||
onClick={() => setWorkspaceTab('catalog')}
|
||||
>
|
||||
AI目录
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={workspaceTab === 'transcript' ? 'active' : ''}
|
||||
|
|
@ -2783,7 +2761,7 @@ const MeetingDetail: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="transcript-scroll-shell">
|
||||
{workspaceTab === 'catalog' ? (
|
||||
{aiCatalogEnabled && workspaceTab === 'catalog' ? (
|
||||
<div className="catalog-list">
|
||||
{catalogPanelNotice && (
|
||||
<Alert
|
||||
|
|
|
|||
|
|
@ -339,6 +339,7 @@ export default function MeetingPreview() {
|
|||
const tags = useMemo(() => splitDisplayItems(meeting?.tags), [meeting?.tags]);
|
||||
const keywords = useMemo(() => analysis.keywords || [], [analysis.keywords]);
|
||||
const playbackAudioUrl = useMemo(() => resolveMeetingPlaybackAudioUrl(meeting), [meeting]);
|
||||
const aiCatalogEnabled = meeting?.aiCatalogEnabled !== false;
|
||||
const statusMeta = STATUS_META[meeting?.status || 0] || {
|
||||
label: TEXT.statusPending,
|
||||
className: "is-warning",
|
||||
|
|
@ -416,6 +417,12 @@ export default function MeetingPreview() {
|
|||
});
|
||||
}, [analysis.chapters, meetingChapters, transcripts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!aiCatalogEnabled && pageTab === "catalog") {
|
||||
setPageTab("summary");
|
||||
}
|
||||
}, [aiCatalogEnabled, pageTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTranscriptId) {
|
||||
return;
|
||||
|
|
@ -967,7 +974,7 @@ export default function MeetingPreview() {
|
|||
onChange={(key) => setPageTab(key as PreviewPageTab)}
|
||||
items={[
|
||||
{ key: "summary", label: TEXT.pageSummary },
|
||||
{ key: "catalog", label: TEXT.pageCatalog },
|
||||
...(aiCatalogEnabled ? [{ key: "catalog", label: TEXT.pageCatalog }] : []),
|
||||
{ key: "transcript", label: TEXT.pageTranscript },
|
||||
]}
|
||||
/>
|
||||
|
|
@ -975,7 +982,7 @@ export default function MeetingPreview() {
|
|||
|
||||
<div className="meeting-preview-tab-content">
|
||||
{pageTab === "summary" ? summaryTabContent : null}
|
||||
{pageTab === "catalog" ? catalogTabContent : null}
|
||||
{aiCatalogEnabled && pageTab === "catalog" ? catalogTabContent : null}
|
||||
{pageTab === "transcript" ? transcriptTabContent : null}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ interface PageHeaderProps {
|
|||
title: string;
|
||||
extra?: ReactNode;
|
||||
back?: boolean;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export default function PageHeader({ title, extra, back = false }: PageHeaderProps) {
|
||||
export default function PageHeader({ title, extra, back = false, onBack }: PageHeaderProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
|
|
@ -20,7 +21,7 @@ export default function PageHeader({ title, extra, back = false }: PageHeaderPro
|
|||
type="text"
|
||||
shape="circle"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => (onBack ? onBack() : navigate(-1))}
|
||||
className="page-header__back"
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {useMemo, useRef, useState} from "react";
|
||||
import {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {
|
||||
AudioOutlined,
|
||||
CalendarOutlined,
|
||||
|
|
@ -195,6 +195,7 @@ export default function MeetingPreviewView({
|
|||
}, [meeting?.participants, transcripts]);
|
||||
const tags = useMemo(() => splitDisplayItems(meeting?.tags), [meeting?.tags]);
|
||||
const playbackAudioUrl = useMemo(() => resolveMeetingPlaybackAudioUrl(meeting), [meeting]);
|
||||
const aiCatalogEnabled = meeting?.aiCatalogEnabled !== false;
|
||||
const statusMeta = STATUS_META[meeting?.status || 0] || {
|
||||
label: TEXT.statusPending,
|
||||
className: "is-warning",
|
||||
|
|
@ -267,6 +268,12 @@ export default function MeetingPreviewView({
|
|||
});
|
||||
}, [analysis.chapters, meetingChapters, transcripts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!aiCatalogEnabled && pageTab === "catalog") {
|
||||
setPageTab("summary");
|
||||
}
|
||||
}, [aiCatalogEnabled, pageTab]);
|
||||
|
||||
const handleTranscriptSeek = (item: MeetingTranscriptVO) => {
|
||||
if (!audioRef.current) return;
|
||||
audioRef.current.currentTime = Math.max(0, (item.startTime || 0) / 1000);
|
||||
|
|
@ -459,7 +466,7 @@ export default function MeetingPreviewView({
|
|||
onChange={(key) => setPageTab(key as PreviewPageTab)}
|
||||
items={[
|
||||
{key: "summary", label: TEXT.pageSummary},
|
||||
{key: "catalog", label: TEXT.pageCatalog},
|
||||
...(aiCatalogEnabled ? [{key: "catalog", label: TEXT.pageCatalog}] : []),
|
||||
{key: "transcript", label: TEXT.pageTranscript},
|
||||
]}
|
||||
/>
|
||||
|
|
@ -489,7 +496,7 @@ export default function MeetingPreviewView({
|
|||
</>
|
||||
) : null}
|
||||
|
||||
{pageTab === "catalog" ? (
|
||||
{aiCatalogEnabled && pageTab === "catalog" ? (
|
||||
<div className="meeting-preview-catalog-list">
|
||||
{catalogChapterLinks.length ? (
|
||||
catalogChapterLinks.map((chapter, index) => (
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
import { App, Button, Card, Input, Space, Typography } from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { getMeetingPreviewAccess, getPublicMeetingPreview } from "@/api/meeting";
|
||||
import LoadingScreen from "@/components/LoadingScreen";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import MeetingPreviewView from "@/components/preview/MeetingPreviewView";
|
||||
import usePageTitle from "@/hooks/usePageTitle";
|
||||
import type { MeetingChapterVO, MeetingTranscriptVO, MeetingVO } from "@/types";
|
||||
import { hasAccessToken } from "@/utils/auth";
|
||||
import { buildMeetingPreviewUrl } from "@/utils/meeting";
|
||||
|
||||
const { Paragraph, Title } = Typography;
|
||||
|
||||
export default function MeetingPreviewPage() {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -27,6 +30,10 @@ export default function MeetingPreviewPage() {
|
|||
const presetAccessPassword = useMemo(() => (searchParams.get("accessPassword") || "").trim(), [searchParams]);
|
||||
usePageTitle(meeting?.title || "会议预览");
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(hasAccessToken() ? "/meetings" : "/login", { replace: true });
|
||||
};
|
||||
|
||||
const loadPreview = async (password?: string) => {
|
||||
const previewResp = await getPublicMeetingPreview(meetingId, password);
|
||||
setMeeting(previewResp.data.data.meeting);
|
||||
|
|
@ -69,7 +76,7 @@ export default function MeetingPreviewPage() {
|
|||
};
|
||||
|
||||
void run();
|
||||
}, [meetingId, presetAccessPassword]);
|
||||
}, [meetingId, presetAccessPassword, message]);
|
||||
|
||||
const handleSubmitPassword = async () => {
|
||||
if (!accessPassword.trim()) {
|
||||
|
|
@ -93,21 +100,24 @@ export default function MeetingPreviewPage() {
|
|||
if (passwordRequired && !passwordVerified) {
|
||||
return (
|
||||
<div className="preview-page">
|
||||
<Card className="surface-card preview-password-card">
|
||||
<Title level={3}>会议预览</Title>
|
||||
<Paragraph type="secondary">该会议已设置访问密码,请输入密码后继续访问分享内容。</Paragraph>
|
||||
<Space direction="vertical" size={16} style={{ width: "100%" }}>
|
||||
<Input.Password
|
||||
value={accessPassword}
|
||||
placeholder="请输入访问密码"
|
||||
onChange={(event) => setAccessPassword(event.target.value)}
|
||||
onPressEnter={() => void handleSubmitPassword()}
|
||||
/>
|
||||
<Button type="primary" block loading={loading} onClick={() => void handleSubmitPassword()}>
|
||||
进入预览
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<div className="preview-page__inner">
|
||||
<PageHeader title="分享预览" back onBack={handleBack} />
|
||||
<Card className="surface-card preview-password-card">
|
||||
<Title level={3}>会议预览</Title>
|
||||
<Paragraph type="secondary">该会议已设置访问密码,请输入密码后继续访问分享内容。</Paragraph>
|
||||
<Space direction="vertical" size={16} style={{ width: "100%" }}>
|
||||
<Input.Password
|
||||
value={accessPassword}
|
||||
placeholder="请输入访问密码"
|
||||
onChange={(event) => setAccessPassword(event.target.value)}
|
||||
onPressEnter={() => void handleSubmitPassword()}
|
||||
/>
|
||||
<Button type="primary" block loading={loading} onClick={() => void handleSubmitPassword()}>
|
||||
进入预览
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -119,9 +129,7 @@ export default function MeetingPreviewPage() {
|
|||
return (
|
||||
<div className="preview-page">
|
||||
<div className="preview-page__inner">
|
||||
<div className="preview-page__header">
|
||||
<span className="login-page__badge">iMeeting 分享预览</span>
|
||||
</div>
|
||||
<PageHeader title="分享预览" back onBack={handleBack} />
|
||||
<MeetingPreviewView
|
||||
meeting={meeting}
|
||||
transcripts={transcripts}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export interface MeetingVO {
|
|||
summaryDetailLevel?: "DETAILED" | "STANDARD" | "BRIEF";
|
||||
summaryModelId?: number;
|
||||
promptId?: number;
|
||||
aiCatalogEnabled?: boolean;
|
||||
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
|
||||
audioSaveMessage?: string;
|
||||
accessPassword?: string | null;
|
||||
|
|
|
|||
Loading…
Reference in New Issue