feat: 添加总结和章节任务重试功能并优化会议处理逻辑
- 在 `MeetingCommandServiceImpl` 中添加 `retrySummary` 和 `retryChapter` 方法,支持总结和章节任务的重试 - 更新 `MeetingCommandService` 接口以包含新的重试方法 - 优化 `executeSummaryFlow` 和 `doDispatchChapterTask` 方法,简化任务执行逻辑 - 更新 `finalizeSummary` 方法,移除不必要的章节版本检查 - 调整 `updateMeetingProgress` 的进度值和消息,更准确地反映任务状态 - 在前端 `MeetingDetail.tsx` 中添加重试按钮和相关逻辑,支持用户手动重试失败的任务dev_na
parent
892275bc65
commit
384494d9ff
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<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) {
|
||||
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, "转写"),
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -531,6 +531,28 @@ public class MeetingController {
|
|||
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 = "更新会议基础信息")
|
||||
@PutMapping("/{id}/basic")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -5,5 +5,7 @@ import com.imeeting.entity.biz.AiTask;
|
|||
|
||||
public interface AiTaskService extends IService<AiTask> {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<AiTaskMapper, AiTask> 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<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
|
||||
@Async("summaryDispatchExecutor")
|
||||
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);
|
||||
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<AiTaskMapper, AiTask> 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<AiTaskMapper, AiTask> 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<AiTaskMapper, AiTask> 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<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
|
|
@ -1176,24 +1139,37 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> 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<AiTaskMapper, AiTask> 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<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) {
|
||||
if (asrTask == null || asrTask.getTaskConfig() == null) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -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<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) {
|
||||
|
|
@ -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<String, Object> 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<String, Object> 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<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
|
||||
|
|
@ -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<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) {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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<MeetingStateNotice | null>(() => {
|
||||
|
|
@ -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 = () => {
|
|||
重新总结
|
||||
</Button>
|
||||
)}
|
||||
{canRetryFailedChapterTask && (
|
||||
<Button icon={<SyncOutlined />} onClick={handleRetryChapterTask} loading={actionLoading}>
|
||||
重试 AI 目录
|
||||
</Button>
|
||||
)}
|
||||
{canRetryFailedSummaryTask && (
|
||||
<Button icon={<SyncOutlined />} onClick={handleRetrySummaryTask} loading={actionLoading}>
|
||||
重试总结
|
||||
</Button>
|
||||
)}
|
||||
{canRetryTranscription && (
|
||||
<Button icon={<SyncOutlined />} onClick={handleRetryTranscription} loading={actionLoading}>
|
||||
重新识别
|
||||
|
|
|
|||
Loading…
Reference in New Issue