feat: 添加总结和章节任务重试功能并优化会议处理逻辑

- 在 `MeetingCommandServiceImpl` 中添加 `retrySummary` 和 `retryChapter` 方法,支持总结和章节任务的重试
- 更新 `MeetingCommandService` 接口以包含新的重试方法
- 优化 `executeSummaryFlow` 和 `doDispatchChapterTask` 方法,简化任务执行逻辑
- 更新 `finalizeSummary` 方法,移除不必要的章节版本检查
- 调整 `updateMeetingProgress` 的进度值和消息,更准确地反映任务状态
- 在前端 `MeetingDetail.tsx` 中添加重试按钮和相关逻辑,支持用户手动重试失败的任务
dev_na
chenhao 2026-05-27 17:44:15 +08:00
parent 892275bc65
commit 384494d9ff
15 changed files with 414 additions and 189 deletions

View File

@ -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_OFFLINE_ENABLED = "meeting.create.offline_enabled";
public static final String MEETING_CREATE_REALTIME_ENABLED = "meeting.create.realtime_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_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";
} }

View File

@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -21,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequestMapping("/api/android/clients") @RequestMapping("/api/android/clients")
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class AndroidClientController { public class AndroidClientController {
private final AndroidAuthService androidAuthService; private final AndroidAuthService androidAuthService;

View File

@ -5,7 +5,9 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
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.RedisKeys; import com.imeeting.common.RedisKeys;
import com.imeeting.common.SysParamKeys;
import com.imeeting.dto.android.AndroidAuthContext; 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.LegacyMeetingAccessPasswordRequest;
import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse; import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest; 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.LegacyMeetingPreviewResult;
import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse; import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse;
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse; import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
import com.imeeting.dto.biz.AiModelVO;
import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.dto.biz.PromptTemplateVO;
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.PromptTemplate; import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.service.android.AndroidAuthService; import com.imeeting.service.android.AndroidAuthService;
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService; import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.*;
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.impl.RedisOnlyMeetingProgressServiceAdapter; import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
import com.unisbase.common.ApiResponse; import com.unisbase.common.ApiResponse;
import com.unisbase.common.annotation.Log; import com.unisbase.common.annotation.Log;
@ -33,6 +31,9 @@ import com.unisbase.dto.PageResult;
import com.unisbase.entity.SysUser; import com.unisbase.entity.SysUser;
import com.unisbase.mapper.SysUserMapper; import com.unisbase.mapper.SysUserMapper;
import com.unisbase.security.LoginUser; 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.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; 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 org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -81,6 +83,9 @@ public class AndroidMeetingController {
private final AiTaskService aiTaskService; private final AiTaskService aiTaskService;
private final PromptTemplateService promptTemplateService; private final PromptTemplateService promptTemplateService;
private final SysUserMapper sysUserMapper; private final SysUserMapper sysUserMapper;
private final AiModelService aiModelService;
private final SysDictItemService dictItemService;
private final SysParamService paramService;
private final MeetingProgressService meetingProgressService; private final MeetingProgressService meetingProgressService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@ -94,6 +99,9 @@ public class AndroidMeetingController {
AiTaskService aiTaskService, AiTaskService aiTaskService,
PromptTemplateService promptTemplateService, PromptTemplateService promptTemplateService,
SysUserMapper sysUserMapper, SysUserMapper sysUserMapper,
AiModelService aiModelService,
SysDictItemService dictItemService,
SysParamService paramService,
MeetingProgressService meetingProgressService, MeetingProgressService meetingProgressService,
ObjectMapper objectMapper) { ObjectMapper objectMapper) {
this.androidAuthService = androidAuthService; this.androidAuthService = androidAuthService;
@ -107,6 +115,9 @@ public class AndroidMeetingController {
this.sysUserMapper = sysUserMapper; this.sysUserMapper = sysUserMapper;
this.meetingProgressService = meetingProgressService; this.meetingProgressService = meetingProgressService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.aiModelService = aiModelService;
this.paramService = paramService;
this.dictItemService = dictItemService;
} }
public AndroidMeetingController(AndroidAuthService androidAuthService, public AndroidMeetingController(AndroidAuthService androidAuthService,
@ -118,6 +129,9 @@ public class AndroidMeetingController {
AiTaskService aiTaskService, AiTaskService aiTaskService,
PromptTemplateService promptTemplateService, PromptTemplateService promptTemplateService,
SysUserMapper sysUserMapper, SysUserMapper sysUserMapper,
AiModelService aiModelService,
SysDictItemService dictItemService,
SysParamService paramService,
StringRedisTemplate redisTemplate, StringRedisTemplate redisTemplate,
ObjectMapper objectMapper) { ObjectMapper objectMapper) {
this( this(
@ -130,6 +144,9 @@ public class AndroidMeetingController {
aiTaskService, aiTaskService,
promptTemplateService, promptTemplateService,
sysUserMapper, sysUserMapper,
aiModelService,
dictItemService,
paramService,
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper), new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
objectMapper objectMapper
); );
@ -266,6 +283,43 @@ public class AndroidMeetingController {
meetingCommandService.deleteMeeting(meetingId); meetingCommandService.deleteMeeting(meetingId);
return ApiResponse.ok(true); return ApiResponse.ok(true);
} }
@GetMapping("/config")
@Log(value = "获取会议配置", type = "Android会议管理")
@Operation(summary = "获取会议配置")
public ApiResponse<AndroidMeetingConfigVo> config(HttpServletRequest request) {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
AndroidMeetingConfigVo resultVo = new AndroidMeetingConfigVo();
PageResult<List<PromptTemplateVO>> promptTemplateList = promptTemplateService.pageTemplates(
1,
1000,
null,
null,
loginUser.getTenantId(),
loginUser.getUserId(),
loginUser.getIsPlatformAdmin(),
loginUser.getIsTenantAdmin()
);
List<PromptTemplateVO> enabledTemplates = promptTemplateList.getRecords() == null
? List.of()
: promptTemplateList.getRecords().stream()
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
.toList();
resultVo.setTemplateList(enabledTemplates);
PageResult<List<AiModelVO>> modelList = aiModelService.pageModels(1, 1000, null, "LLM", loginUser.getTenantId());
List<AiModelVO> 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) { private LegacyMeetingPreviewResult buildPreviewResult(Long meetingId) {
Meeting meeting = meetingService.getById(meetingId); Meeting meeting = meetingService.getById(meetingId);
@ -291,7 +345,7 @@ public class AndroidMeetingController {
buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可查看详情", 100, STAGE_COMPLETED)) buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可查看详情", 100, STAGE_COMPLETED))
); );
} }
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) { if (isFailed(asrTask)) {
return new LegacyMeetingPreviewResult( return new LegacyMeetingPreviewResult(
"503", "503",
buildFailureMessage(asrTask, "转写"), buildFailureMessage(asrTask, "转写"),

View File

@ -283,7 +283,7 @@ public class LegacyMeetingController {
// buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED)) // buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED))
// ); // );
// } // }
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) { if (isFailed(asrTask)) {
return new LegacyMeetingPreviewResult( return new LegacyMeetingPreviewResult(
"503", "503",
buildFailureMessage(asrTask, "转译"), buildFailureMessage(asrTask, "转译"),
@ -440,7 +440,7 @@ public class LegacyMeetingController {
if (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted) { if (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted) {
return new LegacyMeetingProcessingStatusResponse("completed", 100, STAGE_COMPLETED); 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); return new LegacyMeetingProcessingStatusResponse("failed", 50, STAGE_AUDIO_TRANSCRIPTION);
} }
if (isFailed(summaryTask)) { if (isFailed(summaryTask)) {

View File

@ -531,6 +531,28 @@ public class MeetingController {
return ApiResponse.ok(true); return ApiResponse.ok(true);
} }
@Operation(summary = "重试 AI 目录")
@PostMapping("/{id}/chapters/retry")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Boolean> 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<Boolean> 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 = "更新会议基础信息") @Operation(summary = "更新会议基础信息")
@PutMapping("/{id}/basic") @PutMapping("/{id}/basic")
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")

View File

@ -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
* <pre> Copyright: Copyright(c) 2026 </pre>
* <pre> Company : </pre>
* Modification History:
* Date Author Version Discription
* --------------------------------------------------------------------------
* 2026/05/27 ch 1.0 Why & What is modified: <> *
*/
@Data
public class AndroidMeetingConfigVo {
private List<AiModelVO> modelsList;
private List<PromptTemplateVO> templateList;
private List<SysDictItemDTO> summaryDegreeOfDetail;
private Integer maxPauseDuration;
private Integer maxMeetingDuration;
private BigDecimal packetLossRate;
}

View File

@ -21,8 +21,7 @@ public class MeetingSummaryFinalizeDTO {
@Schema(description = "转录指纹") @Schema(description = "转录指纹")
private String sourceFingerprint; private String sourceFingerprint;
@NotNull(message = "chapterVersionId must not be null") @Schema(description = "章节版本ID可选仅用于审计记录")
@Schema(description = "章节版本ID")
private Long chapterVersionId; private Long chapterVersionId;
@NotBlank(message = "summaryContent must not be blank") @NotBlank(message = "summaryContent must not be blank")

View File

@ -11,6 +11,7 @@ import com.unisbase.security.LoginUser;
import com.unisbase.service.TokenValidationService; import com.unisbase.service.TokenValidationService;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -18,6 +19,7 @@ import org.springframework.util.StringUtils;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class AndroidAuthServiceImpl implements AndroidAuthService { public class AndroidAuthServiceImpl implements AndroidAuthService {
private static final String HEADER_DEVICE_ID = "X-Android-Device-Id"; 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); String platform = request.getHeader(HEADER_PLATFORM);
requireAndroidHttpHeaders(deviceId, appVersion, platform); requireAndroidHttpHeaders(deviceId, appVersion, platform);
log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}",deviceId,appVersion,platform);
assertDeviceEnabled(deviceId); assertDeviceEnabled(deviceId);
if (loginUser != null) { if (loginUser != null) {
return buildContext("USER_JWT", false, return buildContext("USER_JWT", false,
deviceId, deviceId,

View File

@ -5,5 +5,7 @@ import com.imeeting.entity.biz.AiTask;
public interface AiTaskService extends IService<AiTask> { public interface AiTaskService extends IService<AiTask> {
void dispatchTasks(Long meetingId, Long tenantId, Long userId); 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 dispatchSummaryTask(Long meetingId, Long tenantId, Long userId);
void reconcileMeetingStatus(Long meetingId);
} }

View File

@ -41,6 +41,10 @@ public interface MeetingCommandService {
void retryTranscription(Long meetingId); void retryTranscription(Long meetingId);
void retrySummary(Long meetingId);
void retryChapter(Long meetingId);
MeetingTranscriptChapterImportResultVO importTranscriptChapters(MeetingTranscriptChapterImportDTO command); MeetingTranscriptChapterImportResultVO importTranscriptChapters(MeetingTranscriptChapterImportDTO command);
void finalizeSummary(MeetingSummaryFinalizeDTO command); void finalizeSummary(MeetingSummaryFinalizeDTO command);

View File

@ -10,6 +10,7 @@ import com.imeeting.common.MeetingProgressStage;
import com.imeeting.common.RedisKeys; import com.imeeting.common.RedisKeys;
import com.imeeting.dto.biz.AiModelVO; import com.imeeting.dto.biz.AiModelVO;
import com.imeeting.dto.biz.MeetingSummarySource; import com.imeeting.dto.biz.MeetingSummarySource;
import com.imeeting.dto.biz.MeetingTranscriptSourceVO;
import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.HotWord; import com.imeeting.entity.biz.HotWord;
import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.Meeting;
@ -247,30 +248,14 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
if (!asrText.isBlank()) { if (!asrText.isBlank()) {
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());
self.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId()); self.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
return; 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)) { if (sumTask != null && canExecuteTask(sumTask)) {
executeSummaryFlow(meeting, sumTask, chapterTask); executeSummaryFlow(meeting, sumTask);
} else if (meeting.getStatus() != 3) {
updateMeetingStatus(meetingId, 3);
} }
reconcileMeetingStatus(meetingId);
} catch (Exception e) { } catch (Exception e) {
log.error("Meeting {} AI Task Flow failed", meetingId, e); log.error("Meeting {} AI Task Flow failed", meetingId, e);
failPendingSummaryTask(findLatestSummaryTask(meetingId), "转录失败,已跳过总结任务: " + e.getMessage()); failPendingSummaryTask(findLatestSummaryTask(meetingId), "转录失败,已跳过总结任务: " + e.getMessage());
@ -282,6 +267,31 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> 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 @Override
@Async("summaryDispatchExecutor") @Async("summaryDispatchExecutor")
public void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId) { public void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId) {
@ -304,32 +314,15 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_SUMMARY_DISPATCH", false); triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_SUMMARY_DISPATCH", false);
return; return;
} }
AiTask chapterTask = findLatestTask(meetingId, "CHAPTER");
AiTask sumTask = findLatestTask(meetingId, "SUMMARY"); AiTask sumTask = findLatestTask(meetingId, "SUMMARY");
try { 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)) { if (sumTask != null && canExecuteTask(sumTask)) {
executeSummaryFlow(meeting, sumTask, chapterTask); executeSummaryFlow(meeting, sumTask);
} }
reconcileMeetingStatus(meetingId);
} catch (Exception e) { } catch (Exception e) {
log.error("Re-summary failed for meeting {}", meetingId, e); log.error("Re-summary failed for meeting {}", meetingId, e);
updateMeetingStatus(meetingId, 4); reconcileMeetingStatus(meetingId);
updateProgress(meetingId, -1, "Summary flow failed: " + e.getMessage(), 0);
} }
} }
@ -994,10 +987,14 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
this.updateById(taskRecord); this.updateById(taskRecord);
meeting.setLatestSummaryTaskId(taskRecord.getId()); meeting.setLatestSummaryTaskId(taskRecord.getId());
meeting.setStatus(3);
meetingMapper.updateById(meeting); 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 { } else {
updateAiTaskFail(taskRecord, "LLM 总结失败: " + response.body()); updateAiTaskFail(taskRecord, "LLM 总结失败: " + response.body());
throw new RuntimeException("AI总结生成异常"); throw new RuntimeException("AI总结生成异常");
@ -1048,54 +1045,9 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
} }
} }
private MeetingSummarySource restorePreparedSummarySource(AiTask chapterTask) { private void executeSummaryFlow(Meeting meeting, AiTask sumTask) throws Exception {
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 {
if (isExternalSummaryModeEnabled()) { if (isExternalSummaryModeEnabled()) {
AiTask chapterTask = findLatestTask(meeting.getId(), "CHAPTER");
triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_AFTER_TRANSCRIPT_READY", false); triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_AFTER_TRANSCRIPT_READY", false);
return; return;
} }
@ -1106,22 +1058,33 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
return; return;
} }
try { try {
MeetingSummarySource summarySource = restorePreparedSummarySource(chapterTask); MeetingSummarySource summarySource = buildRawTranscriptSummarySource(meeting);
if (summarySource == null || summarySource.getText() == null || summarySource.getText().isBlank()) {
summarySource = meetingTranscriptChapterService.resolveSummarySource(meeting, chapterTask != null ? chapterTask : sumTask);
}
if (summarySource.getText() == null || summarySource.getText().isBlank()) { if (summarySource.getText() == null || summarySource.getText().isBlank()) {
failPendingSummaryTask(sumTask, "没有转录内容"); failPendingSummaryTask(sumTask, "没有转录内容");
updateMeetingStatus(meeting.getId(), 4); reconcileMeetingStatus(meeting.getId());
updateProgress(meeting.getId(), -1, "没有转录内容", 0);
return; return;
} }
processSummaryTask(meeting, summarySource, sumTask); processSummaryTask(meeting, summarySource, sumTask);
reconcileMeetingStatus(meeting.getId());
} finally { } finally {
redisValueSupport.delete(summaryLockKey); 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) { 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)
@ -1176,24 +1139,37 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
&& !Integer.valueOf(3).equals(task.getStatus()); && !Integer.valueOf(3).equals(task.getStatus());
} }
private boolean hasDetailedChapterFailureMessage(AiTask chapterTask) { public void reconcileMeetingStatus(Long meetingId) {
String failureMessage = buildChapterFailurePropagationMessage(chapterTask); if (meetingId == null) {
return failureMessage != null && !failureMessage.isBlank() && !failureMessage.equals("章节生成失败,无法继续总结"); 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) { private boolean isTaskCompleted(AiTask task) {
if (chapterTask == null) { return task != null && Integer.valueOf(2).equals(task.getStatus());
return "章节生成失败,无法继续总结"; }
}
String detail = firstNonBlank( private boolean isTaskFailed(AiTask task) {
chapterTask.getErrorMsg(), return task != null && Integer.valueOf(3).equals(task.getStatus());
stringValue(chapterTask.getResponseData() == null ? null : chapterTask.getResponseData().get("failureReason")), }
stringValue(chapterTask.getResponseData() == null ? null : chapterTask.getResponseData().get("exceptionMessage"))
); private boolean isTaskRunningOrQueued(AiTask task) {
if (detail == null) { return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
return "章节生成失败,无法继续总结";
}
return "章节生成失败,无法继续总结: " + detail;
} }
private String stringValue(Object value) { private String stringValue(Object value) {
@ -1212,18 +1188,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
return null; 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) { private Long longValue(Object value) {
if (value == null) { if (value == null) {
return null; return null;
@ -1235,17 +1199,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> 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) { private AiModelVO resolveAsrModelForRevision(AiTask asrTask) {
if (asrTask == null || asrTask.getTaskConfig() == null) { if (asrTask == null || asrTask.getTaskConfig() == null) {
return null; return null;

View File

@ -12,6 +12,7 @@ import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO;
import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO; import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO;
import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO; import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO; import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO;
import com.imeeting.dto.biz.MeetingTranscriptSourceVO;
import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile; import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig; import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
@ -428,8 +429,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
realtimeMeetingSessionStateService.clear(meetingId); realtimeMeetingSessionStateService.clear(meetingId);
meeting.setStatus(2); meeting.setStatus(2);
meetingService.updateById(meeting); meetingService.updateById(meeting);
updateMeetingProgress(meetingId, 90, "正在生成会议总结...", 0); updateMeetingProgress(meetingId, 85, "正在生成 AI 目录与总结...", 0);
meetingDomainSupport.prewarmPlaybackAudioAfterCommit(meeting.getAudioUrl()); meetingDomainSupport.prewarmPlaybackAudioAfterCommit(meeting.getAudioUrl());
aiTaskService.dispatchChapterTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
aiTaskService.dispatchSummaryTask(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()); resetAiTask(summaryTask, summaryTask.getTaskConfig());
aiTaskService.updateById(summaryTask); aiTaskService.updateById(summaryTask);
AiTask chapterTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
.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<String, Object> taskConfig) { private void resetAiTask(AiTask task, Map<String, Object> taskConfig) {
@ -746,14 +757,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
throw new RuntimeException("总结任务不存在或不属于当前会议"); throw new RuntimeException("总结任务不存在或不属于当前会议");
} }
MeetingTranscriptChapterVersion currentVersion = meetingTranscriptChapterService.getCurrentVersion(meeting.getId()); MeetingTranscriptSourceVO transcriptSource = meetingTranscriptChapterService.buildTranscriptSource(meeting.getId());
if (currentVersion == null) { String currentFingerprint = transcriptSource == null ? null : transcriptSource.getSourceFingerprint();
throw new RuntimeException("当前会议不存在有效章节版本"); if (currentFingerprint == null || !Objects.equals(currentFingerprint, command.getSourceFingerprint())) {
}
if (!Objects.equals(currentVersion.getId(), command.getChapterVersionId())) {
throw new RuntimeException("章节版本不是当前生效版本,拒绝回填总结");
}
if (!Objects.equals(currentVersion.getSourceFingerprint(), command.getSourceFingerprint())) {
throw new RuntimeException("转录指纹已变化,拒绝回填过期总结结果"); throw new RuntimeException("转录指纹已变化,拒绝回填过期总结结果");
} }
@ -764,12 +770,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
? new HashMap<>() ? new HashMap<>()
: new HashMap<>(summaryTask.getResponseData()); : new HashMap<>(summaryTask.getResponseData());
Map<String, Object> summarySource = new HashMap<>(); Map<String, Object> summarySource = new HashMap<>();
summarySource.put("sourceType", "CHAPTER_VERSION"); summarySource.put("sourceType", "RAW_TRANSCRIPT");
summarySource.put("chapterVersionId", currentVersion.getId()); summarySource.put("sourceFingerprint", currentFingerprint);
summarySource.put("chapterCount", currentVersion.getChapterCount()); if (command.getChapterVersionId() != null) {
summarySource.put("sourceFingerprint", currentVersion.getSourceFingerprint()); summarySource.put("chapterVersionId", command.getChapterVersionId());
summarySource.put("algorithmVersion", currentVersion.getAlgorithmVersion()); }
summarySource.put("generationMode", currentVersion.getGenerationMode());
responseData.put("summarySource", summarySource); responseData.put("summarySource", summarySource);
Map<String, Object> summaryBundle = new HashMap<>(); Map<String, Object> summaryBundle = new HashMap<>();
@ -786,9 +791,19 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
aiTaskService.updateById(summaryTask); aiTaskService.updateById(summaryTask);
meeting.setLatestSummaryTaskId(summaryTask.getId()); meeting.setLatestSummaryTaskId(summaryTask.getId());
meeting.setStatus(3);
meetingService.updateById(meeting); meetingService.updateById(meeting);
updateMeetingProgress(meeting.getId(), 100, "外部总结回填完成", 0); aiTaskService.reconcileMeetingStatus(meeting.getId());
AiTask latestChapterTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
.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 @Override
@ -870,16 +885,15 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
if ("CHAPTER".equals(stage)) { if ("CHAPTER".equals(stage)) {
markTaskFailed(chapterTask, "外部章节编排失败: " + errorMessage, command.getRawError()); markTaskFailed(chapterTask, "外部章节编排失败: " + errorMessage, command.getRawError());
markTaskFailed(summaryTask, "外部章节编排失败,无法继续总结: " + errorMessage, command.getRawError());
} else { } else {
markTaskFailed(summaryTask, "外部总结编排失败: " + errorMessage, command.getRawError()); markTaskFailed(summaryTask, "外部总结编排失败: " + errorMessage, command.getRawError());
} }
if (summaryTask != null) { if (summaryTask != null) {
meeting.setLatestSummaryTaskId(summaryTask.getId()); meeting.setLatestSummaryTaskId(summaryTask.getId());
meetingService.updateById(meeting);
} }
meeting.setStatus(4); aiTaskService.reconcileMeetingStatus(meeting.getId());
meetingService.updateById(meeting);
updateMeetingProgress(meeting.getId(), -1, errorMessage, 0); updateMeetingProgress(meeting.getId(), -1, errorMessage, 0);
} }
@ -892,34 +906,20 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
} }
String effectiveSummaryDetailLevel = resolveSummaryDetailLevel(summaryDetailLevel != null ? summaryDetailLevel : meeting.getSummaryDetailLevel()); String effectiveSummaryDetailLevel = resolveSummaryDetailLevel(summaryDetailLevel != null ? summaryDetailLevel : meeting.getSummaryDetailLevel());
Long effectiveChapterModelId = chapterModelId != null ? chapterModelId : summaryModelId; meetingDomainSupport.createSummaryTask(
meetingDomainSupport.createChapterTask(
meetingId, meetingId,
summaryModelId, summaryModelId,
effectiveChapterModelId,
promptId, promptId,
userPrompt, userPrompt,
effectiveSummaryDetailLevel 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.setSummaryDetailLevel(effectiveSummaryDetailLevel);
meeting.setStatus(2); meeting.setStatus(2);
meetingService.updateById(meeting); meetingService.updateById(meeting);
if ("EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) { if ("EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) {
updateMeetingProgress(meetingId, 95, "等待外部章节与总结编排...", 0); updateMeetingProgress(meetingId, 95, "等待外部总结编排...", 0);
} else { } else {
updateMeetingProgress(meetingId, 85, "重新总结已提交,正在生成章节...", 0); updateMeetingProgress(meetingId, 90, "重新总结已提交,正在生成总结...", 0);
} }
dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
} }
@ -996,6 +996,74 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); 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<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId));
if (transcriptCount <= 0) {
throw new RuntimeException("当前会议没有可用转录,无法重试总结");
}
AiTask summaryTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
.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<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId));
if (transcriptCount <= 0) {
throw new RuntimeException("当前会议没有可用转录,无法重试 AI 目录");
}
AiTask chapterTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
.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) { private void clearLegacyDispatchState(Long meetingId) {
if (compatibilityRedisTemplate == null || meetingId == null) { if (compatibilityRedisTemplate == null || meetingId == null) {
return; return;
@ -1092,6 +1160,19 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
aiTaskService.updateById(task); 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) { private void dispatchSummaryTaskAfterCommit(Long meetingId, Long tenantId, Long userId) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) { if (!TransactionSynchronizationManager.isSynchronizationActive()) {
aiTaskService.dispatchSummaryTask(meetingId, tenantId, userId); aiTaskService.dispatchSummaryTask(meetingId, tenantId, userId);

View File

@ -225,7 +225,7 @@ public class MeetingMcpToolService {
if (summaryCompleted) { if (summaryCompleted) {
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask)); return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask));
} }
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) { if (isFailed(asrTask)) {
return new LegacyMeetingPreviewResult( return new LegacyMeetingPreviewResult(
"503", "503",
buildFailureMessage(asrTask, "转译"), buildFailureMessage(asrTask, "转译"),
@ -369,7 +369,7 @@ public class MeetingMcpToolService {
if (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted) { if (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted) {
return new LegacyMeetingProcessingStatusResponse("completed", 100, STAGE_COMPLETED); 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); return new LegacyMeetingProcessingStatusResponse("failed", 50, STAGE_AUDIO_TRANSCRIPTION);
} }
if (isFailed(summaryTask)) { if (isFailed(summaryTask)) {

View File

@ -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) => { export const updateMeetingBasic = (data: UpdateMeetingBasicCommand) => {
return http.put<{ code: string; data: boolean; msg: string }>( return http.put<{ code: string; data: boolean; msg: string }>(
`/api/biz/meeting/${data.meetingId}/basic`, `/api/biz/meeting/${data.meetingId}/basic`,

View File

@ -40,6 +40,8 @@ import {
reSummary, reSummary,
resolveAudioMimeType, resolveAudioMimeType,
resolveMeetingPlaybackAudioUrl, resolveMeetingPlaybackAudioUrl,
retryMeetingChapter,
retryMeetingSummary,
retryMeetingTranscription, retryMeetingTranscription,
updateMeetingBasic, updateMeetingBasic,
updateMeetingTranscript, updateMeetingTranscript,
@ -1015,8 +1017,15 @@ const MeetingDetail: React.FC = () => {
return false; return false;
}, [meeting]); }, [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 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 hasSummaryContent = Boolean(meeting?.summaryContent?.trim());
const hasCatalogContent = catalogChapterLinks.length > 0; const hasCatalogContent = catalogChapterLinks.length > 0;
const generationFailureNotice = useMemo<MeetingStateNotice | null>(() => { const generationFailureNotice = useMemo<MeetingStateNotice | null>(() => {
@ -1362,13 +1371,11 @@ const MeetingDetail: React.FC = () => {
setMeeting((current) => current ? { setMeeting((current) => current ? {
...current, ...current,
status: 2, status: 2,
latestChapterAttemptStatus: 0,
latestChapterAttemptErrorMsg: undefined,
latestSummaryAttemptStatus: 0, latestSummaryAttemptStatus: 0,
latestSummaryAttemptErrorMsg: undefined, latestSummaryAttemptErrorMsg: undefined,
} : current); } : current);
setGenerationProgress({ setGenerationProgress({
percent: 85, percent: 90,
message: '已重新发起总结任务', message: '已重新发起总结任务',
updateAt: Date.now(), updateAt: Date.now(),
eta: 0, 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) => { const handleKeywordToggle = (keyword: string, checked: boolean) => {
setSelectedKeywords((current) => { setSelectedKeywords((current) => {
if (checked) { if (checked) {
@ -1976,6 +2021,16 @@ const MeetingDetail: React.FC = () => {
</Button> </Button>
)} )}
{canRetryFailedChapterTask && (
<Button icon={<SyncOutlined />} onClick={handleRetryChapterTask} loading={actionLoading}>
AI
</Button>
)}
{canRetryFailedSummaryTask && (
<Button icon={<SyncOutlined />} onClick={handleRetrySummaryTask} loading={actionLoading}>
</Button>
)}
{canRetryTranscription && ( {canRetryTranscription && (
<Button icon={<SyncOutlined />} onClick={handleRetryTranscription} loading={actionLoading}> <Button icon={<SyncOutlined />} onClick={handleRetryTranscription} loading={actionLoading}>