From 8a082732dfcc2baa44efef8531085db1efddb61d Mon Sep 17 00:00:00 2001 From: chenhao Date: Mon, 1 Jun 2026 13:42:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20ASR=20=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E9=87=8D=E6=96=B0=E8=B0=83=E5=BA=A6=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=A3=E7=90=86=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `AiTaskService` 中添加 `triggerQueuedAsrScheduling` 和 `retryScheduleMeeting` 方法 - 更新多个服务实现类以使用新的 ASR 任务调度方法 - 在前端 `MeetingDetail.tsx` 和 `Meetings.tsx` 中添加重新调度按钮和相关逻辑 --- .../com/imeeting/ImeetingApplication.java | 2 + .../controller/biz/MeetingController.java | 21 ++- .../listener/MeetingTaskDispatchListener.java | 2 +- .../impl/LegacyMeetingAdapterServiceImpl.java | 4 +- .../imeeting/service/biz/AiTaskService.java | 2 + .../service/biz/impl/AiTaskServiceImpl.java | 137 ++++++++++-------- .../biz/impl/MeetingCommandServiceImpl.java | 6 +- .../java/com/imeeting/task/AiTaskConfig.java | 33 +++++ frontend/src/api/business/meeting.ts | 7 + frontend/src/pages/business/MeetingDetail.tsx | 29 ++++ frontend/src/pages/business/Meetings.tsx | 72 ++++++++- 11 files changed, 238 insertions(+), 77 deletions(-) create mode 100644 backend/src/main/java/com/imeeting/task/AiTaskConfig.java diff --git a/backend/src/main/java/com/imeeting/ImeetingApplication.java b/backend/src/main/java/com/imeeting/ImeetingApplication.java index 645a28f..ddc9e76 100644 --- a/backend/src/main/java/com/imeeting/ImeetingApplication.java +++ b/backend/src/main/java/com/imeeting/ImeetingApplication.java @@ -3,9 +3,11 @@ package com.imeeting; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableAsync +@EnableScheduling public class ImeetingApplication { public static void main(String[] args) { SpringApplication.run(ImeetingApplication.class, args); diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java index eb45511..8cf30e5 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java @@ -87,6 +87,7 @@ public class MeetingController { private final MeetingAudioUploadSupport meetingAudioUploadSupport; private final MeetingProgressService meetingProgressService; private final SysParamService sysParamService; + private final AiTaskService aiTaskService; private AiTaskService compatibilityAiTaskService; @Autowired @@ -100,7 +101,8 @@ public class MeetingController { RealtimeMeetingSessionStateService realtimeMeetingSessionStateService, MeetingAudioUploadSupport meetingAudioUploadSupport, MeetingProgressService meetingProgressService, - SysParamService sysParamService) { + SysParamService sysParamService, + AiTaskService aiTaskService) { this.meetingQueryService = meetingQueryService; this.meetingCommandService = meetingCommandService; this.meetingAccessService = meetingAccessService; @@ -112,6 +114,7 @@ public class MeetingController { this.meetingAudioUploadSupport = meetingAudioUploadSupport; this.meetingProgressService = meetingProgressService; this.sysParamService = sysParamService; + this.aiTaskService = aiTaskService; } public MeetingController(MeetingQueryService meetingQueryService, @@ -137,7 +140,8 @@ public class MeetingController { realtimeMeetingSessionStateService, meetingAudioUploadSupport, new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, new com.fasterxml.jackson.databind.ObjectMapper()), - sysParamService + sysParamService, + unusedAiTaskService ); this.compatibilityAiTaskService = unusedAiTaskService; } @@ -203,7 +207,18 @@ public class MeetingController { } return ApiResponse.ok(result); } - @Operation(summary = "上传会议音频") + + @Operation(summary = "重新调度排队中的会议 ASR 任务") + @PostMapping("/{id}/retry-schedule") + @PreAuthorize("isAuthenticated()") + public ApiResponse retrySchedule(@PathVariable Long id) { + LoginUser loginUser = currentLoginUser(); + Meeting meeting = meetingAccessService.requireMeeting(id); + meetingAccessService.assertCanEditMeeting(meeting, loginUser); + return ApiResponse.ok(aiTaskService.retryScheduleMeeting(id)); + } + + @Operation(summary = "上传会议音频") @PostMapping("/upload") @PreAuthorize("isAuthenticated()") public ApiResponse upload(@RequestParam("file") MultipartFile file) throws IOException { diff --git a/backend/src/main/java/com/imeeting/listener/MeetingTaskDispatchListener.java b/backend/src/main/java/com/imeeting/listener/MeetingTaskDispatchListener.java index e2ae635..0cda069 100644 --- a/backend/src/main/java/com/imeeting/listener/MeetingTaskDispatchListener.java +++ b/backend/src/main/java/com/imeeting/listener/MeetingTaskDispatchListener.java @@ -15,6 +15,6 @@ public class MeetingTaskDispatchListener { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onMeetingCreated(MeetingCreatedEvent event) { - aiTaskService.dispatchTasks(event.getMeetingId(), event.getTenantId(), event.getUserId()); + aiTaskService.triggerQueuedAsrScheduling(); } } diff --git a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java index 0ed3570..adce025 100644 --- a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java @@ -301,13 +301,13 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ private void dispatchTasksAfterCommit(Long meetingId, Long tenantId, Long userId) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { - aiTaskService.dispatchTasks(meetingId, tenantId, userId); + aiTaskService.triggerQueuedAsrScheduling(); return; } TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { - aiTaskService.dispatchTasks(meetingId, tenantId, userId); + aiTaskService.triggerQueuedAsrScheduling(); } }); } diff --git a/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java b/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java index 853b5db..f9733af 100644 --- a/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java +++ b/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java @@ -5,6 +5,8 @@ import com.imeeting.entity.biz.AiTask; public interface AiTaskService extends IService { void dispatchTasks(Long meetingId, Long tenantId, Long userId); + void triggerQueuedAsrScheduling(); + boolean retryScheduleMeeting(Long meetingId); void dispatchChapterTask(Long meetingId, Long tenantId, Long userId); void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId); void reconcileMeetingStatus(Long meetingId); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index 07c33ae..d69453e 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -40,6 +40,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; @@ -190,6 +191,30 @@ public class AiTaskServiceImpl extends ServiceImpl impleme asrTaskExecutor.execute(task); } + @Override + public void triggerQueuedAsrScheduling() { + scheduleQueuedAsrTasks(); + } + + @Override + public boolean retryScheduleMeeting(Long meetingId) { + if (meetingId == null) { + return false; + } + AiTask asrTask = findLatestTask(meetingId, "ASR"); + if (asrTask == null || !Integer.valueOf(0).equals(asrTask.getStatus())) { + return false; + } + if (asrTask.getQueuedAt() == null) { + asrTask.setQueuedAt(LocalDateTime.now()); + updateById(asrTask); + } + meetingProgressService.markQueued(meetingId, asrTask, 1, "已触发重新调度"); + triggerQueuedAsrScheduling(); + return true; + } + + private void doDispatchTasks(Long meetingId) { String lockKey = RedisKeys.meetingPollingLockKey(meetingId); Boolean acquired = redisValueSupport.setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES); @@ -218,10 +243,8 @@ public class AiTaskServiceImpl extends ServiceImpl impleme asrTask.setQueuedAt(LocalDateTime.now()); this.updateById(asrTask); } - if (!claimQueuedAsrTask(asrTask)) { - meetingProgressService.markQueued(meetingId, asrTask, 1, "ASR queued and waiting for execution"); - return; - } + meetingProgressService.markQueued(meetingId, asrTask, 1, "ASR queued and waiting for execution"); + return; } } @@ -340,9 +363,14 @@ public class AiTaskServiceImpl extends ServiceImpl impleme } Long asrModelId = extractAsrModelId(asrTask); String externalTaskId = extractExternalTaskId(asrTask); - if (asrModelId == null || externalTaskId == null || externalTaskId.isBlank()) { - requeueAsrTask(asrTask, "恢复时缺少有效外部 ASR 任务,已重新排队", true); - return false; + // Freshly re-scheduled tasks are also claimed as RUNNING before they get a new external task id. + // In that case we should continue into processAsrTask() and submit a brand-new ASR job instead of + // treating them as a broken recovery candidate and requeueing forever. + if (externalTaskId == null || externalTaskId.isBlank()) { + return true; + } + if (asrModelId == null) { + return true; } AiModelVO asrModel = aiModelService.getModelById(asrModelId, "ASR"); if (asrModel == null || !canResumeAsrTask(asrModel, meeting.getId(), externalTaskId)) { @@ -352,63 +380,16 @@ public class AiTaskServiceImpl extends ServiceImpl impleme return true; } - private boolean claimQueuedAsrTask(AiTask task) { - if (task == null || task.getId() == null || !Integer.valueOf(0).equals(task.getStatus())) { - return false; - } - if (getBaseMapper() == null) { - task.setStatus(1); - task.setStartedAt(LocalDateTime.now()); - return true; - } - String scheduleLockKey = RedisKeys.meetingAsrScheduleLockKey(); - Boolean acquired = redisValueSupport.setIfAbsent(scheduleLockKey, "locked", 30, TimeUnit.SECONDS); - if (Boolean.FALSE.equals(acquired)) { - return false; - } - try { - int maxConcurrent = resolveAsrMaxConcurrent(); - long runningCount = count(new LambdaQueryWrapper() - .eq(AiTask::getTaskType, "ASR") - .eq(AiTask::getStatus, 1)); - if (runningCount >= maxConcurrent) { - return false; - } - int available = (int) Math.max(1, maxConcurrent - runningCount); - List dispatchable = list(new LambdaQueryWrapper() - .eq(AiTask::getTaskType, "ASR") - .eq(AiTask::getStatus, 0) - .orderByAsc(AiTask::getQueuedAt) - .orderByAsc(AiTask::getId) - .last("LIMIT " + available)); - boolean eligible = dispatchable.stream().anyMatch(item -> Objects.equals(item.getId(), task.getId())); - if (!eligible) { - return false; - } - LocalDateTime now = LocalDateTime.now(); - boolean claimed = update(new LambdaUpdateWrapper() - .eq(AiTask::getId, task.getId()) - .eq(AiTask::getStatus, 0) - .set(AiTask::getStatus, 1) - .set(AiTask::getStartedAt, now) - .set(AiTask::getCompletedAt, null) - .set(AiTask::getErrorMsg, null)); - if (claimed) { - task.setStatus(1); - task.setStartedAt(now); - meetingProgressService.markStage(task.getMeetingId(), task, 1, MeetingProgressStage.ASR_SUBMITTED, 5, "ASR 任务已开始执行", 0); - refreshQueuedAsrProgress(); - } - return claimed; - } finally { - redisValueSupport.delete(scheduleLockKey); - } - } - private void scheduleQueuedAsrTasks() { if (getBaseMapper() == null) { return; } + long queuedCount = count(new LambdaQueryWrapper() + .eq(AiTask::getTaskType, "ASR") + .eq(AiTask::getStatus, 0)); + if (queuedCount <= 0) { + return; + } String scheduleLockKey = RedisKeys.meetingAsrScheduleLockKey(); Boolean acquired = redisValueSupport.setIfAbsent(scheduleLockKey, "locked", 30, TimeUnit.SECONDS); if (Boolean.FALSE.equals(acquired)) { @@ -423,15 +404,21 @@ public class AiTaskServiceImpl extends ServiceImpl impleme if (available <= 0) { return; } - refreshQueuedAsrProgress(); List queuedTasks = list(new LambdaQueryWrapper() .eq(AiTask::getTaskType, "ASR") .eq(AiTask::getStatus, 0) .orderByAsc(AiTask::getQueuedAt) .orderByAsc(AiTask::getId) .last("LIMIT " + available)); + List claimedTasks = new ArrayList<>(); for (AiTask queuedTask : queuedTasks) { - Meeting queuedMeeting = meetingMapper.selectById(queuedTask.getMeetingId()); + if (claimQueuedAsrTaskForScheduling(queuedTask)) { + claimedTasks.add(queuedTask); + } + } + refreshQueuedAsrProgress(); + for (AiTask queuedTask : claimedTasks) { + Meeting queuedMeeting = meetingMapper.selectByIdIgnoreTenant(queuedTask.getMeetingId()); if (queuedMeeting == null) { continue; } @@ -456,6 +443,27 @@ public class AiTaskServiceImpl extends ServiceImpl impleme } } + private boolean claimQueuedAsrTaskForScheduling(AiTask task) { + if (task == null || task.getId() == null || !Integer.valueOf(0).equals(task.getStatus())) { + return false; + } + LocalDateTime now = LocalDateTime.now(); + boolean claimed = update(new LambdaUpdateWrapper() + .eq(AiTask::getId, task.getId()) + .eq(AiTask::getStatus, 0) + .set(AiTask::getStatus, 1) + .set(AiTask::getStartedAt, now) + .set(AiTask::getCompletedAt, null) + .set(AiTask::getErrorMsg, null)); + if (!claimed) { + return false; + } + task.setStatus(1); + task.setStartedAt(now); + meetingProgressService.markStage(task.getMeetingId(), task, 1, MeetingProgressStage.ASR_SUBMITTED, 5, "ASR 任务已开始执行", 0); + return true; + } + private int resolveAsrMaxConcurrent() { if (sysParamService == null) { return 2; @@ -489,6 +497,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme updateById(task); meetingProgressService.markQueued(task.getMeetingId(), task, 1, reason == null || reason.isBlank() ? "已重新进入 ASR 队列" : reason); refreshQueuedAsrProgress(); + triggerQueuedAsrScheduling(); } private Long extractAsrModelId(AiTask task) { @@ -666,14 +675,14 @@ public class AiTaskServiceImpl extends ServiceImpl impleme } private String submitAsrTask(Meeting meeting, AiTask taskRecord, AiModelVO asrModel, String submitUrl) throws Exception { - updateProgress(meeting.getId(), 5, "重新提交任务...", 0); + updateProgress(meeting.getId(), 5, "提交任务...", 0); Map req = buildAsrRequest(meeting, taskRecord, asrModel); taskRecord.setRequestData(req); this.updateById(taskRecord); String respBody = postJson(submitUrl, req, asrModel.getApiKey()); JsonNode submitNode = objectMapper.readTree(respBody); - if (submitNode.path("code").asInt() != 0) { + if (submitNode.path("code")==null||submitNode.path("code").asInt() != 0) { updateAiTaskFail(taskRecord, "ASR识别失败 " + respBody); throw new RuntimeException("ASR识别失败: " + firstNonBlank( submitNode.path("message").asText(""), diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java index 9d4c764..7d85320 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java @@ -409,7 +409,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { prepareOfflineReprocessTasks(meetingId, currentStatus); realtimeMeetingSessionStateService.clear(meetingId); updateMeetingProgress(meetingId, 0, "正在转入离线音频识别流程...", 0); - aiTaskService.dispatchTasks(meetingId, meeting.getTenantId(), meeting.getCreatorId()); + aiTaskService.triggerQueuedAsrScheduling(); return; } @@ -1196,13 +1196,13 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { private void dispatchTasksAfterCommit(Long meetingId, Long tenantId, Long userId) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { - aiTaskService.dispatchTasks(meetingId, tenantId, userId); + aiTaskService.triggerQueuedAsrScheduling(); return; } TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { - aiTaskService.dispatchTasks(meetingId, tenantId, userId); + aiTaskService.triggerQueuedAsrScheduling(); } }); } diff --git a/backend/src/main/java/com/imeeting/task/AiTaskConfig.java b/backend/src/main/java/com/imeeting/task/AiTaskConfig.java new file mode 100644 index 0000000..7b8b407 --- /dev/null +++ b/backend/src/main/java/com/imeeting/task/AiTaskConfig.java @@ -0,0 +1,33 @@ +package com.imeeting.task; + + +import com.imeeting.service.biz.AiTaskService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * @author : ch + * @version : 1.0 + * @ClassName : AiTaskConfig + * @Description : + * @DATE : Created in 10:29 2026/6/1 + *
       Copyright: Copyright(c) 2026     
+ *
       Company :   	紫光汇智信息技术有限公司		           
+ * Modification History: + * Date Author Version Discription + * -------------------------------------------------------------------------- + * 2026/06/01 ch 1.0 Why & What is modified: <修改原因描述> * + */ +@Component +public class AiTaskConfig { + @Autowired + private AiTaskService aiTaskService; + @Scheduled( + fixedDelayString = "${imeeting.asr-schedule-fixed-delay-ms:15000}", + initialDelayString = "${imeeting.asr-schedule-initial-delay-ms:15000}" + ) + public void compensateQueuedAsrScheduling() { + aiTaskService.triggerQueuedAsrScheduling(); + } +} diff --git a/frontend/src/api/business/meeting.ts b/frontend/src/api/business/meeting.ts index 688f819..44c4e7e 100644 --- a/frontend/src/api/business/meeting.ts +++ b/frontend/src/api/business/meeting.ts @@ -442,6 +442,7 @@ export interface MeetingProgress { updateAt: number; eta?: number; queueAheadCount?: number; + queuedAt?: string; } export const getMeetingProgress = (id: number, options?: { suppressErrorToast?: boolean }) => { @@ -463,6 +464,12 @@ export const getMeetingProgressBatch = (ids: number[], options?: { suppressError ); }; +export const retryScheduleMeeting = (id: number) => { + return http.post<{ code: string; data: boolean; msg: string }>( + `/api/biz/meeting/${id}/retry-schedule` + ); +}; + export const downloadMeetingSummary = (id: number, format: "pdf" | "word") => { const token = localStorage.getItem("accessToken"); return axios.get(`/api/biz/meeting/${id}/summary/export`, { diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index 6423cbd..c4265b4 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -43,6 +43,7 @@ import { retryMeetingChapter, retryMeetingSummary, retryMeetingTranscription, + retryScheduleMeeting, updateMeetingBasic, updateMeetingTranscript, updateMeetingSummary, @@ -106,6 +107,8 @@ type MeetingStateNotice = { scope: 'summary' | 'catalog' | 'global'; }; +const QUEUED_RETRY_THRESHOLD_MS = 2 * 60 * 1000; + const ANALYSIS_EMPTY: MeetingAnalysis = { overview: '', keywords: [], @@ -1026,6 +1029,27 @@ const MeetingDetail: React.FC = () => { 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 canRetrySchedule = isOwner && meeting?.status === 0 && (!generationProgress || generationProgress.percent <= 0) && !!generationProgress?.queuedAt && dayjs().diff(dayjs(generationProgress.queuedAt)) >= QUEUED_RETRY_THRESHOLD_MS; + + const handleRetrySchedule = async () => { + if (!meeting) return; + setActionLoading(true); + try { + const response = await retryScheduleMeeting(meeting.id); + if (response.data?.data) { + message.success("已触发重新调度"); + } else { + message.info("当前没有可重新调度的排队任务"); + } + await fetchData(meeting.id); + } catch { + message.error("重新调度失败"); + } finally { + setActionLoading(false); + } + }; + const hasSummaryContent = Boolean(meeting?.summaryContent?.trim()); const hasCatalogContent = catalogChapterLinks.length > 0; const generationFailureNotice = useMemo(() => { @@ -2016,6 +2040,11 @@ const MeetingDetail: React.FC = () => { + {canRetrySchedule && ( + + )} {canRetrySummary && ( + )} ) : (
@@ -425,6 +453,7 @@ const Meetings: React.FC = () => { }); const [userList, setUserList] = useState([]); const progressTerminalRefreshRef = useRef>(new Map()); + const [retryingMeetingIds, setRetryingMeetingIds] = useState>({}); const activeFilterCount = (statusFilter !== ALL_STATUS_FILTER ? 1 : 0) + (searchTitle ? 1 : 0); @@ -629,6 +658,27 @@ const Meetings: React.FC = () => { } }; + const handleRetrySchedule = async (meeting: MeetingVO) => { + setRetryingMeetingIds((currentMap) => ({ ...currentMap, [meeting.id]: true })); + try { + const response = await retryScheduleMeeting(meeting.id); + if (response.data?.data) { + message.success("已触发重新调度"); + } else { + message.info("当前没有可重新调度的排队任务"); + } + await fetchData(true); + } catch { + message.error("重新调度失败"); + } finally { + setRetryingMeetingIds((currentMap) => { + const nextMap = { ...currentMap }; + delete nextMap[meeting.id]; + return nextMap; + }); + } + }; + const handleOpenMeeting = async (meeting: MeetingVO) => { if (!isRealtimeMeetingCandidate(meeting)) { navigate("/meetings/" + meeting.id); @@ -700,10 +750,23 @@ const Meetings: React.FC = () => { { title: "操作", key: "action", - width: 160, + width: 220, render: (_: unknown, record: MeetingVO) => ( + {canRetryQueuedMeeting(record, progressMap[record.id] || null) && ( + + )} {canManageMeeting(record) && ( deleteMeeting(record.id).then(() => fetchData())}> @@ -840,8 +903,9 @@ const Meetings: React.FC = () => { item={item} config={config} progress={progress} - fetchData={() => void fetchData()} onOpenMeeting={handleOpenMeeting} + onRetrySchedule={(meeting) => { void handleRetrySchedule(meeting); }} + retrying={!!retryingMeetingIds[item.id]} /> ); }}