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