diff --git a/backend/src/main/java/com/imeeting/common/SysParamKeys.java b/backend/src/main/java/com/imeeting/common/SysParamKeys.java index 756bb8b..3cb0411 100644 --- a/backend/src/main/java/com/imeeting/common/SysParamKeys.java +++ b/backend/src/main/java/com/imeeting/common/SysParamKeys.java @@ -9,4 +9,7 @@ public final class SysParamKeys { public static final String MEETING_CREATE_OFFLINE_ENABLED = "meeting.create.offline_enabled"; public static final String MEETING_CREATE_REALTIME_ENABLED = "meeting.create.realtime_enabled"; public static final String MEETING_ASR_MAX_CONCURRENT = "meeting.asr.max_concurrent"; + public static final String MEETING_MAX_PAUSE_DURATION = "meeting.max_pause_duration"; + public static final String MEETING_MAX_MEETING_DURATION = "meeting.max_meeting_duration"; + public static final String MEETING_PACKET_LOSS_RATE = "meeting.packet_loss_rate"; } diff --git a/backend/src/main/java/com/imeeting/controller/android/AndroidClientController.java b/backend/src/main/java/com/imeeting/controller/android/AndroidClientController.java index 9102207..cada2ad 100644 --- a/backend/src/main/java/com/imeeting/controller/android/AndroidClientController.java +++ b/backend/src/main/java/com/imeeting/controller/android/AndroidClientController.java @@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -21,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/android/clients") @RequiredArgsConstructor +@Slf4j public class AndroidClientController { private final AndroidAuthService androidAuthService; diff --git a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java index 5ffd1c0..b8bb8d4 100644 --- a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java @@ -5,7 +5,9 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.imeeting.common.RedisKeys; +import com.imeeting.common.SysParamKeys; import com.imeeting.dto.android.AndroidAuthContext; +import com.imeeting.dto.android.AndroidMeetingConfigVo; import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest; import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse; import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest; @@ -13,19 +15,15 @@ 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.LegacyUploadAudioResponse; +import com.imeeting.dto.biz.AiModelVO; import com.imeeting.dto.biz.MeetingVO; +import com.imeeting.dto.biz.PromptTemplateVO; import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.PromptTemplate; import com.imeeting.service.android.AndroidAuthService; import com.imeeting.service.android.legacy.LegacyMeetingAdapterService; -import com.imeeting.service.biz.AiTaskService; -import com.imeeting.service.biz.MeetingAccessService; -import com.imeeting.service.biz.MeetingCommandService; -import com.imeeting.service.biz.MeetingProgressService; -import com.imeeting.service.biz.MeetingQueryService; -import com.imeeting.service.biz.MeetingService; -import com.imeeting.service.biz.PromptTemplateService; +import com.imeeting.service.biz.*; import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter; import com.unisbase.common.ApiResponse; import com.unisbase.common.annotation.Log; @@ -33,6 +31,9 @@ import com.unisbase.dto.PageResult; import com.unisbase.entity.SysUser; import com.unisbase.mapper.SysUserMapper; import com.unisbase.security.LoginUser; +import com.unisbase.service.SysDictItemService; +import com.unisbase.service.SysParamService; +import com.unisbase.service.impl.SysDictItemServiceImpl; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -54,6 +55,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Arrays; import java.util.LinkedHashMap; @@ -81,6 +83,9 @@ public class AndroidMeetingController { private final AiTaskService aiTaskService; private final PromptTemplateService promptTemplateService; private final SysUserMapper sysUserMapper; + private final AiModelService aiModelService; + private final SysDictItemService dictItemService; + private final SysParamService paramService; private final MeetingProgressService meetingProgressService; private final ObjectMapper objectMapper; @@ -94,6 +99,9 @@ public class AndroidMeetingController { AiTaskService aiTaskService, PromptTemplateService promptTemplateService, SysUserMapper sysUserMapper, + AiModelService aiModelService, + SysDictItemService dictItemService, + SysParamService paramService, MeetingProgressService meetingProgressService, ObjectMapper objectMapper) { this.androidAuthService = androidAuthService; @@ -107,6 +115,9 @@ public class AndroidMeetingController { this.sysUserMapper = sysUserMapper; this.meetingProgressService = meetingProgressService; this.objectMapper = objectMapper; + this.aiModelService = aiModelService; + this.paramService = paramService; + this.dictItemService = dictItemService; } public AndroidMeetingController(AndroidAuthService androidAuthService, @@ -118,6 +129,9 @@ public class AndroidMeetingController { AiTaskService aiTaskService, PromptTemplateService promptTemplateService, SysUserMapper sysUserMapper, + AiModelService aiModelService, + SysDictItemService dictItemService, + SysParamService paramService, StringRedisTemplate redisTemplate, ObjectMapper objectMapper) { this( @@ -130,6 +144,9 @@ public class AndroidMeetingController { aiTaskService, promptTemplateService, sysUserMapper, + aiModelService, + dictItemService, + paramService, new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper), objectMapper ); @@ -266,6 +283,43 @@ public class AndroidMeetingController { meetingCommandService.deleteMeeting(meetingId); return ApiResponse.ok(true); } + @GetMapping("/config") + @Log(value = "获取会议配置", type = "Android会议管理") + @Operation(summary = "获取会议配置") + public ApiResponse config(HttpServletRequest request) { + AndroidAuthContext authContext = androidAuthService.authenticateHttp(request); + LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext); + AndroidMeetingConfigVo resultVo = new AndroidMeetingConfigVo(); + PageResult> promptTemplateList = promptTemplateService.pageTemplates( + 1, + 1000, + null, + null, + loginUser.getTenantId(), + loginUser.getUserId(), + loginUser.getIsPlatformAdmin(), + loginUser.getIsTenantAdmin() + ); + List enabledTemplates = promptTemplateList.getRecords() == null + ? List.of() + : promptTemplateList.getRecords().stream() + .filter(item -> Integer.valueOf(1).equals(item.getStatus())) + .toList(); + resultVo.setTemplateList(enabledTemplates); + PageResult> modelList = aiModelService.pageModels(1, 1000, null, "LLM", loginUser.getTenantId()); + List enabledModels = modelList.getRecords() == null + ? List.of() + : modelList.getRecords().stream() + .filter(item -> Integer.valueOf(1).equals(item.getStatus())) + .toList(); + resultVo.setModelsList(enabledModels); + resultVo.setSummaryDegreeOfDetail(dictItemService.getItemsByTypeCode("summary_degree_detail")); + resultVo.setMaxMeetingDuration(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_MAX_MEETING_DURATION,"30"))); + resultVo.setMaxPauseDuration(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_MAX_PAUSE_DURATION,String.valueOf(60*4)))); + resultVo.setPacketLossRate(new BigDecimal(paramService.getParamValue(SysParamKeys.MEETING_MAX_PAUSE_DURATION,"99"))); + + return ApiResponse.ok(resultVo); + } private LegacyMeetingPreviewResult buildPreviewResult(Long meetingId) { Meeting meeting = meetingService.getById(meetingId); @@ -291,7 +345,7 @@ public class AndroidMeetingController { buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可查看详情", 100, STAGE_COMPLETED)) ); } - if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) { + if (isFailed(asrTask)) { return new LegacyMeetingPreviewResult( "503", buildFailureMessage(asrTask, "转写"), diff --git a/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyMeetingController.java b/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyMeetingController.java index b29d568..69ec57e 100644 --- a/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyMeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/android/legacy/LegacyMeetingController.java @@ -283,7 +283,7 @@ public class LegacyMeetingController { // buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED)) // ); // } - if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) { + if (isFailed(asrTask)) { return new LegacyMeetingPreviewResult( "503", buildFailureMessage(asrTask, "转译"), @@ -440,7 +440,7 @@ public class LegacyMeetingController { if (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted) { return new LegacyMeetingProcessingStatusResponse("completed", 100, STAGE_COMPLETED); } - if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) { + if (isFailed(asrTask)) { return new LegacyMeetingProcessingStatusResponse("failed", 50, STAGE_AUDIO_TRANSCRIPTION); } if (isFailed(summaryTask)) { diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java index a589dd1..eb45511 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java @@ -531,6 +531,28 @@ public class MeetingController { return ApiResponse.ok(true); } + @Operation(summary = "重试 AI 目录") + @PostMapping("/{id}/chapters/retry") + @PreAuthorize("isAuthenticated()") + public ApiResponse retryChapter(@PathVariable Long id) { + LoginUser loginUser = currentLoginUser(); + Meeting meeting = meetingAccessService.requireMeeting(id); + meetingAccessService.assertCanEditMeeting(meeting, loginUser); + meetingCommandService.retryChapter(id); + return ApiResponse.ok(true); + } + + @Operation(summary = "重试会议总结") + @PostMapping("/{id}/summary/retry") + @PreAuthorize("isAuthenticated()") + public ApiResponse retrySummary(@PathVariable Long id) { + LoginUser loginUser = currentLoginUser(); + Meeting meeting = meetingAccessService.requireMeeting(id); + meetingAccessService.assertCanEditMeeting(meeting, loginUser); + meetingCommandService.retrySummary(id); + return ApiResponse.ok(true); + } + @Operation(summary = "更新会议基础信息") @PutMapping("/{id}/basic") @PreAuthorize("isAuthenticated()") diff --git a/backend/src/main/java/com/imeeting/dto/android/AndroidMeetingConfigVo.java b/backend/src/main/java/com/imeeting/dto/android/AndroidMeetingConfigVo.java new file mode 100644 index 0000000..28846ed --- /dev/null +++ b/backend/src/main/java/com/imeeting/dto/android/AndroidMeetingConfigVo.java @@ -0,0 +1,34 @@ +package com.imeeting.dto.android; + + +import com.imeeting.dto.biz.AiModelVO; +import com.imeeting.dto.biz.PromptTemplateVO; +import com.unisbase.dto.SysDictItemDTO; +import com.unisbase.entity.SysDictItem; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * @author : ch + * @version : 1.0 + * @ClassName : AndroidMeetingConfigVo + * @Description : + * @DATE : Created in 17:02 2026/5/27 + *
       Copyright: Copyright(c) 2026     
+ *
       Company :   	紫光汇智信息技术有限公司		           
+ * Modification History: + * Date Author Version Discription + * -------------------------------------------------------------------------- + * 2026/05/27 ch 1.0 Why & What is modified: <修改原因描述> * + */ +@Data +public class AndroidMeetingConfigVo { + private List modelsList; + private List templateList; + private List summaryDegreeOfDetail; + private Integer maxPauseDuration; + private Integer maxMeetingDuration; + private BigDecimal packetLossRate; +} diff --git a/backend/src/main/java/com/imeeting/dto/biz/MeetingSummaryFinalizeDTO.java b/backend/src/main/java/com/imeeting/dto/biz/MeetingSummaryFinalizeDTO.java index d391c26..ff32abd 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/MeetingSummaryFinalizeDTO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/MeetingSummaryFinalizeDTO.java @@ -21,8 +21,7 @@ public class MeetingSummaryFinalizeDTO { @Schema(description = "转录指纹") private String sourceFingerprint; - @NotNull(message = "chapterVersionId must not be null") - @Schema(description = "章节版本ID") + @Schema(description = "章节版本ID,可选,仅用于审计记录") private Long chapterVersionId; @NotBlank(message = "summaryContent must not be blank") diff --git a/backend/src/main/java/com/imeeting/service/android/impl/AndroidAuthServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/impl/AndroidAuthServiceImpl.java index bdab808..3d9cb2c 100644 --- a/backend/src/main/java/com/imeeting/service/android/impl/AndroidAuthServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/impl/AndroidAuthServiceImpl.java @@ -11,6 +11,7 @@ import com.unisbase.security.LoginUser; import com.unisbase.service.TokenValidationService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -18,6 +19,7 @@ import org.springframework.util.StringUtils; @Service @RequiredArgsConstructor +@Slf4j public class AndroidAuthServiceImpl implements AndroidAuthService { private static final String HEADER_DEVICE_ID = "X-Android-Device-Id"; @@ -50,8 +52,8 @@ public class AndroidAuthServiceImpl implements AndroidAuthService { String platform = request.getHeader(HEADER_PLATFORM); requireAndroidHttpHeaders(deviceId, appVersion, platform); + log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}",deviceId,appVersion,platform); assertDeviceEnabled(deviceId); - if (loginUser != null) { return buildContext("USER_JWT", false, deviceId, diff --git a/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java b/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java index 193613c..853b5db 100644 --- a/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java +++ b/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java @@ -5,5 +5,7 @@ import com.imeeting.entity.biz.AiTask; public interface AiTaskService extends IService { void dispatchTasks(Long meetingId, Long tenantId, Long userId); + void dispatchChapterTask(Long meetingId, Long tenantId, Long userId); void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId); + void reconcileMeetingStatus(Long meetingId); } diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java b/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java index 16a036c..e6cd734 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java @@ -41,6 +41,10 @@ public interface MeetingCommandService { void retryTranscription(Long meetingId); + void retrySummary(Long meetingId); + + void retryChapter(Long meetingId); + MeetingTranscriptChapterImportResultVO importTranscriptChapters(MeetingTranscriptChapterImportDTO command); void finalizeSummary(MeetingSummaryFinalizeDTO command); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index 491d658..5f88d56 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -10,6 +10,7 @@ import com.imeeting.common.MeetingProgressStage; import com.imeeting.common.RedisKeys; import com.imeeting.dto.biz.AiModelVO; import com.imeeting.dto.biz.MeetingSummarySource; +import com.imeeting.dto.biz.MeetingTranscriptSourceVO; import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.HotWord; import com.imeeting.entity.biz.Meeting; @@ -247,30 +248,14 @@ public class AiTaskServiceImpl extends ServiceImpl impleme if (!asrText.isBlank()) { 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()); return; } - if (chapterTask != null && canExecuteTask(chapterTask)) { - executeChapterFlow(meeting, chapterTask); - } - if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus()) && hasDetailedChapterFailureMessage(chapterTask)) { - String chapterFailureMessage = buildChapterFailurePropagationMessage(chapterTask); - failPendingSummaryTask(sumTask, chapterFailureMessage); - updateMeetingStatus(meetingId, 4); - updateProgress(meetingId, -1, chapterFailureMessage, 0); - return; - } - if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus())) { - failPendingSummaryTask(sumTask, "章节生成失败,无法继续总结"); - updateMeetingStatus(meetingId, 4); - updateProgress(meetingId, -1, "章节生成失败,无法继续总结", 0); - return; - } if (sumTask != null && canExecuteTask(sumTask)) { - executeSummaryFlow(meeting, sumTask, chapterTask); - } else if (meeting.getStatus() != 3) { - updateMeetingStatus(meetingId, 3); + executeSummaryFlow(meeting, sumTask); } + reconcileMeetingStatus(meetingId); } catch (Exception e) { log.error("Meeting {} AI Task Flow failed", meetingId, e); failPendingSummaryTask(findLatestSummaryTask(meetingId), "转录失败,已跳过总结任务: " + e.getMessage()); @@ -282,6 +267,31 @@ public class AiTaskServiceImpl extends ServiceImpl impleme } } + @Override + @Async("summaryDispatchExecutor") + public void dispatchChapterTask(Long meetingId, Long tenantId, Long userId) { + Runnable task = () -> taskSecurityContextRunner.runAsTenantUser(tenantId, userId, () -> doDispatchChapterTask(meetingId)); + if (summaryTaskExecutor == null) { + task.run(); + return; + } + summaryTaskExecutor.execute(task); + } + + private void doDispatchChapterTask(Long meetingId) { + Meeting meeting = meetingMapper.selectById(meetingId); + if (meeting == null) { + return; + } + AiTask chapterTask = findLatestTask(meetingId, "CHAPTER"); + if (chapterTask == null || !canExecuteTask(chapterTask)) { + reconcileMeetingStatus(meetingId); + return; + } + executeChapterFlow(meeting, chapterTask); + reconcileMeetingStatus(meetingId); + } + @Override @Async("summaryDispatchExecutor") public void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId) { @@ -304,32 +314,15 @@ public class AiTaskServiceImpl extends ServiceImpl impleme triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_SUMMARY_DISPATCH", false); return; } - AiTask chapterTask = findLatestTask(meetingId, "CHAPTER"); AiTask sumTask = findLatestTask(meetingId, "SUMMARY"); try { - if (chapterTask != null && canExecuteTask(chapterTask)) { - executeChapterFlow(meeting, chapterTask); - } - if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus()) && hasDetailedChapterFailureMessage(chapterTask)) { - String chapterFailureMessage = buildChapterFailurePropagationMessage(chapterTask); - failPendingSummaryTask(sumTask, chapterFailureMessage); - updateMeetingStatus(meetingId, 4); - updateProgress(meetingId, -1, chapterFailureMessage, 0); - return; - } - if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus())) { - failPendingSummaryTask(sumTask, "章节生成失败,无法继续总结"); - updateMeetingStatus(meetingId, 4); - updateProgress(meetingId, -1, "章节生成失败,无法继续总结", 0); - return; - } if (sumTask != null && canExecuteTask(sumTask)) { - executeSummaryFlow(meeting, sumTask, chapterTask); + executeSummaryFlow(meeting, sumTask); } + reconcileMeetingStatus(meetingId); } catch (Exception e) { log.error("Re-summary failed for meeting {}", meetingId, e); - updateMeetingStatus(meetingId, 4); - updateProgress(meetingId, -1, "Summary flow failed: " + e.getMessage(), 0); + reconcileMeetingStatus(meetingId); } } @@ -994,10 +987,14 @@ public class AiTaskServiceImpl extends ServiceImpl impleme this.updateById(taskRecord); meeting.setLatestSummaryTaskId(taskRecord.getId()); - meeting.setStatus(3); meetingMapper.updateById(meeting); - updateProgress(meeting.getId(), 100, "全流程分析完成", 0); + AiTask latestChapterTask = findLatestTask(meeting.getId(), "CHAPTER"); + if (latestChapterTask != null && Integer.valueOf(2).equals(latestChapterTask.getStatus())) { + updateProgress(meeting.getId(), 100, "全流程分析完成", 0); + } else { + updateProgress(meeting.getId(), 95, "总结生成完成,等待 AI 目录完成...", 0); + } } else { updateAiTaskFail(taskRecord, "LLM 总结失败: " + response.body()); throw new RuntimeException("AI总结生成异常"); @@ -1048,54 +1045,9 @@ public class AiTaskServiceImpl extends ServiceImpl impleme } } - private MeetingSummarySource restorePreparedSummarySource(AiTask chapterTask) { - if (chapterTask == null || chapterTask.getResponseData() == null) { - return null; - } - String rawTranscriptText = stringValue(chapterTask.getResponseData().get("rawTranscriptText")); - String chapterOutlineText = stringValue(chapterTask.getResponseData().get("chapterOutlineText")); - String text = stringValue(chapterTask.getResponseData().get("summarySourceText")); - if ((rawTranscriptText == null || rawTranscriptText.isBlank()) - && (text == null || text.isBlank())) { - return null; - } - - Object summarySourceSnapshot = chapterTask.getResponseData().get("summarySource"); - Map snapshot = summarySourceSnapshot instanceof Map map ? map : Map.of(); - if (text == null || text.isBlank()) { - text = rawTranscriptText != null && !rawTranscriptText.isBlank() - ? rawTranscriptText - : chapterOutlineText; - } - return MeetingSummarySource.builder() - .text(text) - .sourceType(stringValue(snapshot.get("sourceType"))) - .fallbackUsed(Boolean.TRUE.equals(snapshot.get("fallbackUsed"))) - .sourceFingerprint(firstNonBlank( - stringValue(snapshot.get("sourceFingerprint")), - stringValue(chapterTask.getResponseData().get("sourceFingerprint")) - )) - .chapterVersionId(longValue(firstNonNull( - snapshot.get("chapterVersionId"), - chapterTask.getResponseData().get("chapterVersionId") - ))) - .chapterCount(intValue(firstNonNull( - snapshot.get("chapterCount"), - chapterTask.getResponseData().get("chapterCount") - ))) - .algorithmVersion(stringValue(snapshot.get("algorithmVersion"))) - .generationMode(stringValue(snapshot.get("generationMode"))) - .rawTranscriptText(rawTranscriptText) - .chapterOutlineText(chapterOutlineText) - .chapterFilePath(firstNonBlank( - stringValue(snapshot.get("chapterFilePath")), - stringValue(chapterTask.getResponseData().get("chapterFilePath")) - )) - .build(); - } - - private void executeSummaryFlow(Meeting meeting, AiTask sumTask, AiTask chapterTask) throws Exception { + private void executeSummaryFlow(Meeting meeting, AiTask sumTask) throws Exception { if (isExternalSummaryModeEnabled()) { + AiTask chapterTask = findLatestTask(meeting.getId(), "CHAPTER"); triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_AFTER_TRANSCRIPT_READY", false); return; } @@ -1106,22 +1058,33 @@ public class AiTaskServiceImpl extends ServiceImpl impleme return; } try { - MeetingSummarySource summarySource = restorePreparedSummarySource(chapterTask); - if (summarySource == null || summarySource.getText() == null || summarySource.getText().isBlank()) { - summarySource = meetingTranscriptChapterService.resolveSummarySource(meeting, chapterTask != null ? chapterTask : sumTask); - } + MeetingSummarySource summarySource = buildRawTranscriptSummarySource(meeting); if (summarySource.getText() == null || summarySource.getText().isBlank()) { failPendingSummaryTask(sumTask, "没有转录内容"); - updateMeetingStatus(meeting.getId(), 4); - updateProgress(meeting.getId(), -1, "没有转录内容", 0); + reconcileMeetingStatus(meeting.getId()); return; } processSummaryTask(meeting, summarySource, sumTask); + reconcileMeetingStatus(meeting.getId()); } finally { redisValueSupport.delete(summaryLockKey); } } + private MeetingSummarySource buildRawTranscriptSummarySource(Meeting meeting) { + MeetingTranscriptSourceVO transcriptSource = meetingTranscriptChapterService.buildTranscriptSource(meeting.getId()); + String transcriptText = transcriptSource == null ? null : stringValue(transcriptSource.getTranscriptText()); + return MeetingSummarySource.builder() + .text(transcriptText) + .sourceType("RAW_TRANSCRIPT") + .fallbackUsed(false) + .sourceFingerprint(transcriptSource == null ? null : transcriptSource.getSourceFingerprint()) + .generationMode("NONE") + .rawTranscriptText(transcriptText) + .chapterOutlineText("") + .build(); + } + private AiTask findLatestTask(Long meetingId, String taskType) { return this.getOne(new LambdaQueryWrapper() .eq(AiTask::getMeetingId, meetingId) @@ -1176,24 +1139,37 @@ public class AiTaskServiceImpl extends ServiceImpl impleme && !Integer.valueOf(3).equals(task.getStatus()); } - private boolean hasDetailedChapterFailureMessage(AiTask chapterTask) { - String failureMessage = buildChapterFailurePropagationMessage(chapterTask); - return failureMessage != null && !failureMessage.isBlank() && !failureMessage.equals("章节生成失败,无法继续总结"); + public void reconcileMeetingStatus(Long meetingId) { + if (meetingId == null) { + return; + } + AiTask asrTask = findLatestTask(meetingId, "ASR"); + AiTask chapterTask = findLatestTask(meetingId, "CHAPTER"); + AiTask summaryTask = findLatestTask(meetingId, "SUMMARY"); + + if (isTaskFailed(asrTask) || isTaskFailed(chapterTask) || isTaskFailed(summaryTask)) { + updateMeetingStatus(meetingId, 4); + return; + } + if (isTaskCompleted(chapterTask) && isTaskCompleted(summaryTask)) { + updateMeetingStatus(meetingId, 3); + return; + } + if (isTaskCompleted(asrTask) || isTaskRunningOrQueued(chapterTask) || isTaskRunningOrQueued(summaryTask)) { + updateMeetingStatus(meetingId, 2); + } } - private String buildChapterFailurePropagationMessage(AiTask chapterTask) { - if (chapterTask == null) { - return "章节生成失败,无法继续总结"; - } - String detail = firstNonBlank( - chapterTask.getErrorMsg(), - stringValue(chapterTask.getResponseData() == null ? null : chapterTask.getResponseData().get("failureReason")), - stringValue(chapterTask.getResponseData() == null ? null : chapterTask.getResponseData().get("exceptionMessage")) - ); - if (detail == null) { - return "章节生成失败,无法继续总结"; - } - return "章节生成失败,无法继续总结: " + detail; + private boolean isTaskCompleted(AiTask task) { + return task != null && Integer.valueOf(2).equals(task.getStatus()); + } + + private boolean isTaskFailed(AiTask task) { + return task != null && Integer.valueOf(3).equals(task.getStatus()); + } + + private boolean isTaskRunningOrQueued(AiTask task) { + return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus())); } private String stringValue(Object value) { @@ -1212,18 +1188,6 @@ public class AiTaskServiceImpl extends ServiceImpl impleme return null; } - private Object firstNonNull(Object... values) { - if (values == null) { - return null; - } - for (Object value : values) { - if (value != null) { - return value; - } - } - return null; - } - private Long longValue(Object value) { if (value == null) { return null; @@ -1235,17 +1199,6 @@ public class AiTaskServiceImpl extends ServiceImpl impleme } } - private Integer intValue(Object value) { - if (value == null) { - return null; - } - try { - return Integer.parseInt(String.valueOf(value).trim()); - } catch (Exception ex) { - return null; - } - } - private AiModelVO resolveAsrModelForRevision(AiTask asrTask) { if (asrTask == null || asrTask.getTaskConfig() == null) { return null; diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java index 7e480a8..de26b01 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java @@ -12,6 +12,7 @@ import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO; import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO; import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO; import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO; +import com.imeeting.dto.biz.MeetingTranscriptSourceVO; import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile; import com.imeeting.dto.biz.RealtimeMeetingResumeConfig; @@ -428,8 +429,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { realtimeMeetingSessionStateService.clear(meetingId); meeting.setStatus(2); meetingService.updateById(meeting); - updateMeetingProgress(meetingId, 90, "正在生成会议总结...", 0); + updateMeetingProgress(meetingId, 85, "正在生成 AI 目录与总结...", 0); meetingDomainSupport.prewarmPlaybackAudioAfterCommit(meeting.getAudioUrl()); + aiTaskService.dispatchChapterTask(meetingId, meeting.getTenantId(), meeting.getCreatorId()); aiTaskService.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId()); } @@ -501,6 +503,15 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { resetAiTask(summaryTask, summaryTask.getTaskConfig()); aiTaskService.updateById(summaryTask); + AiTask chapterTask = aiTaskService.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meetingId) + .eq(AiTask::getTaskType, "CHAPTER") + .orderByDesc(AiTask::getId) + .last("LIMIT 1")); + if (chapterTask != null) { + resetAiTask(chapterTask, chapterTask.getTaskConfig()); + aiTaskService.updateById(chapterTask); + } } private void resetAiTask(AiTask task, Map taskConfig) { @@ -746,14 +757,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { throw new RuntimeException("总结任务不存在或不属于当前会议"); } - MeetingTranscriptChapterVersion currentVersion = meetingTranscriptChapterService.getCurrentVersion(meeting.getId()); - if (currentVersion == null) { - throw new RuntimeException("当前会议不存在有效章节版本"); - } - if (!Objects.equals(currentVersion.getId(), command.getChapterVersionId())) { - throw new RuntimeException("章节版本不是当前生效版本,拒绝回填总结"); - } - if (!Objects.equals(currentVersion.getSourceFingerprint(), command.getSourceFingerprint())) { + MeetingTranscriptSourceVO transcriptSource = meetingTranscriptChapterService.buildTranscriptSource(meeting.getId()); + String currentFingerprint = transcriptSource == null ? null : transcriptSource.getSourceFingerprint(); + if (currentFingerprint == null || !Objects.equals(currentFingerprint, command.getSourceFingerprint())) { throw new RuntimeException("转录指纹已变化,拒绝回填过期总结结果"); } @@ -764,12 +770,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { ? new HashMap<>() : new HashMap<>(summaryTask.getResponseData()); Map summarySource = new HashMap<>(); - summarySource.put("sourceType", "CHAPTER_VERSION"); - summarySource.put("chapterVersionId", currentVersion.getId()); - summarySource.put("chapterCount", currentVersion.getChapterCount()); - summarySource.put("sourceFingerprint", currentVersion.getSourceFingerprint()); - summarySource.put("algorithmVersion", currentVersion.getAlgorithmVersion()); - summarySource.put("generationMode", currentVersion.getGenerationMode()); + summarySource.put("sourceType", "RAW_TRANSCRIPT"); + summarySource.put("sourceFingerprint", currentFingerprint); + if (command.getChapterVersionId() != null) { + summarySource.put("chapterVersionId", command.getChapterVersionId()); + } responseData.put("summarySource", summarySource); Map summaryBundle = new HashMap<>(); @@ -786,9 +791,19 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { aiTaskService.updateById(summaryTask); meeting.setLatestSummaryTaskId(summaryTask.getId()); - meeting.setStatus(3); meetingService.updateById(meeting); - updateMeetingProgress(meeting.getId(), 100, "外部总结回填完成", 0); + aiTaskService.reconcileMeetingStatus(meeting.getId()); + + AiTask latestChapterTask = aiTaskService.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meeting.getId()) + .eq(AiTask::getTaskType, "CHAPTER") + .orderByDesc(AiTask::getId) + .last("LIMIT 1")); + if (latestChapterTask != null && Integer.valueOf(2).equals(latestChapterTask.getStatus())) { + updateMeetingProgress(meeting.getId(), 100, "外部总结回填完成", 0); + } else { + updateMeetingProgress(meeting.getId(), 95, "外部总结回填完成,等待 AI 目录完成...", 0); + } } @Override @@ -870,16 +885,15 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { if ("CHAPTER".equals(stage)) { markTaskFailed(chapterTask, "外部章节编排失败: " + errorMessage, command.getRawError()); - markTaskFailed(summaryTask, "外部章节编排失败,无法继续总结: " + errorMessage, command.getRawError()); } else { markTaskFailed(summaryTask, "外部总结编排失败: " + errorMessage, command.getRawError()); } if (summaryTask != null) { meeting.setLatestSummaryTaskId(summaryTask.getId()); + meetingService.updateById(meeting); } - meeting.setStatus(4); - meetingService.updateById(meeting); + aiTaskService.reconcileMeetingStatus(meeting.getId()); updateMeetingProgress(meeting.getId(), -1, errorMessage, 0); } @@ -892,34 +906,20 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { } String effectiveSummaryDetailLevel = resolveSummaryDetailLevel(summaryDetailLevel != null ? summaryDetailLevel : meeting.getSummaryDetailLevel()); - Long effectiveChapterModelId = chapterModelId != null ? chapterModelId : summaryModelId; - meetingDomainSupport.createChapterTask( + meetingDomainSupport.createSummaryTask( meetingId, summaryModelId, - effectiveChapterModelId, promptId, userPrompt, effectiveSummaryDetailLevel ); - if (Objects.equals(effectiveChapterModelId, summaryModelId)) { - meetingDomainSupport.createSummaryTask(meetingId, summaryModelId, promptId, userPrompt, effectiveSummaryDetailLevel); - } else { - meetingDomainSupport.createSummaryTask( - meetingId, - summaryModelId, - effectiveChapterModelId, - promptId, - userPrompt, - effectiveSummaryDetailLevel - ); - } meeting.setSummaryDetailLevel(effectiveSummaryDetailLevel); meeting.setStatus(2); meetingService.updateById(meeting); if ("EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) { - updateMeetingProgress(meetingId, 95, "等待外部章节与总结编排...", 0); + updateMeetingProgress(meetingId, 95, "等待外部总结编排...", 0); } else { - updateMeetingProgress(meetingId, 85, "重新总结已提交,正在生成章节...", 0); + updateMeetingProgress(meetingId, 90, "重新总结已提交,正在生成总结...", 0); } dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); } @@ -996,6 +996,74 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); } + @Override + @Transactional(rollbackFor = Exception.class) + public void retrySummary(Long meetingId) { + Meeting meeting = meetingService.getById(meetingId); + if (meeting == null) { + throw new RuntimeException("会议不存在"); + } + if (meeting.getStatus() == 2) { + throw new RuntimeException("当前会议仍在处理中,请稍后再试"); + } + long transcriptCount = transcriptMapper.selectCount(new LambdaQueryWrapper() + .eq(MeetingTranscript::getMeetingId, meetingId)); + if (transcriptCount <= 0) { + throw new RuntimeException("当前会议没有可用转录,无法重试总结"); + } + AiTask summaryTask = aiTaskService.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meetingId) + .eq(AiTask::getTaskType, "SUMMARY") + .orderByDesc(AiTask::getId) + .last("LIMIT 1")); + if (summaryTask == null || summaryTask.getTaskConfig() == null) { + throw new RuntimeException("未找到可用的总结任务配置"); + } + if (!Integer.valueOf(3).equals(summaryTask.getStatus())) { + throw new RuntimeException("当前总结环节未失败,无需重试"); + } + resetAiTask(summaryTask, new HashMap<>(summaryTask.getTaskConfig())); + aiTaskService.updateById(summaryTask); + meeting.setStatus(2); + meetingService.updateById(meeting); + updateMeetingProgress(meetingId, 90, "已重新提交总结任务,正在生成总结...", 0); + dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void retryChapter(Long meetingId) { + Meeting meeting = meetingService.getById(meetingId); + if (meeting == null) { + throw new RuntimeException("会议不存在"); + } + if (meeting.getStatus() == 2) { + throw new RuntimeException("当前会议仍在处理中,请稍后再试"); + } + long transcriptCount = transcriptMapper.selectCount(new LambdaQueryWrapper() + .eq(MeetingTranscript::getMeetingId, meetingId)); + if (transcriptCount <= 0) { + throw new RuntimeException("当前会议没有可用转录,无法重试 AI 目录"); + } + AiTask chapterTask = aiTaskService.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meetingId) + .eq(AiTask::getTaskType, "CHAPTER") + .orderByDesc(AiTask::getId) + .last("LIMIT 1")); + if (chapterTask == null || chapterTask.getTaskConfig() == null) { + throw new RuntimeException("未找到可用的 AI 目录任务配置"); + } + if (!Integer.valueOf(3).equals(chapterTask.getStatus())) { + throw new RuntimeException("当前 AI 目录环节未失败,无需重试"); + } + resetAiTask(chapterTask, new HashMap<>(chapterTask.getTaskConfig())); + aiTaskService.updateById(chapterTask); + meeting.setStatus(2); + meetingService.updateById(meeting); + updateMeetingProgress(meetingId, 85, "已重新提交 AI 目录任务,正在生成目录...", 0); + dispatchChapterTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); + } + private void clearLegacyDispatchState(Long meetingId) { if (compatibilityRedisTemplate == null || meetingId == null) { return; @@ -1092,6 +1160,19 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { aiTaskService.updateById(task); } + private void dispatchChapterTaskAfterCommit(Long meetingId, Long tenantId, Long userId) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + aiTaskService.dispatchChapterTask(meetingId, tenantId, userId); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + aiTaskService.dispatchChapterTask(meetingId, tenantId, userId); + } + }); + } + private void dispatchSummaryTaskAfterCommit(Long meetingId, Long tenantId, Long userId) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { aiTaskService.dispatchSummaryTask(meetingId, tenantId, userId); diff --git a/backend/src/main/java/com/imeeting/service/mcp/MeetingMcpToolService.java b/backend/src/main/java/com/imeeting/service/mcp/MeetingMcpToolService.java index eb6cb21..d675482 100644 --- a/backend/src/main/java/com/imeeting/service/mcp/MeetingMcpToolService.java +++ b/backend/src/main/java/com/imeeting/service/mcp/MeetingMcpToolService.java @@ -225,7 +225,7 @@ public class MeetingMcpToolService { if (summaryCompleted) { return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask)); } - if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) { + if (isFailed(asrTask)) { return new LegacyMeetingPreviewResult( "503", buildFailureMessage(asrTask, "转译"), @@ -369,7 +369,7 @@ public class MeetingMcpToolService { if (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted) { return new LegacyMeetingProcessingStatusResponse("completed", 100, STAGE_COMPLETED); } - if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) { + if (isFailed(asrTask)) { return new LegacyMeetingProcessingStatusResponse("failed", 50, STAGE_AUDIO_TRANSCRIPTION); } if (isFailed(summaryTask)) { diff --git a/frontend/src/api/business/meeting.ts b/frontend/src/api/business/meeting.ts index f66c97b..688f819 100644 --- a/frontend/src/api/business/meeting.ts +++ b/frontend/src/api/business/meeting.ts @@ -380,6 +380,20 @@ export const retryMeetingTranscription = (meetingId: number) => { ); }; +export const retryMeetingSummary = (meetingId: number) => { + return http.post<{ code: string; data: boolean; msg: string }>( + `/api/biz/meeting/${meetingId}/summary/retry`, + {} + ); +}; + +export const retryMeetingChapter = (meetingId: number) => { + return http.post<{ code: string; data: boolean; msg: string }>( + `/api/biz/meeting/${meetingId}/chapters/retry`, + {} + ); +}; + export const updateMeetingBasic = (data: UpdateMeetingBasicCommand) => { return http.put<{ code: string; data: boolean; msg: string }>( `/api/biz/meeting/${data.meetingId}/basic`, diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index 2eec373..6423cbd 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -40,6 +40,8 @@ import { reSummary, resolveAudioMimeType, resolveMeetingPlaybackAudioUrl, + retryMeetingChapter, + retryMeetingSummary, retryMeetingTranscription, updateMeetingBasic, updateMeetingTranscript, @@ -1015,8 +1017,15 @@ const MeetingDetail: React.FC = () => { return false; }, [meeting]); - const canRetrySummary = isOwner && transcripts.length > 0 && meeting?.status !== 1 && meeting?.status !== 2; + const canRetrySummary = isOwner + && transcripts.length > 0 + && meeting?.status !== 1 + && meeting?.status !== 2 + && meeting?.latestSummaryAttemptStatus !== 3 + && 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 hasSummaryContent = Boolean(meeting?.summaryContent?.trim()); const hasCatalogContent = catalogChapterLinks.length > 0; const generationFailureNotice = useMemo(() => { @@ -1362,13 +1371,11 @@ const MeetingDetail: React.FC = () => { setMeeting((current) => current ? { ...current, status: 2, - latestChapterAttemptStatus: 0, - latestChapterAttemptErrorMsg: undefined, latestSummaryAttemptStatus: 0, latestSummaryAttemptErrorMsg: undefined, } : current); setGenerationProgress({ - percent: 85, + percent: 90, message: '已重新发起总结任务', updateAt: Date.now(), eta: 0, @@ -1455,6 +1462,44 @@ const MeetingDetail: React.FC = () => { } }; + const handleRetrySummaryTask = async () => { + setActionLoading(true); + try { + await retryMeetingSummary(Number(id)); + message.success('已重新提交总结任务'); + setMeeting((current) => current ? { + ...current, + status: 2, + latestSummaryAttemptStatus: 0, + latestSummaryAttemptErrorMsg: undefined, + } : current); + await fetchData(Number(id)); + } catch (error) { + console.error(error); + } finally { + setActionLoading(false); + } + }; + + const handleRetryChapterTask = async () => { + setActionLoading(true); + try { + await retryMeetingChapter(Number(id)); + message.success('已重新提交 AI 目录任务'); + setMeeting((current) => current ? { + ...current, + status: 2, + latestChapterAttemptStatus: 0, + latestChapterAttemptErrorMsg: undefined, + } : current); + await fetchData(Number(id)); + } catch (error) { + console.error(error); + } finally { + setActionLoading(false); + } + }; + const handleKeywordToggle = (keyword: string, checked: boolean) => { setSelectedKeywords((current) => { if (checked) { @@ -1976,6 +2021,16 @@ const MeetingDetail: React.FC = () => { 重新总结 )} + {canRetryFailedChapterTask && ( + + )} + {canRetryFailedSummaryTask && ( + + )} {canRetryTranscription && (