feat: 添加会议MCP工具服务和优化章节查询

- 新增 `MeetingMcpToolService` 以提供会议列表、预览、详情和Markdown包
- 在 `MeetingQueryService` 中添加忽略租户的章节查询方法
- 更新 `MeetingPublicPreviewController` 以使用新的章节查询方法
dev_na
chenhao 2026-05-11 18:09:31 +08:00
parent ccb408ade5
commit b9ec41095f
4 changed files with 600 additions and 1 deletions

View File

@ -53,7 +53,7 @@ public class MeetingPublicPreviewController {
data.getMeeting().setAccessPassword(null); data.getMeeting().setAccessPassword(null);
} }
data.setTranscripts(meetingQueryService.getTranscripts(id)); data.setTranscripts(meetingQueryService.getTranscripts(id));
data.setChapters(meetingQueryService.getChapters(id)); data.setChapters(meetingQueryService.getChaptersIgnoreTenant(id));
return ApiResponse.ok(data); return ApiResponse.ok(data);
} catch (RuntimeException ex) { } catch (RuntimeException ex) {
return ApiResponse.error(ex.getMessage()); return ApiResponse.error(ex.getMessage());

View File

@ -22,6 +22,8 @@ public interface MeetingQueryService {
List<Map<String, Object>> getChapters(Long meetingId); List<Map<String, Object>> getChapters(Long meetingId);
List<Map<String, Object>> getChaptersIgnoreTenant(Long meetingId);
MeetingTranscriptSourceVO getTranscriptSource(Long meetingId); MeetingTranscriptSourceVO getTranscriptSource(Long meetingId);
MeetingSummaryPromptContextVO buildSummaryPromptContext(Long meetingId, MeetingSummaryPromptContextRequestDTO request); MeetingSummaryPromptContextVO buildSummaryPromptContext(Long meetingId, MeetingSummaryPromptContextRequestDTO request);

View File

@ -112,6 +112,15 @@ public class MeetingQueryServiceImpl implements MeetingQueryService {
return meetingTranscriptChapterService.listDisplayChapterAnalysis(meeting); return meetingTranscriptChapterService.listDisplayChapterAnalysis(meeting);
} }
@Override
public List<Map<String, Object>> getChaptersIgnoreTenant(Long meetingId) {
Meeting meeting = meetingMapper.selectByIdIgnoreTenant(meetingId);
if (meeting == null) {
return List.of();
}
return meetingTranscriptChapterService.listDisplayChapterAnalysis(meeting);
}
@Override @Override
public MeetingTranscriptSourceVO getTranscriptSource(Long meetingId) { public MeetingTranscriptSourceVO getTranscriptSource(Long meetingId) {
return meetingTranscriptChapterService.buildTranscriptSource(meetingId); return meetingTranscriptChapterService.buildTranscriptSource(meetingId);

View File

@ -0,0 +1,588 @@
package com.imeeting.service.mcp;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.common.RedisKeys;
import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingItemResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingListResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult;
import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingTagResponse;
import com.imeeting.dto.biz.MeetingTranscriptSourceVO;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.service.biz.AiTaskService;
import com.imeeting.service.biz.MeetingAccessService;
import com.imeeting.service.biz.MeetingQueryService;
import com.imeeting.service.biz.MeetingService;
import com.imeeting.service.biz.MeetingTranscriptChapterService;
import com.imeeting.service.biz.MeetingTranscriptFileService;
import com.imeeting.service.biz.PromptTemplateService;
import com.unisbase.dto.PageResult;
import com.unisbase.entity.SysUser;
import com.unisbase.mapper.SysUserMapper;
import com.unisbase.security.LoginUser;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class MeetingMcpToolService {
private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
private static final String STAGE_AUDIO_TRANSCRIPTION = "audio_transcription";
private static final String STAGE_SUMMARY_GENERATION = "summary_generation";
private static final String STAGE_COMPLETED = "completed";
private final MeetingQueryService meetingQueryService;
private final MeetingAccessService meetingAccessService;
private final MeetingService meetingService;
private final AiTaskService aiTaskService;
private final PromptTemplateService promptTemplateService;
private final MeetingTranscriptFileService meetingTranscriptFileService;
private final MeetingTranscriptChapterService meetingTranscriptChapterService;
private final SysUserMapper sysUserMapper;
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
@Value("${unisbase.app.server-base-url:}")
private String serverBaseUrl;
public LegacyMeetingListResponse listCurrentUserMeetings(Integer page, Integer pageSize, String title) {
LoginUser loginUser = currentLoginUser();
int normalizedPage = normalizePositive(page, 1);
int normalizedPageSize = normalizePositive(pageSize, 10);
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
|| Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
PageResult<List<MeetingVO>> result = meetingQueryService.pageMeetings(
normalizedPage,
normalizedPageSize,
normalizeOptionalText(title),
loginUser.getTenantId(),
loginUser.getUserId(),
resolveCreatorName(loginUser),
"all",
isAdmin
);
LegacyMeetingListResponse data = new LegacyMeetingListResponse();
data.setPage(normalizedPage);
data.setPageSize(normalizedPageSize);
data.setTotal(result == null ? 0L : result.getTotal());
data.setTotalPages(normalizedPageSize <= 0 ? 0 : (data.getTotal() + normalizedPageSize - 1) / normalizedPageSize);
data.setHasMore(normalizedPage < data.getTotalPages());
data.setMeetings(result == null || result.getRecords() == null
? List.of()
: result.getRecords().stream().map(this::buildListItem).toList());
return data;
}
public LegacyMeetingPreviewResult getMeetingPreview(Long meetingId) {
if (meetingId == null) {
throw new IllegalArgumentException("meetingId is required");
}
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
return buildPreviewResult(meeting);
}
public Map<String, Object> getMeetingRichDetail(Long meetingId) {
if (meetingId == null) {
throw new IllegalArgumentException("meetingId is required");
}
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
MeetingVO detail = meetingQueryService.getDetail(meetingId);
normalizeMeetingAudioUrls(detail);
LegacyMeetingPreviewResult preview = buildPreviewResult(meeting);
Map<String, Object> previewData = preview.getData() == null
? new LinkedHashMap<>()
: objectMapper.convertValue(preview.getData(), new TypeReference<Map<String, Object>>() {});
List<Map<String, Object>> chapters = meetingQueryService.getChapters(meetingId);
MeetingTranscriptSourceVO transcriptSource = meetingQueryService.getTranscriptSource(meetingId);
Map<String, Object> analysis = detail == null || detail.getAnalysis() == null ? Map.of() : detail.getAnalysis();
previewData.put("previewCode", preview.getCode());
previewData.put("previewMessage", preview.getMessage());
previewData.put("meetingDetail", detail);
previewData.put("keywords", normalizeStringList(analysis.get("keywords")));
previewData.put("analysis", analysis);
previewData.put("chapters", chapters);
previewData.put("transcriptSource", transcriptSource);
previewData.put("audioUrl", detail == null ? toAbsoluteUrl(meeting.getAudioUrl()) : detail.getAudioUrl());
previewData.put("playbackAudioUrl", detail == null ? null : detail.getPlaybackAudioUrl());
previewData.put("hasRawTranscript", transcriptSource != null
&& transcriptSource.getTranscriptText() != null
&& !transcriptSource.getTranscriptText().isBlank());
return previewData;
}
public Map<String, Object> getMeetingMarkdownBundle(Long meetingId) {
if (meetingId == null) {
throw new IllegalArgumentException("meetingId is required");
}
LoginUser loginUser = currentLoginUser();
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
MeetingVO detail = meetingQueryService.getDetail(meetingId);
Map<String, Object> result = new LinkedHashMap<>();
result.put("meetingId", meetingId);
result.put("summaryMarkdown", detail == null ? null : detail.getSummaryContent());
result.put("transcriptMarkdown", meetingTranscriptFileService.loadTranscriptMarkdown(meeting, detail));
result.put("chapterMarkdown", meetingTranscriptChapterService.loadCurrentChapterMarkdown(meeting));
return result;
}
private LegacyMeetingPreviewResult buildPreviewResult(Meeting meeting) {
if (meeting == null) {
return new LegacyMeetingPreviewResult("404", "会议不存在", null);
}
Long meetingId = meeting.getId();
AiTask asrTask = findLatestTask(meetingId, "ASR");
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
boolean summaryCompleted = summaryTask != null && Integer.valueOf(2).equals(summaryTask.getStatus());
MeetingVO detail = (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted)
? meetingQueryService.getDetail(meetingId)
: null;
boolean hasSummary = detail != null && detail.getSummaryContent() != null && !detail.getSummaryContent().isBlank();
if (hasSummary) {
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask));
}
if (summaryCompleted) {
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask));
}
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) {
return new LegacyMeetingPreviewResult(
"503",
buildFailureMessage(asrTask, "转译"),
buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
if (isFailed(summaryTask)) {
return new LegacyMeetingPreviewResult(
"503",
buildFailureMessage(summaryTask, "总结"),
buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 75, STAGE_SUMMARY_GENERATION))
);
}
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
if (realtimeProgress != null) {
if (realtimeProgress >= 100) {
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
boolean completedHasSummary = completedDetail != null
&& completedDetail.getSummaryContent() != null
&& !completedDetail.getSummaryContent().isBlank();
if (completedHasSummary) {
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, completedDetail, summaryTask));
}
return new LegacyMeetingPreviewResult(
"504",
"处理已完成,但摘要尚未同步,请稍后重试",
buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED))
);
}
if (realtimeProgress < 90) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在转译音频", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION))
);
}
boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
if (!isAsrStage && !isSummaryStage) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
);
}
if (!isSummaryStage) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在转译音频", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION))
);
}
private LegacyMeetingPreviewDataResponse buildCompletedPreview(Meeting meeting, MeetingVO detail, AiTask summaryTask) {
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();
data.setMeetingId(meeting.getId());
data.setTitle(meeting.getTitle());
data.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
data.setSummary(detail == null ? null : detail.getSummaryContent());
data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
Long promptId = resolvePromptId(summaryTask);
data.setPromptId(promptId);
data.setPromptName(resolvePromptName(promptId));
List<LegacyMeetingAttendeeResponse> attendees = buildAttendees(meeting.getParticipants());
data.setAttendees(attendees);
data.setAttendeesCount(attendees.size());
data.setHasPassword(meeting.getAccessPassword() != null && !meeting.getAccessPassword().isBlank());
data.setProcessingStatus(processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED));
return data;
}
private LegacyMeetingPreviewDataResponse buildProcessingPreview(Meeting meeting,
AiTask summaryTask,
LegacyMeetingProcessingStatusResponse status) {
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();
data.setMeetingId(meeting.getId());
data.setTitle(meeting.getTitle());
data.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
Long promptId = resolvePromptId(summaryTask);
data.setPromptId(promptId);
data.setPromptName(resolvePromptName(promptId));
data.setHasPassword(meeting.getAccessPassword() != null && !meeting.getAccessPassword().isBlank());
data.setProcessingStatus(status);
return data;
}
private LegacyMeetingItemResponse buildListItem(MeetingVO meeting) {
LegacyMeetingItemResponse item = new LegacyMeetingItemResponse();
item.setMeetingId(meeting.getId());
item.setTitle(meeting.getTitle());
item.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
item.setCreatedAt(formatDateTime(meeting.getCreatedAt()));
item.setCreatorId(meeting.getCreatorId());
item.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
item.setAudioFilePath(toAbsoluteUrl(meeting.getAudioUrl()));
item.setAudioDuration(meeting.getDuration());
item.setAccessPassword(resolveAccessPassword(meeting.getId()));
List<Long> attendeeIds = meeting.getParticipantIds() == null ? List.of() : meeting.getParticipantIds();
item.setAttendeeIds(attendeeIds);
item.setAttendees(buildAttendees(attendeeIds));
item.setTags(buildTags(meeting.getTags()));
item.setSummary(resolveListSummary(meeting.getId()));
LegacyMeetingProcessingStatusResponse status = buildListStatus(meeting);
item.setOverallStatus(status.getOverallStatus());
item.setOverallProgress(status.getOverallProgress());
item.setCurrentStage(translateListStage(status.getCurrentStage()));
return item;
}
private LegacyMeetingProcessingStatusResponse buildListStatus(MeetingVO meeting) {
Long meetingId = meeting.getId();
AiTask asrTask = findLatestTask(meetingId, "ASR");
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
boolean summaryCompleted = summaryTask != null && Integer.valueOf(2).equals(summaryTask.getStatus());
if (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted) {
return new LegacyMeetingProcessingStatusResponse("completed", 100, STAGE_COMPLETED);
}
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) {
return new LegacyMeetingProcessingStatusResponse("failed", 50, STAGE_AUDIO_TRANSCRIPTION);
}
if (isFailed(summaryTask)) {
return new LegacyMeetingProcessingStatusResponse("failed", 75, STAGE_SUMMARY_GENERATION);
}
boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
if (!isAsrStage && !isSummaryStage) {
return new LegacyMeetingProcessingStatusResponse("pending", 0, STAGE_DATA_INITIALIZATION);
}
if (isSummaryStage) {
return new LegacyMeetingProcessingStatusResponse("summarizing", 75, STAGE_SUMMARY_GENERATION);
}
return new LegacyMeetingProcessingStatusResponse("transcribing", 50, STAGE_AUDIO_TRANSCRIPTION);
}
private String buildFailureMessage(AiTask failedTask, String stageName) {
String error = failedTask == null || failedTask.getErrorMsg() == null || failedTask.getErrorMsg().isBlank()
? "处理失败"
: failedTask.getErrorMsg();
return "会议" + stageName + "失败: " + error;
}
private boolean isRunningAsr(AiTask task) {
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
}
private boolean isRunningSummary(AiTask task) {
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
}
private boolean isFailed(AiTask task) {
return task != null && Integer.valueOf(3).equals(task.getStatus());
}
private AiTask findLatestTask(Long meetingId, String taskType) {
return aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
.eq(AiTask::getMeetingId, meetingId)
.eq(AiTask::getTaskType, taskType)
.orderByDesc(AiTask::getId)
.last("LIMIT 1"));
}
private Long resolvePromptId(AiTask summaryTask) {
if (summaryTask == null || summaryTask.getTaskConfig() == null) {
return null;
}
Object rawPromptId = summaryTask.getTaskConfig().get("promptId");
if (rawPromptId == null) {
return null;
}
if (rawPromptId instanceof Number number) {
return number.longValue();
}
String value = String.valueOf(rawPromptId).trim();
if (value.isEmpty()) {
return null;
}
try {
return Long.parseLong(value);
} catch (NumberFormatException ignored) {
return null;
}
}
private String resolvePromptName(Long promptId) {
if (promptId == null) {
return null;
}
PromptTemplate template = promptTemplateService.getById(promptId);
return template == null ? null : template.getTemplateName();
}
private List<LegacyMeetingAttendeeResponse> buildAttendees(String participants) {
return buildAttendees(parseParticipantIds(participants));
}
private List<LegacyMeetingAttendeeResponse> buildAttendees(List<Long> participantIds) {
if (participantIds == null || participantIds.isEmpty()) {
return List.of();
}
Map<Long, SysUser> userMap = sysUserMapper.selectBatchIds(participantIds).stream()
.collect(Collectors.toMap(SysUser::getUserId, user -> user, (left, right) -> left, LinkedHashMap::new));
return participantIds.stream()
.map(userId -> {
SysUser user = userMap.get(userId);
String caption = user == null
? String.valueOf(userId)
: (user.getDisplayName() != null ? user.getDisplayName() : user.getUsername());
String username = user == null ? null : user.getUsername();
return new LegacyMeetingAttendeeResponse(userId, username, caption);
})
.toList();
}
private List<LegacyMeetingTagResponse> buildTags(String rawTags) {
if (rawTags == null || rawTags.isBlank()) {
return List.of();
}
return Arrays.stream(rawTags.split(","))
.map(String::trim)
.filter(value -> !value.isEmpty())
.map(value -> new LegacyMeetingTagResponse(null, value))
.toList();
}
private List<Long> parseParticipantIds(String participants) {
if (participants == null || participants.isBlank()) {
return List.of();
}
return Arrays.stream(participants.split(","))
.map(String::trim)
.filter(value -> !value.isEmpty())
.map(value -> {
try {
return Long.parseLong(value);
} catch (NumberFormatException ignored) {
return null;
}
})
.filter(Objects::nonNull)
.toList();
}
private String resolveListSummary(Long meetingId) {
MeetingVO detail = meetingQueryService.getDetail(meetingId);
if (detail == null || detail.getSummaryContent() == null || detail.getSummaryContent().isBlank()) {
return null;
}
String summary = detail.getSummaryContent().trim();
return summary.length() <= 240 ? summary : summary.substring(0, 240);
}
private String resolveAccessPassword(Long meetingId) {
Meeting meeting = meetingService.getById(meetingId);
return meeting == null ? null : normalizeOptionalText(meeting.getAccessPassword());
}
private String resolveCreatorDisplayName(Long creatorId, String fallbackName) {
if (creatorId == null) {
return fallbackName;
}
SysUser creator = sysUserMapper.selectById(creatorId);
if (creator == null) {
return fallbackName;
}
if (creator.getDisplayName() != null && !creator.getDisplayName().isBlank()) {
return creator.getDisplayName();
}
if (creator.getUsername() != null && !creator.getUsername().isBlank()) {
return creator.getUsername();
}
return fallbackName;
}
private void normalizeMeetingAudioUrls(MeetingVO meeting) {
if (meeting == null) {
return;
}
meeting.setAudioUrl(toAbsoluteUrl(meeting.getAudioUrl()));
meeting.setPlaybackAudioUrl(toAbsoluteUrl(meeting.getPlaybackAudioUrl()));
}
private String toAbsoluteUrl(String url) {
if (url == null || url.isBlank()) {
return url;
}
String trimmedUrl = url.trim();
if (trimmedUrl.matches("^[a-zA-Z][a-zA-Z\\d+\\-.]*://.*$") || trimmedUrl.startsWith("//")) {
return trimmedUrl;
}
if (serverBaseUrl == null || serverBaseUrl.isBlank()) {
return trimmedUrl;
}
String base = serverBaseUrl.trim();
if (base.endsWith("/") && trimmedUrl.startsWith("/")) {
return base.substring(0, base.length() - 1) + trimmedUrl;
}
if (!base.endsWith("/") && !trimmedUrl.startsWith("/")) {
return base + "/" + trimmedUrl;
}
return base + trimmedUrl;
}
private boolean hasAudio(Meeting meeting) {
return meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank();
}
private boolean hasAudio(MeetingVO meeting) {
return meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank();
}
private boolean isSummaryStage(Integer meetingStatus, AiTask summaryTask) {
return Integer.valueOf(2).equals(meetingStatus) || isRunningSummary(summaryTask);
}
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
return Integer.valueOf(1).equals(meetingStatus)
|| isRunningAsr(asrTask)
|| (hasAudio && !isSummaryStage);
}
private Integer resolveRealtimeProgress(Long meetingId) {
String rawProgress = redisTemplate.opsForValue().get(RedisKeys.meetingProgressKey(meetingId));
if (rawProgress == null || rawProgress.isBlank()) {
return null;
}
try {
JsonNode progress = objectMapper.readTree(rawProgress);
return progress.hasNonNull("percent") ? progress.path("percent").asInt() : null;
} catch (Exception ignored) {
return null;
}
}
private LegacyMeetingProcessingStatusResponse processingStatus(String overallStatus, int overallProgress, String currentStage) {
return new LegacyMeetingProcessingStatusResponse(overallStatus, overallProgress, currentStage);
}
private String formatDateTime(LocalDateTime value) {
return value == null ? null : value.toString();
}
private String translateListStage(String stage) {
if (STAGE_SUMMARY_GENERATION.equals(stage)) {
return "llm";
}
if (STAGE_COMPLETED.equals(stage)) {
return "completed";
}
return "transcription";
}
private LoginUser currentLoginUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser loginUser)) {
throw new IllegalStateException("MCP login user is required");
}
return loginUser;
}
private String resolveCreatorName(LoginUser loginUser) {
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
}
private List<String> normalizeStringList(Object value) {
if (!(value instanceof List<?> list)) {
return List.of();
}
return list.stream()
.filter(Objects::nonNull)
.map(String::valueOf)
.map(String::trim)
.filter(item -> !item.isEmpty())
.toList();
}
private int normalizePositive(Integer value, int defaultValue) {
return value == null || value <= 0 ? defaultValue : value;
}
private String normalizeOptionalText(String value) {
if (value == null) {
return null;
}
String normalized = value.trim();
return normalized.isEmpty() ? null : normalized;
}
}