feat: 添加 Redis 支持和优化会议进度管理
- 新增 `RedisValueSupport` 类,提供 Redis 操作支持 - 实现 `MeetingProgressServiceImpl` 服务,管理会议进度的读写操作 - 更新 `MeetingMcpToolService` 和 `MeetingCommandServiceImpl` 以使用新的进度服务 - 优化会议进度解析和状态更新逻辑dev_na
parent
7989b6aa11
commit
188809305e
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.imeeting.common;
|
||||||
|
|
||||||
|
public enum MeetingProgressStage {
|
||||||
|
QUEUED("queued", 10, false),
|
||||||
|
ASR_SUBMITTED("asr_submitted", 20, false),
|
||||||
|
ASR_RUNNING("asr_running", 30, false),
|
||||||
|
ASR_COMPLETED("asr_completed", 40, false),
|
||||||
|
CHAPTER_RUNNING("chapter_running", 50, false),
|
||||||
|
SUMMARY_RUNNING("summary_running", 60, false),
|
||||||
|
COMPLETED("completed", 100, true),
|
||||||
|
FAILED("failed", 100, true);
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final int order;
|
||||||
|
private final boolean terminal;
|
||||||
|
|
||||||
|
MeetingProgressStage(String code, int order, boolean terminal) {
|
||||||
|
this.code = code;
|
||||||
|
this.order = order;
|
||||||
|
this.terminal = terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOrder() {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTerminal() {
|
||||||
|
return terminal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,22 @@ public final class RedisKeys {
|
||||||
return "biz:meeting:summary:lock:" + meetingId;
|
return "biz:meeting:summary:lock:" + meetingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String meetingAsrScheduleLockKey() {
|
||||||
|
return "biz:meeting:asr:schedule:lock";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String meetingAsrPermitSetKey() {
|
||||||
|
return "biz:meeting:asr:permit:set";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String meetingAsrPermitSyncLockKey() {
|
||||||
|
return "biz:meeting:asr:permit:sync:lock";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String meetingAsrRefillLockKey() {
|
||||||
|
return "biz:meeting:asr:refill:lock";
|
||||||
|
}
|
||||||
|
|
||||||
public static String realtimeMeetingSocketSessionKey(String sessionToken) {
|
public static String realtimeMeetingSocketSessionKey(String sessionToken) {
|
||||||
return "biz:meeting:realtime:socket:" + sessionToken;
|
return "biz:meeting:realtime:socket:" + sessionToken;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ public final class SysParamKeys {
|
||||||
public static final String MEETING_OFFLINE_AUDIO_MAX_SIZE_MB = "meeting.offline_audio.max_size_mb";
|
public static final String MEETING_OFFLINE_AUDIO_MAX_SIZE_MB = "meeting.offline_audio.max_size_mb";
|
||||||
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";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.imeeting.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class MeetingAsyncExecutorConfig {
|
||||||
|
|
||||||
|
@Bean("asrDispatchExecutor")
|
||||||
|
public Executor asrDispatchExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(8);
|
||||||
|
executor.setMaxPoolSize(16);
|
||||||
|
executor.setQueueCapacity(128);
|
||||||
|
executor.setThreadNamePrefix("imeeting-asr-dispatch-");
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("asrTaskExecutor")
|
||||||
|
public Executor asrTaskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(8);
|
||||||
|
executor.setMaxPoolSize(16);
|
||||||
|
executor.setQueueCapacity(0);
|
||||||
|
executor.setThreadNamePrefix("imeeting-asr-worker-");
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("summaryDispatchExecutor")
|
||||||
|
public Executor summaryDispatchExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(4);
|
||||||
|
executor.setMaxPoolSize(8);
|
||||||
|
executor.setQueueCapacity(128);
|
||||||
|
executor.setThreadNamePrefix("imeeting-summary-dispatch-");
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("summaryTaskExecutor")
|
||||||
|
public Executor summaryTaskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(4);
|
||||||
|
executor.setMaxPoolSize(8);
|
||||||
|
executor.setQueueCapacity(256);
|
||||||
|
executor.setThreadNamePrefix("imeeting-summary-worker-");
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,9 +22,11 @@ import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.MeetingAccessService;
|
import com.imeeting.service.biz.MeetingAccessService;
|
||||||
import com.imeeting.service.biz.MeetingCommandService;
|
import com.imeeting.service.biz.MeetingCommandService;
|
||||||
|
import com.imeeting.service.biz.MeetingProgressService;
|
||||||
import com.imeeting.service.biz.MeetingQueryService;
|
import com.imeeting.service.biz.MeetingQueryService;
|
||||||
import com.imeeting.service.biz.MeetingService;
|
import com.imeeting.service.biz.MeetingService;
|
||||||
import com.imeeting.service.biz.PromptTemplateService;
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
|
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;
|
||||||
import com.unisbase.dto.PageResult;
|
import com.unisbase.dto.PageResult;
|
||||||
|
|
@ -38,7 +40,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
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 org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
@ -63,7 +65,6 @@ import java.util.stream.Collectors;
|
||||||
@Tag(name = "Android会议接口")
|
@Tag(name = "Android会议接口")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/android/meetings")
|
@RequestMapping("/api/android/meetings")
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AndroidMeetingController {
|
public class AndroidMeetingController {
|
||||||
|
|
||||||
private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
|
private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
|
||||||
|
|
@ -80,9 +81,60 @@ 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 StringRedisTemplate redisTemplate;
|
private final MeetingProgressService meetingProgressService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public AndroidMeetingController(AndroidAuthService androidAuthService,
|
||||||
|
LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||||
|
MeetingQueryService meetingQueryService,
|
||||||
|
MeetingAccessService meetingAccessService,
|
||||||
|
MeetingCommandService meetingCommandService,
|
||||||
|
MeetingService meetingService,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
PromptTemplateService promptTemplateService,
|
||||||
|
SysUserMapper sysUserMapper,
|
||||||
|
MeetingProgressService meetingProgressService,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this.androidAuthService = androidAuthService;
|
||||||
|
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
|
||||||
|
this.meetingQueryService = meetingQueryService;
|
||||||
|
this.meetingAccessService = meetingAccessService;
|
||||||
|
this.meetingCommandService = meetingCommandService;
|
||||||
|
this.meetingService = meetingService;
|
||||||
|
this.aiTaskService = aiTaskService;
|
||||||
|
this.promptTemplateService = promptTemplateService;
|
||||||
|
this.sysUserMapper = sysUserMapper;
|
||||||
|
this.meetingProgressService = meetingProgressService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndroidMeetingController(AndroidAuthService androidAuthService,
|
||||||
|
LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||||
|
MeetingQueryService meetingQueryService,
|
||||||
|
MeetingAccessService meetingAccessService,
|
||||||
|
MeetingCommandService meetingCommandService,
|
||||||
|
MeetingService meetingService,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
PromptTemplateService promptTemplateService,
|
||||||
|
SysUserMapper sysUserMapper,
|
||||||
|
StringRedisTemplate redisTemplate,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this(
|
||||||
|
androidAuthService,
|
||||||
|
legacyMeetingAdapterService,
|
||||||
|
meetingQueryService,
|
||||||
|
meetingAccessService,
|
||||||
|
meetingCommandService,
|
||||||
|
meetingService,
|
||||||
|
aiTaskService,
|
||||||
|
promptTemplateService,
|
||||||
|
sysUserMapper,
|
||||||
|
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
||||||
|
objectMapper
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "创建Android离线会议")
|
@Operation(summary = "创建Android离线会议")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||||
|
|
@ -254,6 +306,13 @@ public class AndroidMeetingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
|
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
|
||||||
|
if (asrTask != null && Integer.valueOf(0).equals(asrTask.getStatus()) && realtimeProgress != null && realtimeProgress <= 0) {
|
||||||
|
return new LegacyMeetingPreviewResult(
|
||||||
|
"400",
|
||||||
|
"会议正在处理中",
|
||||||
|
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
|
||||||
|
);
|
||||||
|
}
|
||||||
if (realtimeProgress != null) {
|
if (realtimeProgress != null) {
|
||||||
if (realtimeProgress >= 100) {
|
if (realtimeProgress >= 100) {
|
||||||
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
|
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
|
||||||
|
|
@ -348,16 +407,7 @@ public class AndroidMeetingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer resolveRealtimeProgress(Long meetingId) {
|
private Integer resolveRealtimeProgress(Long meetingId) {
|
||||||
String rawProgress = redisTemplate.opsForValue().get(RedisKeys.meetingProgressKey(meetingId));
|
return meetingProgressService.resolvePercent(meetingId);
|
||||||
if (rawProgress == null || rawProgress.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JsonNode progress = objectMapper.readTree(rawProgress);
|
|
||||||
return progress.hasNonNull("percent") ? progress.path("percent").asInt() : null;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildFailureMessage(AiTask failedTask, String stageName) {
|
private String buildFailureMessage(AiTask failedTask, String stageName) {
|
||||||
|
|
@ -368,11 +418,11 @@ public class AndroidMeetingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRunningAsr(AiTask task) {
|
private boolean isRunningAsr(AiTask task) {
|
||||||
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
|
return task != null && Integer.valueOf(1).equals(task.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRunningSummary(AiTask task) {
|
private boolean isRunningSummary(AiTask task) {
|
||||||
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
|
return task != null && Integer.valueOf(1).equals(task.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFailed(AiTask task) {
|
private boolean isFailed(AiTask task) {
|
||||||
|
|
@ -492,9 +542,9 @@ public class AndroidMeetingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
|
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
|
||||||
return Integer.valueOf(1).equals(meetingStatus)
|
return (Integer.valueOf(1).equals(meetingStatus) && (asrTask == null || !Integer.valueOf(0).equals(asrTask.getStatus())))
|
||||||
|| isRunningAsr(asrTask)
|
|| isRunningAsr(asrTask)
|
||||||
|| (hasAudio && !isSummaryStage);
|
|| (asrTask == null && hasAudio && !isSummaryStage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String formatDateTime(LocalDateTime value) {
|
private String formatDateTime(LocalDateTime value) {
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,11 @@ import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.MeetingAccessService;
|
import com.imeeting.service.biz.MeetingAccessService;
|
||||||
import com.imeeting.service.biz.MeetingCommandService;
|
import com.imeeting.service.biz.MeetingCommandService;
|
||||||
|
import com.imeeting.service.biz.MeetingProgressService;
|
||||||
import com.imeeting.service.biz.MeetingQueryService;
|
import com.imeeting.service.biz.MeetingQueryService;
|
||||||
import com.imeeting.service.biz.MeetingService;
|
import com.imeeting.service.biz.MeetingService;
|
||||||
import com.imeeting.service.biz.PromptTemplateService;
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
|
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
||||||
import com.unisbase.common.annotation.Log;
|
import com.unisbase.common.annotation.Log;
|
||||||
import com.unisbase.dto.PageResult;
|
import com.unisbase.dto.PageResult;
|
||||||
import com.unisbase.entity.SysUser;
|
import com.unisbase.entity.SysUser;
|
||||||
|
|
@ -80,7 +82,7 @@ public class LegacyMeetingController {
|
||||||
private final PromptTemplateService promptTemplateService;
|
private final PromptTemplateService promptTemplateService;
|
||||||
private final MeetingTranscriptMapper meetingTranscriptMapper;
|
private final MeetingTranscriptMapper meetingTranscriptMapper;
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final MeetingProgressService meetingProgressService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||||
|
|
@ -89,9 +91,9 @@ public class LegacyMeetingController {
|
||||||
MeetingCommandService meetingCommandService,
|
MeetingCommandService meetingCommandService,
|
||||||
MeetingService meetingService,
|
MeetingService meetingService,
|
||||||
AiTaskService aiTaskService,
|
AiTaskService aiTaskService,
|
||||||
PromptTemplateService promptTemplateService,
|
PromptTemplateService promptTemplateService,
|
||||||
MeetingTranscriptMapper meetingTranscriptMapper,
|
MeetingTranscriptMapper meetingTranscriptMapper,
|
||||||
SysUserMapper sysUserMapper) {
|
SysUserMapper sysUserMapper) {
|
||||||
this(legacyMeetingAdapterService,
|
this(legacyMeetingAdapterService,
|
||||||
meetingQueryService,
|
meetingQueryService,
|
||||||
meetingAccessService,
|
meetingAccessService,
|
||||||
|
|
@ -101,11 +103,10 @@ public class LegacyMeetingController {
|
||||||
promptTemplateService,
|
promptTemplateService,
|
||||||
meetingTranscriptMapper,
|
meetingTranscriptMapper,
|
||||||
sysUserMapper,
|
sysUserMapper,
|
||||||
null,
|
(MeetingProgressService) null,
|
||||||
new ObjectMapper());
|
new ObjectMapper());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||||
MeetingQueryService meetingQueryService,
|
MeetingQueryService meetingQueryService,
|
||||||
MeetingAccessService meetingAccessService,
|
MeetingAccessService meetingAccessService,
|
||||||
|
|
@ -117,6 +118,33 @@ public class LegacyMeetingController {
|
||||||
SysUserMapper sysUserMapper,
|
SysUserMapper sysUserMapper,
|
||||||
StringRedisTemplate redisTemplate,
|
StringRedisTemplate redisTemplate,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper) {
|
||||||
|
this(
|
||||||
|
legacyMeetingAdapterService,
|
||||||
|
meetingQueryService,
|
||||||
|
meetingAccessService,
|
||||||
|
meetingCommandService,
|
||||||
|
meetingService,
|
||||||
|
aiTaskService,
|
||||||
|
promptTemplateService,
|
||||||
|
meetingTranscriptMapper,
|
||||||
|
sysUserMapper,
|
||||||
|
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
||||||
|
objectMapper
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||||
|
MeetingQueryService meetingQueryService,
|
||||||
|
MeetingAccessService meetingAccessService,
|
||||||
|
MeetingCommandService meetingCommandService,
|
||||||
|
MeetingService meetingService,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
PromptTemplateService promptTemplateService,
|
||||||
|
MeetingTranscriptMapper meetingTranscriptMapper,
|
||||||
|
SysUserMapper sysUserMapper,
|
||||||
|
MeetingProgressService meetingProgressService,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
|
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
|
||||||
this.meetingQueryService = meetingQueryService;
|
this.meetingQueryService = meetingQueryService;
|
||||||
this.meetingAccessService = meetingAccessService;
|
this.meetingAccessService = meetingAccessService;
|
||||||
|
|
@ -126,7 +154,7 @@ public class LegacyMeetingController {
|
||||||
this.promptTemplateService = promptTemplateService;
|
this.promptTemplateService = promptTemplateService;
|
||||||
this.meetingTranscriptMapper = meetingTranscriptMapper;
|
this.meetingTranscriptMapper = meetingTranscriptMapper;
|
||||||
this.sysUserMapper = sysUserMapper;
|
this.sysUserMapper = sysUserMapper;
|
||||||
this.redisTemplate = redisTemplate;
|
this.meetingProgressService = meetingProgressService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,6 +298,13 @@ public class LegacyMeetingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
|
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
|
||||||
|
if (asrTask != null && Integer.valueOf(0).equals(asrTask.getStatus()) && realtimeProgress != null && realtimeProgress <= 0) {
|
||||||
|
return new LegacyMeetingPreviewResult(
|
||||||
|
"400",
|
||||||
|
"会议正在处理中",
|
||||||
|
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
|
||||||
|
);
|
||||||
|
}
|
||||||
if (realtimeProgress != null) {
|
if (realtimeProgress != null) {
|
||||||
if (realtimeProgress >= 100) {
|
if (realtimeProgress >= 100) {
|
||||||
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
|
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
|
||||||
|
|
@ -389,19 +424,10 @@ public class LegacyMeetingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer resolveRealtimeProgress(Long meetingId) {
|
private Integer resolveRealtimeProgress(Long meetingId) {
|
||||||
if (redisTemplate == null) {
|
if (meetingProgressService == null) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String rawProgress = redisTemplate.opsForValue().get(RedisKeys.meetingProgressKey(meetingId));
|
|
||||||
if (rawProgress == null || rawProgress.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JsonNode progress = objectMapper.readTree(rawProgress);
|
|
||||||
return progress.hasNonNull("percent") ? progress.path("percent").asInt() : null;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return meetingProgressService.resolvePercent(meetingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LegacyMeetingProcessingStatusResponse buildListStatus(MeetingVO meeting) {
|
private LegacyMeetingProcessingStatusResponse buildListStatus(MeetingVO meeting) {
|
||||||
|
|
@ -440,11 +466,11 @@ public class LegacyMeetingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRunningAsr(AiTask task) {
|
private boolean isRunningAsr(AiTask task) {
|
||||||
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
|
return task != null && Integer.valueOf(1).equals(task.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRunningSummary(AiTask task) {
|
private boolean isRunningSummary(AiTask task) {
|
||||||
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
|
return task != null && Integer.valueOf(1).equals(task.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFailed(AiTask task) {
|
private boolean isFailed(AiTask task) {
|
||||||
|
|
@ -593,9 +619,9 @@ public class LegacyMeetingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
|
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
|
||||||
return Integer.valueOf(1).equals(meetingStatus)
|
return (Integer.valueOf(1).equals(meetingStatus) && (asrTask == null || !Integer.valueOf(0).equals(asrTask.getStatus())))
|
||||||
|| isRunningAsr(asrTask)
|
|| isRunningAsr(asrTask)
|
||||||
|| (hasAudio && !isSummaryStage);
|
|| (asrTask == null && hasAudio && !isSummaryStage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String formatDateTime(LocalDateTime value) {
|
private String formatDateTime(LocalDateTime value) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.imeeting.controller.biz;
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.imeeting.common.MeetingConstants;
|
import com.imeeting.common.MeetingConstants;
|
||||||
import com.imeeting.common.RedisKeys;
|
import com.imeeting.common.RedisKeys;
|
||||||
import com.imeeting.common.SysParamKeys;
|
import com.imeeting.common.SysParamKeys;
|
||||||
|
|
@ -22,16 +23,20 @@ import com.imeeting.dto.biz.UpdateMeetingBasicCommand;
|
||||||
import com.imeeting.dto.biz.UpdateMeetingParticipantsCommand;
|
import com.imeeting.dto.biz.UpdateMeetingParticipantsCommand;
|
||||||
import com.imeeting.dto.biz.UpdateMeetingSummaryCommand;
|
import com.imeeting.dto.biz.UpdateMeetingSummaryCommand;
|
||||||
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
|
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
|
||||||
|
import com.imeeting.entity.biz.AiTask;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.MeetingAccessService;
|
import com.imeeting.service.biz.MeetingAccessService;
|
||||||
import com.imeeting.service.biz.MeetingCommandService;
|
import com.imeeting.service.biz.MeetingCommandService;
|
||||||
import com.imeeting.service.biz.MeetingExportService;
|
import com.imeeting.service.biz.MeetingExportService;
|
||||||
|
import com.imeeting.service.biz.MeetingProgressService;
|
||||||
import com.imeeting.service.biz.MeetingQueryService;
|
import com.imeeting.service.biz.MeetingQueryService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||||
import com.imeeting.service.biz.PromptTemplateService;
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
||||||
import com.imeeting.service.biz.impl.MeetingAudioUploadSupport;
|
import com.imeeting.service.biz.impl.MeetingAudioUploadSupport;
|
||||||
|
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;
|
||||||
import com.unisbase.dto.PageResult;
|
import com.unisbase.dto.PageResult;
|
||||||
|
|
@ -40,6 +45,7 @@ import com.unisbase.service.SysParamService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
|
@ -79,9 +85,11 @@ public class MeetingController {
|
||||||
private final RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService;
|
private final RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService;
|
||||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||||
private final MeetingAudioUploadSupport meetingAudioUploadSupport;
|
private final MeetingAudioUploadSupport meetingAudioUploadSupport;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final MeetingProgressService meetingProgressService;
|
||||||
private final SysParamService sysParamService;
|
private final SysParamService sysParamService;
|
||||||
|
private AiTaskService compatibilityAiTaskService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
public MeetingController(MeetingQueryService meetingQueryService,
|
public MeetingController(MeetingQueryService meetingQueryService,
|
||||||
MeetingCommandService meetingCommandService,
|
MeetingCommandService meetingCommandService,
|
||||||
MeetingAccessService meetingAccessService,
|
MeetingAccessService meetingAccessService,
|
||||||
|
|
@ -91,7 +99,7 @@ public class MeetingController {
|
||||||
RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService,
|
RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService,
|
||||||
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
||||||
MeetingAudioUploadSupport meetingAudioUploadSupport,
|
MeetingAudioUploadSupport meetingAudioUploadSupport,
|
||||||
StringRedisTemplate redisTemplate,
|
MeetingProgressService meetingProgressService,
|
||||||
SysParamService sysParamService) {
|
SysParamService sysParamService) {
|
||||||
this.meetingQueryService = meetingQueryService;
|
this.meetingQueryService = meetingQueryService;
|
||||||
this.meetingCommandService = meetingCommandService;
|
this.meetingCommandService = meetingCommandService;
|
||||||
|
|
@ -102,10 +110,38 @@ public class MeetingController {
|
||||||
this.realtimeMeetingSocketSessionService = realtimeMeetingSocketSessionService;
|
this.realtimeMeetingSocketSessionService = realtimeMeetingSocketSessionService;
|
||||||
this.realtimeMeetingSessionStateService = realtimeMeetingSessionStateService;
|
this.realtimeMeetingSessionStateService = realtimeMeetingSessionStateService;
|
||||||
this.meetingAudioUploadSupport = meetingAudioUploadSupport;
|
this.meetingAudioUploadSupport = meetingAudioUploadSupport;
|
||||||
this.redisTemplate = redisTemplate;
|
this.meetingProgressService = meetingProgressService;
|
||||||
this.sysParamService = sysParamService;
|
this.sysParamService = sysParamService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MeetingController(MeetingQueryService meetingQueryService,
|
||||||
|
MeetingCommandService meetingCommandService,
|
||||||
|
MeetingAccessService meetingAccessService,
|
||||||
|
MeetingExportService meetingExportService,
|
||||||
|
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||||
|
PromptTemplateService promptTemplateService,
|
||||||
|
RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService,
|
||||||
|
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
||||||
|
AiTaskService unusedAiTaskService,
|
||||||
|
MeetingAudioUploadSupport meetingAudioUploadSupport,
|
||||||
|
StringRedisTemplate redisTemplate,
|
||||||
|
SysParamService sysParamService) {
|
||||||
|
this(
|
||||||
|
meetingQueryService,
|
||||||
|
meetingCommandService,
|
||||||
|
meetingAccessService,
|
||||||
|
meetingExportService,
|
||||||
|
meetingTranscriptFileService,
|
||||||
|
promptTemplateService,
|
||||||
|
realtimeMeetingSocketSessionService,
|
||||||
|
realtimeMeetingSessionStateService,
|
||||||
|
meetingAudioUploadSupport,
|
||||||
|
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, new com.fasterxml.jackson.databind.ObjectMapper()),
|
||||||
|
sysParamService
|
||||||
|
);
|
||||||
|
this.compatibilityAiTaskService = unusedAiTaskService;
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "查询会议处理进度")
|
@Operation(summary = "查询会议处理进度")
|
||||||
@GetMapping("/{id}/progress")
|
@GetMapping("/{id}/progress")
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
|
@ -113,29 +149,21 @@ public class MeetingController {
|
||||||
LoginUser loginUser = currentLoginUser();
|
LoginUser loginUser = currentLoginUser();
|
||||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||||
|
Map<String, Object> progress = meetingProgressService.getProgressMap(id);
|
||||||
String key = RedisKeys.meetingProgressKey(id);
|
if (compatibilityAiTaskService != null && "Waiting...".equals(progress.get("message"))) {
|
||||||
String json = redisTemplate.opsForValue().get(key);
|
AiTask asrTask = compatibilityAiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||||
if (json != null) {
|
.eq(AiTask::getMeetingId, id)
|
||||||
try {
|
.eq(AiTask::getTaskType, "ASR")
|
||||||
return ApiResponse.ok(new com.fasterxml.jackson.databind.ObjectMapper().readValue(json, Map.class));
|
.orderByDesc(AiTask::getId)
|
||||||
} catch (Exception ex) {
|
.last("LIMIT 1"));
|
||||||
return ApiResponse.error("进度解析失败");
|
if (asrTask != null && Integer.valueOf(0).equals(asrTask.getStatus())) {
|
||||||
|
return ApiResponse.ok(Map.of("percent", 0, "message", "排队中,等待 ASR 执行名额..."));
|
||||||
|
}
|
||||||
|
if (asrTask != null && Integer.valueOf(1).equals(asrTask.getStatus())) {
|
||||||
|
return ApiResponse.ok(Map.of("percent", 5, "message", "识别中,等待进度刷新..."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return ApiResponse.ok(progress);
|
||||||
Map<String, Object> fallback = new HashMap<>();
|
|
||||||
if (meeting.getStatus() == 3) {
|
|
||||||
fallback.put("percent", 100);
|
|
||||||
fallback.put("message", "Completed");
|
|
||||||
} else if (meeting.getStatus() == 4) {
|
|
||||||
fallback.put("percent", -1);
|
|
||||||
fallback.put("message", "Failed");
|
|
||||||
} else {
|
|
||||||
fallback.put("percent", 0);
|
|
||||||
fallback.put("message", "Waiting...");
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(fallback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "上传会议音频")
|
@Operation(summary = "上传会议音频")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "会议创建配置")
|
||||||
|
public class MeetingCreateConfigVO {
|
||||||
|
|
||||||
|
@Schema(description = "是否启用离线上传")
|
||||||
|
private Boolean offlineEnabled;
|
||||||
|
|
||||||
|
@Schema(description = "是否启用实时会议")
|
||||||
|
private Boolean realtimeEnabled;
|
||||||
|
|
||||||
|
@Schema(description = "离线音频上传大小上限,单位 MB")
|
||||||
|
private Long offlineAudioMaxSizeMb;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MeetingProgressSnapshot {
|
||||||
|
private Long meetingId;
|
||||||
|
private Long taskId;
|
||||||
|
private String taskType;
|
||||||
|
private Integer taskStatus;
|
||||||
|
private Integer meetingStatus;
|
||||||
|
private String stage;
|
||||||
|
private Integer stageOrder;
|
||||||
|
private Integer percent;
|
||||||
|
private String message;
|
||||||
|
private Integer eta;
|
||||||
|
private String externalTaskId;
|
||||||
|
private LocalDateTime queuedAt;
|
||||||
|
private LocalDateTime startedAt;
|
||||||
|
private LocalDateTime completedAt;
|
||||||
|
private Long updateAt;
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,9 @@ public class AiTask {
|
||||||
@Schema(description = "错误信息")
|
@Schema(description = "错误信息")
|
||||||
private String errorMsg;
|
private String errorMsg;
|
||||||
|
|
||||||
|
@Schema(description = "排队时间")
|
||||||
|
private LocalDateTime queuedAt;
|
||||||
|
|
||||||
@Schema(description = "开始时间")
|
@Schema(description = "开始时间")
|
||||||
private LocalDateTime startedAt;
|
private LocalDateTime startedAt;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,93 +4,93 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.imeeting.common.RedisKeys;
|
import com.imeeting.common.RedisKeys;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.mapper.biz.MeetingMapper;
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
import com.imeeting.support.TaskSecurityContextRunner;
|
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import com.imeeting.support.RedisValueSupport;
|
||||||
|
import com.imeeting.support.TaskSecurityContextRunner;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.ApplicationArguments;
|
import org.springframework.boot.ApplicationArguments;
|
||||||
import org.springframework.boot.ApplicationRunner;
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务自愈监听器:在系统重启后自动恢复挂起的 AI 任务
|
|
||||||
*/
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
||||||
|
|
||||||
private final MeetingMapper meetingMapper;
|
private final MeetingMapper meetingMapper;
|
||||||
private final AiTaskService aiTaskService;
|
private final AiTaskService aiTaskService;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final RedisValueSupport redisValueSupport;
|
||||||
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||||
|
private StringRedisTemplate compatibilityRedisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public MeetingTaskRecoveryListener(MeetingMapper meetingMapper,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
RedisValueSupport redisValueSupport,
|
||||||
|
TaskSecurityContextRunner taskSecurityContextRunner) {
|
||||||
|
this.meetingMapper = meetingMapper;
|
||||||
|
this.aiTaskService = aiTaskService;
|
||||||
|
this.redisValueSupport = redisValueSupport;
|
||||||
|
this.taskSecurityContextRunner = taskSecurityContextRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MeetingTaskRecoveryListener(MeetingMapper meetingMapper,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
StringRedisTemplate redisTemplate,
|
||||||
|
TaskSecurityContextRunner taskSecurityContextRunner) {
|
||||||
|
this(meetingMapper, aiTaskService, new RedisValueSupport(redisTemplate, new com.fasterxml.jackson.databind.ObjectMapper()), taskSecurityContextRunner);
|
||||||
|
this.compatibilityRedisTemplate = redisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(ApplicationArguments args) {
|
public void run(ApplicationArguments args) {
|
||||||
log.info("Starting meeting task self-healing check...");
|
log.info("Starting meeting task recovery check...");
|
||||||
|
|
||||||
// 1. 查询状态为 1(识别中) 或 2(总结中) 且未删除的会议
|
|
||||||
List<Meeting> pendingMeetings = taskSecurityContextRunner.callAsPlatformAdmin(() ->
|
List<Meeting> pendingMeetings = taskSecurityContextRunner.callAsPlatformAdmin(() ->
|
||||||
meetingMapper.selectList(new LambdaQueryWrapper<Meeting>()
|
meetingMapper.selectList(new LambdaQueryWrapper<Meeting>()
|
||||||
.in(Meeting::getStatus, 1, 2)
|
.in(Meeting::getStatus, 1, 2)
|
||||||
.eq(Meeting::getIsDeleted, 0))
|
.eq(Meeting::getIsDeleted, 0))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pendingMeetings.isEmpty()) {
|
if (pendingMeetings.isEmpty()) {
|
||||||
log.info("No pending tasks found. Recovery check completed.");
|
log.info("No pending meeting tasks found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
|
|
||||||
|
|
||||||
for (Meeting meeting : pendingMeetings) {
|
for (Meeting meeting : pendingMeetings) {
|
||||||
try {
|
try {
|
||||||
// 处理 1 小时以上的死任务
|
redisValueSupport.delete(RedisKeys.meetingPollingLockKey(meeting.getId()));
|
||||||
if (meeting.getUpdatedAt() != null && meeting.getUpdatedAt().isBefore(oneHourAgo)) {
|
redisValueSupport.delete(RedisKeys.meetingSummaryLockKey(meeting.getId()));
|
||||||
log.warn("Meeting {} is stuck for more than 1 hour, marking as failed", meeting.getId());
|
clearLegacyRedisState(meeting.getId());
|
||||||
markAsFailed(meeting, "任务因系统维护超时,请重新发起分析");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 清理旧的 Redis 锁和进度缓存,确保恢复线程能拿到控制权
|
if (Integer.valueOf(1).equals(meeting.getStatus())) {
|
||||||
redisTemplate.delete(RedisKeys.meetingPollingLockKey(meeting.getId()));
|
log.info("Recovering ASR task for meeting {}", meeting.getId());
|
||||||
redisTemplate.delete(RedisKeys.meetingSummaryLockKey(meeting.getId()));
|
|
||||||
|
|
||||||
// 3. 根据状态重新派发任务 (平滑拉起)
|
|
||||||
if (meeting.getStatus() == 1) {
|
|
||||||
log.info("Resuming ASR task for meeting {}", meeting.getId());
|
|
||||||
aiTaskService.dispatchTasks(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
|
aiTaskService.dispatchTasks(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
|
||||||
} else if (meeting.getStatus() == 2) {
|
} else if (Integer.valueOf(2).equals(meeting.getStatus())) {
|
||||||
log.info("Resuming Summary task for meeting {}", meeting.getId());
|
log.info("Recovering summary task for meeting {}", meeting.getId());
|
||||||
aiTaskService.dispatchSummaryTask(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
|
aiTaskService.dispatchSummaryTask(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增加小延迟防止惊群效应
|
|
||||||
TimeUnit.MILLISECONDS.sleep(200);
|
TimeUnit.MILLISECONDS.sleep(200);
|
||||||
|
} catch (Exception ex) {
|
||||||
} catch (Exception e) {
|
log.error("Failed to recover meeting task {}", meeting.getId(), ex);
|
||||||
log.error("Failed to recover meeting task {}", meeting.getId(), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Successfully processed {} pending tasks for recovery.", pendingMeetings.size());
|
log.info("Meeting task recovery processed {} meetings.", pendingMeetings.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markAsFailed(Meeting m, String reason) {
|
private void clearLegacyRedisState(Long meetingId) {
|
||||||
Meeting update = new Meeting();
|
if (compatibilityRedisTemplate == null || meetingId == null) {
|
||||||
update.setId(m.getId());
|
return;
|
||||||
update.setStatus(4); // 失败
|
}
|
||||||
taskSecurityContextRunner.runAsTenantUser(m.getTenantId(), m.getCreatorId(), () -> meetingMapper.updateById(update));
|
compatibilityRedisTemplate.delete(List.of(
|
||||||
|
RedisKeys.meetingAsrPermitSyncLockKey(),
|
||||||
// 同步 Redis 进度为失败
|
RedisKeys.meetingAsrRefillLockKey()
|
||||||
String progressKey = RedisKeys.meetingProgressKey(m.getId());
|
));
|
||||||
redisTemplate.opsForValue().set(progressKey,
|
compatibilityRedisTemplate.opsForSet().remove(RedisKeys.meetingAsrPermitSetKey(), String.valueOf(meetingId));
|
||||||
"{\"percent\":-1, \"message\":\"" + reason + "\", \"updateAt\":" + System.currentTimeMillis() + "}",
|
|
||||||
1, TimeUnit.HOURS);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
||||||
task.setTaskType(taskType);
|
task.setTaskType(taskType);
|
||||||
}
|
}
|
||||||
task.setStatus(0);
|
task.setStatus(0);
|
||||||
|
task.setQueuedAt(LocalDateTime.now());
|
||||||
task.setTaskConfig(taskConfig);
|
task.setTaskConfig(taskConfig);
|
||||||
task.setRequestData(null);
|
task.setRequestData(null);
|
||||||
task.setResponseData(null);
|
task.setResponseData(null);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.imeeting.service.biz;
|
||||||
|
|
||||||
|
import com.imeeting.common.MeetingProgressStage;
|
||||||
|
import com.imeeting.dto.biz.MeetingProgressSnapshot;
|
||||||
|
import com.imeeting.entity.biz.AiTask;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface MeetingProgressService {
|
||||||
|
void clear(Long meetingId);
|
||||||
|
|
||||||
|
Map<String, Object> getProgressMap(Long meetingId);
|
||||||
|
|
||||||
|
Integer resolvePercent(Long meetingId);
|
||||||
|
|
||||||
|
void markQueued(Long meetingId, AiTask task, Integer meetingStatus, String message);
|
||||||
|
|
||||||
|
void markQueuedAfterCommitOrNow(Long meetingId, AiTask task, Integer meetingStatus, String message);
|
||||||
|
|
||||||
|
void markStage(Long meetingId, AiTask task, Integer meetingStatus, MeetingProgressStage stage, int percent, String message, int eta);
|
||||||
|
|
||||||
|
void markStageAfterCommitOrNow(Long meetingId, AiTask task, Integer meetingStatus, MeetingProgressStage stage, int percent, String message, int eta);
|
||||||
|
|
||||||
|
void syncFromDatabase(Long meetingId);
|
||||||
|
|
||||||
|
void writeSnapshot(MeetingProgressSnapshot snapshot);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package com.imeeting.service.biz.impl;
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
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.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;
|
||||||
|
|
@ -19,16 +21,21 @@ import com.imeeting.support.TaskSecurityContextRunner;
|
||||||
import com.imeeting.service.biz.AiModelService;
|
import com.imeeting.service.biz.AiModelService;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.HotWordService;
|
import com.imeeting.service.biz.HotWordService;
|
||||||
|
import com.imeeting.service.biz.MeetingProgressService;
|
||||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptRevisionService;
|
import com.imeeting.service.biz.MeetingTranscriptRevisionService;
|
||||||
|
import com.imeeting.support.RedisValueSupport;
|
||||||
|
|
||||||
import com.unisbase.entity.SysUser;
|
import com.unisbase.entity.SysUser;
|
||||||
import com.unisbase.mapper.SysUserMapper;
|
import com.unisbase.mapper.SysUserMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import com.unisbase.service.SysParamService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -46,12 +53,12 @@ import java.nio.file.Paths;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> implements AiTaskService {
|
public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> implements AiTaskService {
|
||||||
|
|
||||||
private final MeetingMapper meetingMapper;
|
private final MeetingMapper meetingMapper;
|
||||||
|
|
@ -60,7 +67,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
private final HotWordService hotWordService;
|
private final HotWordService hotWordService;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final RedisValueSupport redisValueSupport;
|
||||||
|
private final MeetingProgressService meetingProgressService;
|
||||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||||
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
||||||
private final MeetingTranscriptRevisionService meetingTranscriptRevisionService;
|
private final MeetingTranscriptRevisionService meetingTranscriptRevisionService;
|
||||||
|
|
@ -68,6 +76,19 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler;
|
private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler;
|
||||||
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||||
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
||||||
|
private final SysParamService sysParamService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("asrTaskExecutor")
|
||||||
|
private Executor asrTaskExecutor;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("summaryTaskExecutor")
|
||||||
|
private Executor summaryTaskExecutor;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Lazy
|
||||||
|
private AiTaskService self;
|
||||||
|
|
||||||
@Value("${unisbase.app.server-base-url}")
|
@Value("${unisbase.app.server-base-url}")
|
||||||
private String serverBaseUrl;
|
private String serverBaseUrl;
|
||||||
|
|
@ -83,15 +104,89 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
.version(HttpClient.Version.HTTP_1_1)
|
.version(HttpClient.Version.HTTP_1_1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public AiTaskServiceImpl(MeetingMapper meetingMapper,
|
||||||
|
MeetingTranscriptMapper transcriptMapper,
|
||||||
|
AiModelService aiModelService,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
SysUserMapper sysUserMapper,
|
||||||
|
HotWordService hotWordService,
|
||||||
|
RedisValueSupport redisValueSupport,
|
||||||
|
MeetingProgressService meetingProgressService,
|
||||||
|
MeetingSummaryFileService meetingSummaryFileService,
|
||||||
|
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||||
|
MeetingTranscriptRevisionService meetingTranscriptRevisionService,
|
||||||
|
MeetingTranscriptChapterService meetingTranscriptChapterService,
|
||||||
|
MeetingSummaryPromptAssembler meetingSummaryPromptAssembler,
|
||||||
|
TaskSecurityContextRunner taskSecurityContextRunner,
|
||||||
|
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
|
||||||
|
SysParamService sysParamService) {
|
||||||
|
this.meetingMapper = meetingMapper;
|
||||||
|
this.transcriptMapper = transcriptMapper;
|
||||||
|
this.aiModelService = aiModelService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.sysUserMapper = sysUserMapper;
|
||||||
|
this.hotWordService = hotWordService;
|
||||||
|
this.redisValueSupport = redisValueSupport;
|
||||||
|
this.meetingProgressService = meetingProgressService;
|
||||||
|
this.meetingSummaryFileService = meetingSummaryFileService;
|
||||||
|
this.meetingTranscriptFileService = meetingTranscriptFileService;
|
||||||
|
this.meetingTranscriptRevisionService = meetingTranscriptRevisionService;
|
||||||
|
this.meetingTranscriptChapterService = meetingTranscriptChapterService;
|
||||||
|
this.meetingSummaryPromptAssembler = meetingSummaryPromptAssembler;
|
||||||
|
this.taskSecurityContextRunner = taskSecurityContextRunner;
|
||||||
|
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
|
||||||
|
this.sysParamService = sysParamService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AiTaskServiceImpl(MeetingMapper meetingMapper,
|
||||||
|
MeetingTranscriptMapper transcriptMapper,
|
||||||
|
AiModelService aiModelService,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
SysUserMapper sysUserMapper,
|
||||||
|
HotWordService hotWordService,
|
||||||
|
StringRedisTemplate redisTemplate,
|
||||||
|
MeetingSummaryFileService meetingSummaryFileService,
|
||||||
|
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||||
|
MeetingTranscriptRevisionService meetingTranscriptRevisionService,
|
||||||
|
MeetingTranscriptChapterService meetingTranscriptChapterService,
|
||||||
|
MeetingSummaryPromptAssembler meetingSummaryPromptAssembler,
|
||||||
|
TaskSecurityContextRunner taskSecurityContextRunner,
|
||||||
|
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger) {
|
||||||
|
this(
|
||||||
|
meetingMapper,
|
||||||
|
transcriptMapper,
|
||||||
|
aiModelService,
|
||||||
|
objectMapper,
|
||||||
|
sysUserMapper,
|
||||||
|
hotWordService,
|
||||||
|
new RedisValueSupport(redisTemplate, objectMapper),
|
||||||
|
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
||||||
|
meetingSummaryFileService,
|
||||||
|
meetingTranscriptFileService,
|
||||||
|
meetingTranscriptRevisionService,
|
||||||
|
meetingTranscriptChapterService,
|
||||||
|
meetingSummaryPromptAssembler,
|
||||||
|
taskSecurityContextRunner,
|
||||||
|
meetingExternalSummaryWebhookTrigger,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async
|
@Async("asrDispatchExecutor")
|
||||||
public void dispatchTasks(Long meetingId, Long tenantId, Long userId) {
|
public void dispatchTasks(Long meetingId, Long tenantId, Long userId) {
|
||||||
taskSecurityContextRunner.runAsTenantUser(tenantId, userId, () -> doDispatchTasks(meetingId));
|
Runnable task = () -> taskSecurityContextRunner.runAsTenantUser(tenantId, userId, () -> doDispatchTasks(meetingId));
|
||||||
|
if (asrTaskExecutor == null) {
|
||||||
|
task.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
asrTaskExecutor.execute(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doDispatchTasks(Long meetingId) {
|
private void doDispatchTasks(Long meetingId) {
|
||||||
String lockKey = RedisKeys.meetingPollingLockKey(meetingId);
|
String lockKey = RedisKeys.meetingPollingLockKey(meetingId);
|
||||||
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
|
Boolean acquired = redisValueSupport.setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
|
||||||
if (Boolean.FALSE.equals(acquired)) {
|
if (Boolean.FALSE.equals(acquired)) {
|
||||||
log.warn("Meeting {} is already being processed", meetingId);
|
log.warn("Meeting {} is already being processed", meetingId);
|
||||||
return;
|
return;
|
||||||
|
|
@ -107,6 +202,23 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
.orderByDesc(AiTask::getId)
|
.orderByDesc(AiTask::getId)
|
||||||
.last("limit 1"));
|
.last("limit 1"));
|
||||||
|
|
||||||
|
if (asrTask != null) {
|
||||||
|
if (Integer.valueOf(1).equals(asrTask.getStatus())) {
|
||||||
|
if (!prepareRunningAsrTaskForRecovery(meeting, asrTask)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (Integer.valueOf(0).equals(asrTask.getStatus())) {
|
||||||
|
if (asrTask.getQueuedAt() == null) {
|
||||||
|
asrTask.setQueuedAt(LocalDateTime.now());
|
||||||
|
this.updateById(asrTask);
|
||||||
|
}
|
||||||
|
if (!claimQueuedAsrTask(asrTask)) {
|
||||||
|
meetingProgressService.markQueued(meetingId, asrTask, 1, "已进入 ASR 队列,等待执行");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String asrText = "";
|
String asrText = "";
|
||||||
if (asrTask != null && canExecuteTask(asrTask)) {
|
if (asrTask != null && canExecuteTask(asrTask)) {
|
||||||
asrText = processAsrTask(meeting, asrTask);
|
asrText = processAsrTask(meeting, asrTask);
|
||||||
|
|
@ -135,6 +247,11 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
updateProgress(meetingId, -1, "未识别到可用于总结的转录内容", 0);
|
updateProgress(meetingId, -1, "未识别到可用于总结的转录内容", 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!asrText.isBlank()) {
|
||||||
|
meetingProgressService.markStage(meetingId, asrTask, 1, MeetingProgressStage.ASR_COMPLETED, 80, "转写完成,准备生成总结", 0);
|
||||||
|
self.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (chapterTask != null && canExecuteTask(chapterTask)) {
|
if (chapterTask != null && canExecuteTask(chapterTask)) {
|
||||||
executeChapterFlow(meeting, chapterTask);
|
executeChapterFlow(meeting, chapterTask);
|
||||||
}
|
}
|
||||||
|
|
@ -155,14 +272,20 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
updateMeetingStatus(meetingId, 4);
|
updateMeetingStatus(meetingId, 4);
|
||||||
updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
|
updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
|
||||||
} finally {
|
} finally {
|
||||||
redisTemplate.delete(lockKey);
|
redisValueSupport.delete(lockKey);
|
||||||
|
scheduleQueuedAsrTasks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async
|
@Async("summaryDispatchExecutor")
|
||||||
public void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId) {
|
public void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId) {
|
||||||
taskSecurityContextRunner.runAsTenantUser(tenantId, userId, () -> doDispatchSummaryTask(meetingId));
|
Runnable task = () -> taskSecurityContextRunner.runAsTenantUser(tenantId, userId, () -> doDispatchSummaryTask(meetingId));
|
||||||
|
if (summaryTaskExecutor == null) {
|
||||||
|
task.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
summaryTaskExecutor.execute(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doDispatchSummaryTask(Long meetingId) {
|
private void doDispatchSummaryTask(Long meetingId) {
|
||||||
|
|
@ -198,6 +321,164 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean prepareRunningAsrTaskForRecovery(Meeting meeting, AiTask asrTask) {
|
||||||
|
if (meeting == null || asrTask == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Long asrModelId = extractAsrModelId(asrTask);
|
||||||
|
String externalTaskId = extractExternalTaskId(asrTask);
|
||||||
|
if (asrModelId == null || externalTaskId == null || externalTaskId.isBlank()) {
|
||||||
|
requeueAsrTask(asrTask, "恢复时缺少有效外部 ASR 任务,已重新排队", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AiModelVO asrModel = aiModelService.getModelById(asrModelId, "ASR");
|
||||||
|
if (asrModel == null || !canResumeAsrTask(asrModel, meeting.getId(), externalTaskId)) {
|
||||||
|
requeueAsrTask(asrTask, "外部 ASR 状态异常,已重新排队", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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<AiTask>()
|
||||||
|
.eq(AiTask::getTaskType, "ASR")
|
||||||
|
.eq(AiTask::getStatus, 1));
|
||||||
|
if (runningCount >= maxConcurrent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int available = (int) Math.max(1, maxConcurrent - runningCount);
|
||||||
|
List<AiTask> dispatchable = list(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.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<AiTask>()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
return claimed;
|
||||||
|
} finally {
|
||||||
|
redisValueSupport.delete(scheduleLockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleQueuedAsrTasks() {
|
||||||
|
if (getBaseMapper() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String scheduleLockKey = RedisKeys.meetingAsrScheduleLockKey();
|
||||||
|
Boolean acquired = redisValueSupport.setIfAbsent(scheduleLockKey, "locked", 30, TimeUnit.SECONDS);
|
||||||
|
if (Boolean.FALSE.equals(acquired)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int maxConcurrent = resolveAsrMaxConcurrent();
|
||||||
|
long runningCount = count(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getTaskType, "ASR")
|
||||||
|
.eq(AiTask::getStatus, 1));
|
||||||
|
int available = (int) (maxConcurrent - runningCount);
|
||||||
|
if (available <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<AiTask> queuedTasks = list(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getTaskType, "ASR")
|
||||||
|
.eq(AiTask::getStatus, 0)
|
||||||
|
.orderByAsc(AiTask::getQueuedAt)
|
||||||
|
.orderByAsc(AiTask::getId)
|
||||||
|
.last("LIMIT " + available));
|
||||||
|
for (AiTask queuedTask : queuedTasks) {
|
||||||
|
Meeting queuedMeeting = meetingMapper.selectById(queuedTask.getMeetingId());
|
||||||
|
if (queuedMeeting == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.dispatchTasks(queuedMeeting.getId(), queuedMeeting.getTenantId(), queuedMeeting.getCreatorId());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
redisValueSupport.delete(scheduleLockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveAsrMaxConcurrent() {
|
||||||
|
if (sysParamService == null) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
String configured = sysParamService.getCachedParamValue(com.imeeting.common.SysParamKeys.MEETING_ASR_MAX_CONCURRENT, "2");
|
||||||
|
try {
|
||||||
|
return Math.max(1, Integer.parseInt(configured.trim()));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requeueAsrTask(AiTask task, String reason, boolean clearExternalTaskId) {
|
||||||
|
if (task == null || task.getId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
task.setStatus(0);
|
||||||
|
task.setQueuedAt(LocalDateTime.now());
|
||||||
|
task.setStartedAt(null);
|
||||||
|
task.setCompletedAt(null);
|
||||||
|
task.setErrorMsg(null);
|
||||||
|
if (clearExternalTaskId) {
|
||||||
|
clearAsrTaskId(task);
|
||||||
|
}
|
||||||
|
Map<String, Object> responseData = task.getResponseData() == null
|
||||||
|
? new HashMap<>()
|
||||||
|
: new HashMap<>(task.getResponseData());
|
||||||
|
responseData.put("requeueReason", reason);
|
||||||
|
responseData.put("requeuedAt", LocalDateTime.now().toString());
|
||||||
|
task.setResponseData(responseData);
|
||||||
|
updateById(task);
|
||||||
|
meetingProgressService.markQueued(task.getMeetingId(), task, 1, reason == null || reason.isBlank() ? "已重新进入 ASR 队列" : reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long extractAsrModelId(AiTask task) {
|
||||||
|
if (task == null || task.getTaskConfig() == null || task.getTaskConfig().get("asrModelId") == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(String.valueOf(task.getTaskConfig().get("asrModelId")));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractExternalTaskId(AiTask task) {
|
||||||
|
if (task == null || task.getResponseData() == null || task.getResponseData().get("task_id") == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return String.valueOf(task.getResponseData().get("task_id"));
|
||||||
|
}
|
||||||
|
|
||||||
private String processAsrTask(Meeting meeting, AiTask taskRecord) throws Exception {
|
private String processAsrTask(Meeting meeting, AiTask taskRecord) throws Exception {
|
||||||
updateMeetingStatus(meeting.getId(), 1);
|
updateMeetingStatus(meeting.getId(), 1);
|
||||||
|
|
||||||
|
|
@ -698,7 +979,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String summaryLockKey = RedisKeys.meetingSummaryLockKey(meeting.getId());
|
String summaryLockKey = RedisKeys.meetingSummaryLockKey(meeting.getId());
|
||||||
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(summaryLockKey, "locked", 30, TimeUnit.MINUTES);
|
Boolean acquired = redisValueSupport.setIfAbsent(summaryLockKey, "locked", 30, TimeUnit.MINUTES);
|
||||||
if (Boolean.FALSE.equals(acquired)) {
|
if (Boolean.FALSE.equals(acquired)) {
|
||||||
log.warn("Meeting {} summary is already being processed", meeting.getId());
|
log.warn("Meeting {} summary is already being processed", meeting.getId());
|
||||||
return;
|
return;
|
||||||
|
|
@ -713,7 +994,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
}
|
}
|
||||||
processSummaryTask(meeting, summarySource, sumTask);
|
processSummaryTask(meeting, summarySource, sumTask);
|
||||||
} finally {
|
} finally {
|
||||||
redisTemplate.delete(summaryLockKey);
|
redisValueSupport.delete(summaryLockKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -725,6 +1006,18 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
.last("limit 1"));
|
.last("limit 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AiTask findLatestTaskForProgress(Long meetingId) {
|
||||||
|
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
|
||||||
|
if (summaryTask != null && Integer.valueOf(1).equals(summaryTask.getStatus())) {
|
||||||
|
return summaryTask;
|
||||||
|
}
|
||||||
|
AiTask chapterTask = findLatestTask(meetingId, "CHAPTER");
|
||||||
|
if (chapterTask != null && Integer.valueOf(1).equals(chapterTask.getStatus())) {
|
||||||
|
return chapterTask;
|
||||||
|
}
|
||||||
|
return findLatestTask(meetingId, "ASR");
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isExternalSummaryModeEnabled() {
|
private boolean isExternalSummaryModeEnabled() {
|
||||||
return "EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode);
|
return "EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode);
|
||||||
}
|
}
|
||||||
|
|
@ -779,21 +1072,28 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
if (meetingId == null) {
|
if (meetingId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
MeetingProgressStage stage;
|
||||||
Map<String, Object> progress = new HashMap<>();
|
int meetingStatus;
|
||||||
progress.put("percent", percent);
|
if (percent < 0) {
|
||||||
progress.put("message", msg);
|
stage = MeetingProgressStage.FAILED;
|
||||||
progress.put("eta", eta);
|
meetingStatus = 4;
|
||||||
progress.put("updateAt", System.currentTimeMillis());
|
} else if (percent >= 100) {
|
||||||
redisTemplate.opsForValue().set(
|
stage = MeetingProgressStage.COMPLETED;
|
||||||
RedisKeys.meetingProgressKey(meetingId),
|
meetingStatus = 3;
|
||||||
objectMapper.writeValueAsString(progress),
|
} else if (percent >= 90) {
|
||||||
1,
|
stage = MeetingProgressStage.SUMMARY_RUNNING;
|
||||||
TimeUnit.HOURS
|
meetingStatus = 2;
|
||||||
);
|
} else if (percent >= 85) {
|
||||||
} catch (Exception e) {
|
stage = MeetingProgressStage.CHAPTER_RUNNING;
|
||||||
log.error("Redis progress update error", e);
|
meetingStatus = 2;
|
||||||
|
} else if (percent >= 5) {
|
||||||
|
stage = MeetingProgressStage.ASR_RUNNING;
|
||||||
|
meetingStatus = 1;
|
||||||
|
} else {
|
||||||
|
stage = MeetingProgressStage.QUEUED;
|
||||||
|
meetingStatus = 1;
|
||||||
}
|
}
|
||||||
|
meetingProgressService.markStage(meetingId, findLatestTaskForProgress(meetingId), meetingStatus, stage, percent, msg, eta);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String postJson(String url, Object body, String apiKey) throws Exception {
|
private String postJson(String url, Object body, String apiKey) throws Exception {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.HotWordService;
|
import com.imeeting.service.biz.HotWordService;
|
||||||
import com.imeeting.service.biz.MeetingCommandService;
|
import com.imeeting.service.biz.MeetingCommandService;
|
||||||
|
import com.imeeting.service.biz.MeetingProgressService;
|
||||||
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
||||||
import com.imeeting.service.biz.MeetingService;
|
import com.imeeting.service.biz.MeetingService;
|
||||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||||
|
|
@ -34,9 +35,10 @@ import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptRevisionService;
|
import com.imeeting.service.biz.MeetingTranscriptRevisionService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||||
|
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
||||||
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import lombok.RequiredArgsConstructor;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -55,7 +57,6 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MeetingCommandServiceImpl implements MeetingCommandService {
|
public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
|
|
||||||
private final MeetingService meetingService;
|
private final MeetingService meetingService;
|
||||||
|
|
@ -70,13 +71,82 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
private final MeetingRuntimeProfileResolver meetingRuntimeProfileResolver;
|
private final MeetingRuntimeProfileResolver meetingRuntimeProfileResolver;
|
||||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||||
private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService;
|
private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final MeetingProgressService meetingProgressService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
||||||
|
private StringRedisTemplate compatibilityRedisTemplate;
|
||||||
|
|
||||||
@Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}")
|
@Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}")
|
||||||
private String summaryOrchestrationMode;
|
private String summaryOrchestrationMode;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public MeetingCommandServiceImpl(MeetingService meetingService,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
HotWordService hotWordService,
|
||||||
|
com.imeeting.mapper.biz.MeetingTranscriptMapper transcriptMapper,
|
||||||
|
MeetingSummaryFileService meetingSummaryFileService,
|
||||||
|
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||||
|
MeetingTranscriptRevisionService meetingTranscriptRevisionService,
|
||||||
|
MeetingTranscriptChapterService meetingTranscriptChapterService,
|
||||||
|
MeetingDomainSupport meetingDomainSupport,
|
||||||
|
MeetingRuntimeProfileResolver meetingRuntimeProfileResolver,
|
||||||
|
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
||||||
|
RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService,
|
||||||
|
MeetingProgressService meetingProgressService,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger) {
|
||||||
|
this.meetingService = meetingService;
|
||||||
|
this.aiTaskService = aiTaskService;
|
||||||
|
this.hotWordService = hotWordService;
|
||||||
|
this.transcriptMapper = transcriptMapper;
|
||||||
|
this.meetingSummaryFileService = meetingSummaryFileService;
|
||||||
|
this.meetingTranscriptFileService = meetingTranscriptFileService;
|
||||||
|
this.meetingTranscriptRevisionService = meetingTranscriptRevisionService;
|
||||||
|
this.meetingTranscriptChapterService = meetingTranscriptChapterService;
|
||||||
|
this.meetingDomainSupport = meetingDomainSupport;
|
||||||
|
this.meetingRuntimeProfileResolver = meetingRuntimeProfileResolver;
|
||||||
|
this.realtimeMeetingSessionStateService = realtimeMeetingSessionStateService;
|
||||||
|
this.realtimeMeetingAudioStorageService = realtimeMeetingAudioStorageService;
|
||||||
|
this.meetingProgressService = meetingProgressService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MeetingCommandServiceImpl(MeetingService meetingService,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
HotWordService hotWordService,
|
||||||
|
com.imeeting.mapper.biz.MeetingTranscriptMapper transcriptMapper,
|
||||||
|
MeetingSummaryFileService meetingSummaryFileService,
|
||||||
|
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||||
|
MeetingTranscriptRevisionService meetingTranscriptRevisionService,
|
||||||
|
MeetingTranscriptChapterService meetingTranscriptChapterService,
|
||||||
|
MeetingDomainSupport meetingDomainSupport,
|
||||||
|
MeetingRuntimeProfileResolver meetingRuntimeProfileResolver,
|
||||||
|
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
||||||
|
RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService,
|
||||||
|
StringRedisTemplate redisTemplate,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger) {
|
||||||
|
this(
|
||||||
|
meetingService,
|
||||||
|
aiTaskService,
|
||||||
|
hotWordService,
|
||||||
|
transcriptMapper,
|
||||||
|
meetingSummaryFileService,
|
||||||
|
meetingTranscriptFileService,
|
||||||
|
meetingTranscriptRevisionService,
|
||||||
|
meetingTranscriptChapterService,
|
||||||
|
meetingDomainSupport,
|
||||||
|
meetingRuntimeProfileResolver,
|
||||||
|
realtimeMeetingSessionStateService,
|
||||||
|
realtimeMeetingAudioStorageService,
|
||||||
|
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
||||||
|
objectMapper,
|
||||||
|
meetingExternalSummaryWebhookTrigger
|
||||||
|
);
|
||||||
|
this.compatibilityRedisTemplate = redisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource) {
|
public MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource) {
|
||||||
|
|
@ -92,6 +162,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
asrTask.setMeetingId(meeting.getId());
|
asrTask.setMeetingId(meeting.getId());
|
||||||
asrTask.setTaskType("ASR");
|
asrTask.setTaskType("ASR");
|
||||||
asrTask.setStatus(0);
|
asrTask.setStatus(0);
|
||||||
|
asrTask.setQueuedAt(java.time.LocalDateTime.now());
|
||||||
|
|
||||||
Map<String, Object> asrConfig = new HashMap<>();
|
Map<String, Object> asrConfig = new HashMap<>();
|
||||||
asrConfig.put("asrModelId", runtimeProfile.getResolvedAsrModelId());
|
asrConfig.put("asrModelId", runtimeProfile.getResolvedAsrModelId());
|
||||||
|
|
@ -204,7 +275,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
.eq(AiTask::getMeetingId, id));
|
.eq(AiTask::getMeetingId, id));
|
||||||
meetingService.removeById(id);
|
meetingService.removeById(id);
|
||||||
realtimeMeetingSessionStateService.clear(id);
|
realtimeMeetingSessionStateService.clear(id);
|
||||||
redisTemplate.delete(RedisKeys.meetingProgressKey(id));
|
meetingProgressService.clear(id);
|
||||||
deleteMeetingArtifactsAfterCommit(id);
|
deleteMeetingArtifactsAfterCommit(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -434,6 +505,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
|
|
||||||
private void resetAiTask(AiTask task, Map<String, Object> taskConfig) {
|
private void resetAiTask(AiTask task, Map<String, Object> taskConfig) {
|
||||||
task.setStatus(0);
|
task.setStatus(0);
|
||||||
|
task.setQueuedAt(java.time.LocalDateTime.now());
|
||||||
task.setTaskConfig(taskConfig);
|
task.setTaskConfig(taskConfig);
|
||||||
task.setRequestData(null);
|
task.setRequestData(null);
|
||||||
task.setResponseData(null);
|
task.setResponseData(null);
|
||||||
|
|
@ -878,6 +950,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
if (asrTask == null || asrTask.getTaskConfig() == null || asrTask.getTaskConfig().get("asrModelId") == null) {
|
if (asrTask == null || asrTask.getTaskConfig() == null || asrTask.getTaskConfig().get("asrModelId") == null) {
|
||||||
throw new RuntimeException("未找到可用的识别任务配置");
|
throw new RuntimeException("未找到可用的识别任务配置");
|
||||||
}
|
}
|
||||||
|
if (Integer.valueOf(1).equals(asrTask.getStatus()) && !Integer.valueOf(4).equals(meeting.getStatus())) {
|
||||||
|
throw new RuntimeException("当前会议转写任务仍在处理中,请勿重复重试");
|
||||||
|
}
|
||||||
|
|
||||||
AiTask summaryTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
AiTask summaryTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||||
.eq(AiTask::getMeetingId, meetingId)
|
.eq(AiTask::getMeetingId, meetingId)
|
||||||
|
|
@ -917,10 +992,20 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
|
|
||||||
meeting.setStatus(1);
|
meeting.setStatus(1);
|
||||||
meetingService.updateById(meeting);
|
meetingService.updateById(meeting);
|
||||||
|
clearLegacyDispatchState(meetingId);
|
||||||
updateMeetingProgress(meetingId, 0, "已重新提交识别任务,等待 ASR 处理...", 0);
|
updateMeetingProgress(meetingId, 0, "已重新提交识别任务,等待 ASR 处理...", 0);
|
||||||
dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearLegacyDispatchState(Long meetingId) {
|
||||||
|
if (compatibilityRedisTemplate == null || meetingId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
compatibilityRedisTemplate.delete(RedisKeys.meetingPollingLockKey(meetingId));
|
||||||
|
compatibilityRedisTemplate.delete(RedisKeys.meetingSummaryLockKey(meetingId));
|
||||||
|
compatibilityRedisTemplate.opsForSet().remove(RedisKeys.meetingAsrPermitSetKey(), String.valueOf(meetingId));
|
||||||
|
}
|
||||||
|
|
||||||
private void ensureExternalSummaryModeEnabled() {
|
private void ensureExternalSummaryModeEnabled() {
|
||||||
if (!"EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) {
|
if (!"EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) {
|
||||||
throw new RuntimeException("外部 n8n 总结编排模式未开启");
|
throw new RuntimeException("外部 n8n 总结编排模式未开启");
|
||||||
|
|
@ -1055,21 +1140,28 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMeetingProgress(Long meetingId, int percent, String message, int eta) {
|
private void updateMeetingProgress(Long meetingId, int percent, String message, int eta) {
|
||||||
try {
|
com.imeeting.common.MeetingProgressStage stage;
|
||||||
Map<String, Object> progress = new HashMap<>();
|
int meetingStatus;
|
||||||
progress.put("percent", percent);
|
if (percent < 0) {
|
||||||
progress.put("message", message);
|
stage = com.imeeting.common.MeetingProgressStage.FAILED;
|
||||||
progress.put("eta", eta);
|
meetingStatus = 4;
|
||||||
progress.put("updateAt", System.currentTimeMillis());
|
} else if (percent >= 100) {
|
||||||
redisTemplate.opsForValue().set(
|
stage = com.imeeting.common.MeetingProgressStage.COMPLETED;
|
||||||
RedisKeys.meetingProgressKey(meetingId),
|
meetingStatus = 3;
|
||||||
objectMapper.writeValueAsString(progress),
|
} else if (percent >= 90) {
|
||||||
1,
|
stage = com.imeeting.common.MeetingProgressStage.SUMMARY_RUNNING;
|
||||||
TimeUnit.HOURS
|
meetingStatus = 2;
|
||||||
);
|
} else if (percent >= 85) {
|
||||||
} catch (Exception ignored) {
|
stage = com.imeeting.common.MeetingProgressStage.CHAPTER_RUNNING;
|
||||||
// Ignore progress write failures.
|
meetingStatus = 2;
|
||||||
|
} else if (percent >= 5) {
|
||||||
|
stage = com.imeeting.common.MeetingProgressStage.ASR_RUNNING;
|
||||||
|
meetingStatus = 1;
|
||||||
|
} else {
|
||||||
|
stage = com.imeeting.common.MeetingProgressStage.QUEUED;
|
||||||
|
meetingStatus = 1;
|
||||||
}
|
}
|
||||||
|
meetingProgressService.markStageAfterCommitOrNow(meetingId, null, meetingStatus, stage, percent, message, eta);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RealtimeMeetingResumeConfig buildRealtimeResumeConfig(CreateRealtimeMeetingCommand command,
|
private RealtimeMeetingResumeConfig buildRealtimeResumeConfig(CreateRealtimeMeetingCommand command,
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ public class MeetingDomainSupport {
|
||||||
chapterTask.setMeetingId(meetingId);
|
chapterTask.setMeetingId(meetingId);
|
||||||
chapterTask.setTaskType("CHAPTER");
|
chapterTask.setTaskType("CHAPTER");
|
||||||
chapterTask.setStatus(0);
|
chapterTask.setStatus(0);
|
||||||
|
chapterTask.setQueuedAt(LocalDateTime.now());
|
||||||
chapterTask.setTaskConfig(meetingSummaryPromptAssembler.buildTaskConfig(
|
chapterTask.setTaskConfig(meetingSummaryPromptAssembler.buildTaskConfig(
|
||||||
summaryModelId,
|
summaryModelId,
|
||||||
chapterModelId,
|
chapterModelId,
|
||||||
|
|
@ -141,6 +142,7 @@ public class MeetingDomainSupport {
|
||||||
sumTask.setMeetingId(meetingId);
|
sumTask.setMeetingId(meetingId);
|
||||||
sumTask.setTaskType("SUMMARY");
|
sumTask.setTaskType("SUMMARY");
|
||||||
sumTask.setStatus(0);
|
sumTask.setStatus(0);
|
||||||
|
sumTask.setQueuedAt(LocalDateTime.now());
|
||||||
sumTask.setTaskConfig(meetingSummaryPromptAssembler.buildTaskConfig(
|
sumTask.setTaskConfig(meetingSummaryPromptAssembler.buildTaskConfig(
|
||||||
summaryModelId,
|
summaryModelId,
|
||||||
chapterModelId,
|
chapterModelId,
|
||||||
|
|
@ -393,11 +395,11 @@ public class MeetingDomainSupport {
|
||||||
} else {
|
} else {
|
||||||
vo.setParticipantIds(Collections.emptyList());
|
vo.setParticipantIds(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
fillLatestTaskAttemptInfo(meeting, vo);
|
||||||
if (includeSummary) {
|
if (includeSummary) {
|
||||||
vo.setSummaryContent(meetingSummaryFileService.loadSummaryContent(meeting));
|
vo.setSummaryContent(meetingSummaryFileService.loadSummaryContent(meeting));
|
||||||
vo.setAnalysis(meetingSummaryFileService.loadSummaryAnalysis(meeting));
|
vo.setAnalysis(meetingSummaryFileService.loadSummaryAnalysis(meeting));
|
||||||
vo.setLastUserPrompt(resolveLastSummaryUserPrompt(meeting));
|
vo.setLastUserPrompt(resolveLastSummaryUserPrompt(meeting));
|
||||||
fillLatestTaskAttemptInfo(meeting, vo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.imeeting.common.MeetingProgressStage;
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.biz.MeetingProgressSnapshot;
|
||||||
|
import com.imeeting.entity.biz.AiTask;
|
||||||
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
import com.imeeting.mapper.biz.AiTaskMapper;
|
||||||
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
|
import com.imeeting.service.biz.MeetingProgressService;
|
||||||
|
import com.imeeting.support.RedisValueSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MeetingProgressServiceImpl implements MeetingProgressService {
|
||||||
|
|
||||||
|
private static final long PROGRESS_TTL_HOURS = 1L;
|
||||||
|
|
||||||
|
private final MeetingMapper meetingMapper;
|
||||||
|
private final AiTaskMapper aiTaskMapper;
|
||||||
|
private final RedisValueSupport redisValueSupport;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisValueSupport.delete(RedisKeys.meetingProgressKey(meetingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getProgressMap(Long meetingId) {
|
||||||
|
MeetingProgressSnapshot snapshot = redisValueSupport.getJson(RedisKeys.meetingProgressKey(meetingId), MeetingProgressSnapshot.class);
|
||||||
|
if (snapshot == null) {
|
||||||
|
snapshot = buildFallbackSnapshot(meetingId);
|
||||||
|
if (snapshot != null) {
|
||||||
|
writeSnapshot(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (snapshot == null) {
|
||||||
|
return Map.of("percent", 0, "message", "Waiting...");
|
||||||
|
}
|
||||||
|
return objectMapper.convertValue(snapshot, Map.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer resolvePercent(Long meetingId) {
|
||||||
|
MeetingProgressSnapshot snapshot = redisValueSupport.getJson(RedisKeys.meetingProgressKey(meetingId), MeetingProgressSnapshot.class);
|
||||||
|
if (snapshot != null && snapshot.getPercent() != null) {
|
||||||
|
return snapshot.getPercent();
|
||||||
|
}
|
||||||
|
MeetingProgressSnapshot fallback = buildFallbackSnapshot(meetingId);
|
||||||
|
return fallback == null ? null : fallback.getPercent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markQueued(Long meetingId, AiTask task, Integer meetingStatus, String message) {
|
||||||
|
writeSnapshot(buildSnapshot(meetingId, task, meetingStatus, MeetingProgressStage.QUEUED, 0, message, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markQueuedAfterCommitOrNow(Long meetingId, AiTask task, Integer meetingStatus, String message) {
|
||||||
|
afterCommitOrNow(() -> markQueued(meetingId, task, meetingStatus, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markStage(Long meetingId, AiTask task, Integer meetingStatus, MeetingProgressStage stage, int percent, String message, int eta) {
|
||||||
|
writeSnapshot(buildSnapshot(meetingId, task, meetingStatus, stage, percent, message, eta));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markStageAfterCommitOrNow(Long meetingId, AiTask task, Integer meetingStatus, MeetingProgressStage stage, int percent, String message, int eta) {
|
||||||
|
afterCommitOrNow(() -> markStage(meetingId, task, meetingStatus, stage, percent, message, eta));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncFromDatabase(Long meetingId) {
|
||||||
|
MeetingProgressSnapshot snapshot = buildFallbackSnapshot(meetingId);
|
||||||
|
if (snapshot != null) {
|
||||||
|
writeSnapshot(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeSnapshot(MeetingProgressSnapshot snapshot) {
|
||||||
|
if (snapshot == null || snapshot.getMeetingId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MeetingProgressSnapshot existing = redisValueSupport.getJson(RedisKeys.meetingProgressKey(snapshot.getMeetingId()), MeetingProgressSnapshot.class);
|
||||||
|
if (!shouldReplace(existing, snapshot)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisValueSupport.setJson(RedisKeys.meetingProgressKey(snapshot.getMeetingId()), snapshot, PROGRESS_TTL_HOURS, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MeetingProgressSnapshot buildSnapshot(Long meetingId,
|
||||||
|
AiTask task,
|
||||||
|
Integer meetingStatus,
|
||||||
|
MeetingProgressStage stage,
|
||||||
|
int percent,
|
||||||
|
String message,
|
||||||
|
int eta) {
|
||||||
|
String externalTaskId = null;
|
||||||
|
if (task != null && task.getResponseData() != null && task.getResponseData().get("task_id") != null) {
|
||||||
|
externalTaskId = String.valueOf(task.getResponseData().get("task_id"));
|
||||||
|
}
|
||||||
|
return MeetingProgressSnapshot.builder()
|
||||||
|
.meetingId(meetingId)
|
||||||
|
.taskId(task == null ? null : task.getId())
|
||||||
|
.taskType(task == null ? null : task.getTaskType())
|
||||||
|
.taskStatus(task == null ? null : task.getStatus())
|
||||||
|
.meetingStatus(meetingStatus)
|
||||||
|
.stage(stage.getCode())
|
||||||
|
.stageOrder(stage.getOrder())
|
||||||
|
.percent(percent)
|
||||||
|
.message(message)
|
||||||
|
.eta(eta)
|
||||||
|
.externalTaskId(externalTaskId)
|
||||||
|
.queuedAt(task == null ? null : task.getQueuedAt())
|
||||||
|
.startedAt(task == null ? null : task.getStartedAt())
|
||||||
|
.completedAt(task == null ? null : task.getCompletedAt())
|
||||||
|
.updateAt(System.currentTimeMillis())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MeetingProgressSnapshot buildFallbackSnapshot(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Meeting meeting = meetingMapper.selectById(meetingId);
|
||||||
|
if (meeting == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
AiTask latestTask = aiTaskMapper.selectOne(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getMeetingId, meetingId)
|
||||||
|
.orderByDesc(AiTask::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (meeting.getStatus() != null && meeting.getStatus() == 3) {
|
||||||
|
return buildSnapshot(meetingId, latestTask, meeting.getStatus(), MeetingProgressStage.COMPLETED, 100, "处理完成", 0);
|
||||||
|
}
|
||||||
|
if (meeting.getStatus() != null && meeting.getStatus() == 4) {
|
||||||
|
String message = latestTask != null && latestTask.getErrorMsg() != null && !latestTask.getErrorMsg().isBlank()
|
||||||
|
? latestTask.getErrorMsg()
|
||||||
|
: "处理失败";
|
||||||
|
return buildSnapshot(meetingId, latestTask, meeting.getStatus(), MeetingProgressStage.FAILED, -1, message, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
AiTask latestSummary = findLatestTask(meetingId, "SUMMARY");
|
||||||
|
if (latestSummary != null && Integer.valueOf(1).equals(latestSummary.getStatus())) {
|
||||||
|
return buildSnapshot(meetingId, latestSummary, meeting.getStatus(), MeetingProgressStage.SUMMARY_RUNNING, 90, "正在生成会议总结...", 0);
|
||||||
|
}
|
||||||
|
AiTask latestChapter = findLatestTask(meetingId, "CHAPTER");
|
||||||
|
if (latestChapter != null && Integer.valueOf(1).equals(latestChapter.getStatus())) {
|
||||||
|
return buildSnapshot(meetingId, latestChapter, meeting.getStatus(), MeetingProgressStage.CHAPTER_RUNNING, 85, "正在生成会议章节...", 0);
|
||||||
|
}
|
||||||
|
AiTask latestAsr = findLatestTask(meetingId, "ASR");
|
||||||
|
if (latestAsr != null) {
|
||||||
|
if (Integer.valueOf(1).equals(latestAsr.getStatus())) {
|
||||||
|
return buildSnapshot(meetingId, latestAsr, meeting.getStatus(), MeetingProgressStage.ASR_RUNNING, 10, "正在识别音频...", 0);
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(0).equals(latestAsr.getStatus())) {
|
||||||
|
return buildSnapshot(meetingId, latestAsr, meeting.getStatus(), MeetingProgressStage.QUEUED, 0, "已进入 ASR 队列,等待执行", 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buildSnapshot(meetingId, latestTask, meeting.getStatus(), MeetingProgressStage.QUEUED, 0, "Waiting...", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiTask findLatestTask(Long meetingId, String taskType) {
|
||||||
|
return aiTaskMapper.selectOne(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getMeetingId, meetingId)
|
||||||
|
.eq(AiTask::getTaskType, taskType)
|
||||||
|
.orderByDesc(AiTask::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldReplace(MeetingProgressSnapshot existing, MeetingProgressSnapshot candidate) {
|
||||||
|
if (candidate == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (existing == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRequeue(existing, candidate)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTerminal(existing) && !isTerminal(candidate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isTerminal(candidate)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int existingOrder = existing.getStageOrder() == null ? 0 : existing.getStageOrder();
|
||||||
|
int candidateOrder = candidate.getStageOrder() == null ? 0 : candidate.getStageOrder();
|
||||||
|
if (candidateOrder > existingOrder) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (candidateOrder < existingOrder) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int existingPercent = existing.getPercent() == null ? Integer.MIN_VALUE : existing.getPercent();
|
||||||
|
int candidatePercent = candidate.getPercent() == null ? Integer.MIN_VALUE : candidate.getPercent();
|
||||||
|
if (candidatePercent >= existingPercent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long existingUpdateAt = existing.getUpdateAt() == null ? 0L : existing.getUpdateAt();
|
||||||
|
Long candidateUpdateAt = candidate.getUpdateAt() == null ? 0L : candidate.getUpdateAt();
|
||||||
|
return candidateUpdateAt >= existingUpdateAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTerminal(MeetingProgressSnapshot snapshot) {
|
||||||
|
return snapshot != null
|
||||||
|
&& snapshot.getStage() != null
|
||||||
|
&& (MeetingProgressStage.COMPLETED.getCode().equals(snapshot.getStage())
|
||||||
|
|| MeetingProgressStage.FAILED.getCode().equals(snapshot.getStage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRequeue(MeetingProgressSnapshot existing, MeetingProgressSnapshot candidate) {
|
||||||
|
return candidate.getTaskStatus() != null
|
||||||
|
&& candidate.getTaskStatus() == 0
|
||||||
|
&& MeetingProgressStage.QUEUED.getCode().equals(candidate.getStage())
|
||||||
|
&& candidate.getQueuedAt() != null
|
||||||
|
&& (existing.getQueuedAt() == null || candidate.getQueuedAt().isAfter(existing.getQueuedAt()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void afterCommitOrNow(Runnable runnable) {
|
||||||
|
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
|
runnable.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
@Override
|
||||||
|
public void afterCommit() {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.imeeting.common.MeetingProgressStage;
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.biz.MeetingProgressSnapshot;
|
||||||
|
import com.imeeting.entity.biz.AiTask;
|
||||||
|
import com.imeeting.service.biz.MeetingProgressService;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class RedisOnlyMeetingProgressServiceAdapter implements MeetingProgressService {
|
||||||
|
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public RedisOnlyMeetingProgressServiceAdapter(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
this.objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear(Long meetingId) {
|
||||||
|
if (redisTemplate == null || meetingId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getProgressMap(Long meetingId) {
|
||||||
|
MeetingProgressSnapshot snapshot = readSnapshot(meetingId);
|
||||||
|
if (snapshot == null) {
|
||||||
|
return Map.of("percent", 0, "message", "Waiting...");
|
||||||
|
}
|
||||||
|
return objectMapper.convertValue(snapshot, Map.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer resolvePercent(Long meetingId) {
|
||||||
|
MeetingProgressSnapshot snapshot = readSnapshot(meetingId);
|
||||||
|
return snapshot == null ? null : snapshot.getPercent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markQueued(Long meetingId, AiTask task, Integer meetingStatus, String message) {
|
||||||
|
writeSnapshot(buildSnapshot(meetingId, task, meetingStatus, MeetingProgressStage.QUEUED, 0, message, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markQueuedAfterCommitOrNow(Long meetingId, AiTask task, Integer meetingStatus, String message) {
|
||||||
|
markQueued(meetingId, task, meetingStatus, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markStage(Long meetingId, AiTask task, Integer meetingStatus, MeetingProgressStage stage, int percent, String message, int eta) {
|
||||||
|
writeSnapshot(buildSnapshot(meetingId, task, meetingStatus, stage, percent, message, eta));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markStageAfterCommitOrNow(Long meetingId, AiTask task, Integer meetingStatus, MeetingProgressStage stage, int percent, String message, int eta) {
|
||||||
|
markStage(meetingId, task, meetingStatus, stage, percent, message, eta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncFromDatabase(Long meetingId) {
|
||||||
|
// No-op for constructor compatibility in tests.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeSnapshot(MeetingProgressSnapshot snapshot) {
|
||||||
|
if (redisTemplate == null || snapshot == null || snapshot.getMeetingId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
redisTemplate.opsForValue().set(
|
||||||
|
RedisKeys.meetingProgressKey(snapshot.getMeetingId()),
|
||||||
|
objectMapper.writeValueAsString(snapshot),
|
||||||
|
1,
|
||||||
|
TimeUnit.HOURS
|
||||||
|
);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Compatibility adapter keeps test setup lightweight.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MeetingProgressSnapshot readSnapshot(Long meetingId) {
|
||||||
|
if (redisTemplate == null || meetingId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String raw = redisTemplate.opsForValue().get(RedisKeys.meetingProgressKey(meetingId));
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return objectMapper.readValue(raw, MeetingProgressSnapshot.class);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MeetingProgressSnapshot buildSnapshot(Long meetingId,
|
||||||
|
AiTask task,
|
||||||
|
Integer meetingStatus,
|
||||||
|
MeetingProgressStage stage,
|
||||||
|
int percent,
|
||||||
|
String message,
|
||||||
|
int eta) {
|
||||||
|
return MeetingProgressSnapshot.builder()
|
||||||
|
.meetingId(meetingId)
|
||||||
|
.taskId(task == null ? null : task.getId())
|
||||||
|
.taskType(task == null ? null : task.getTaskType())
|
||||||
|
.taskStatus(task == null ? null : task.getStatus())
|
||||||
|
.meetingStatus(meetingStatus)
|
||||||
|
.stage(stage.getCode())
|
||||||
|
.stageOrder(stage.getOrder())
|
||||||
|
.percent(percent)
|
||||||
|
.message(message)
|
||||||
|
.eta(eta)
|
||||||
|
.queuedAt(task == null ? null : task.getQueuedAt())
|
||||||
|
.startedAt(task == null ? null : task.getStartedAt())
|
||||||
|
.completedAt(task == null ? null : task.getCompletedAt())
|
||||||
|
.updateAt(System.currentTimeMillis())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,16 +19,18 @@ import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.entity.biz.PromptTemplate;
|
import com.imeeting.entity.biz.PromptTemplate;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.MeetingAccessService;
|
import com.imeeting.service.biz.MeetingAccessService;
|
||||||
|
import com.imeeting.service.biz.MeetingProgressService;
|
||||||
import com.imeeting.service.biz.MeetingQueryService;
|
import com.imeeting.service.biz.MeetingQueryService;
|
||||||
import com.imeeting.service.biz.MeetingService;
|
import com.imeeting.service.biz.MeetingService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||||
import com.imeeting.service.biz.PromptTemplateService;
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
|
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
||||||
import com.unisbase.dto.PageResult;
|
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 lombok.RequiredArgsConstructor;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
@ -44,7 +46,6 @@ import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MeetingMcpToolService {
|
public class MeetingMcpToolService {
|
||||||
|
|
||||||
private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
|
private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
|
||||||
|
|
@ -60,9 +61,56 @@ public class MeetingMcpToolService {
|
||||||
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
||||||
private final MeetingTranscriptChapterService meetingTranscriptChapterService;
|
private final MeetingTranscriptChapterService meetingTranscriptChapterService;
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final MeetingProgressService meetingProgressService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public MeetingMcpToolService(MeetingQueryService meetingQueryService,
|
||||||
|
MeetingAccessService meetingAccessService,
|
||||||
|
MeetingService meetingService,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
PromptTemplateService promptTemplateService,
|
||||||
|
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||||
|
MeetingTranscriptChapterService meetingTranscriptChapterService,
|
||||||
|
SysUserMapper sysUserMapper,
|
||||||
|
MeetingProgressService meetingProgressService,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this.meetingQueryService = meetingQueryService;
|
||||||
|
this.meetingAccessService = meetingAccessService;
|
||||||
|
this.meetingService = meetingService;
|
||||||
|
this.aiTaskService = aiTaskService;
|
||||||
|
this.promptTemplateService = promptTemplateService;
|
||||||
|
this.meetingTranscriptFileService = meetingTranscriptFileService;
|
||||||
|
this.meetingTranscriptChapterService = meetingTranscriptChapterService;
|
||||||
|
this.sysUserMapper = sysUserMapper;
|
||||||
|
this.meetingProgressService = meetingProgressService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MeetingMcpToolService(MeetingQueryService meetingQueryService,
|
||||||
|
MeetingAccessService meetingAccessService,
|
||||||
|
MeetingService meetingService,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
PromptTemplateService promptTemplateService,
|
||||||
|
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||||
|
MeetingTranscriptChapterService meetingTranscriptChapterService,
|
||||||
|
SysUserMapper sysUserMapper,
|
||||||
|
StringRedisTemplate redisTemplate,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this(
|
||||||
|
meetingQueryService,
|
||||||
|
meetingAccessService,
|
||||||
|
meetingService,
|
||||||
|
aiTaskService,
|
||||||
|
promptTemplateService,
|
||||||
|
meetingTranscriptFileService,
|
||||||
|
meetingTranscriptChapterService,
|
||||||
|
sysUserMapper,
|
||||||
|
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
||||||
|
objectMapper
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Value("${unisbase.app.server-base-url:}")
|
@Value("${unisbase.app.server-base-url:}")
|
||||||
private String serverBaseUrl;
|
private String serverBaseUrl;
|
||||||
|
|
||||||
|
|
@ -192,6 +240,13 @@ public class MeetingMcpToolService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
|
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
|
||||||
|
if (asrTask != null && Integer.valueOf(0).equals(asrTask.getStatus()) && realtimeProgress != null && realtimeProgress <= 0) {
|
||||||
|
return new LegacyMeetingPreviewResult(
|
||||||
|
"400",
|
||||||
|
"会议正在处理中",
|
||||||
|
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
|
||||||
|
);
|
||||||
|
}
|
||||||
if (realtimeProgress != null) {
|
if (realtimeProgress != null) {
|
||||||
if (realtimeProgress >= 100) {
|
if (realtimeProgress >= 100) {
|
||||||
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
|
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
|
||||||
|
|
@ -340,11 +395,11 @@ public class MeetingMcpToolService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRunningAsr(AiTask task) {
|
private boolean isRunningAsr(AiTask task) {
|
||||||
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
|
return task != null && Integer.valueOf(1).equals(task.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRunningSummary(AiTask task) {
|
private boolean isRunningSummary(AiTask task) {
|
||||||
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
|
return task != null && Integer.valueOf(1).equals(task.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFailed(AiTask task) {
|
private boolean isFailed(AiTask task) {
|
||||||
|
|
@ -514,22 +569,13 @@ public class MeetingMcpToolService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
|
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
|
||||||
return Integer.valueOf(1).equals(meetingStatus)
|
return (Integer.valueOf(1).equals(meetingStatus) && (asrTask == null || !Integer.valueOf(0).equals(asrTask.getStatus())))
|
||||||
|| isRunningAsr(asrTask)
|
|| isRunningAsr(asrTask)
|
||||||
|| (hasAudio && !isSummaryStage);
|
|| (asrTask == null && hasAudio && !isSummaryStage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer resolveRealtimeProgress(Long meetingId) {
|
private Integer resolveRealtimeProgress(Long meetingId) {
|
||||||
String rawProgress = redisTemplate.opsForValue().get(RedisKeys.meetingProgressKey(meetingId));
|
return meetingProgressService.resolvePercent(meetingId);
|
||||||
if (rawProgress == null || rawProgress.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JsonNode progress = objectMapper.readTree(rawProgress);
|
|
||||||
return progress.hasNonNull("percent") ? progress.path("percent").asInt() : null;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private LegacyMeetingProcessingStatusResponse processingStatus(String overallStatus, int overallProgress, String currentStage) {
|
private LegacyMeetingProcessingStatusResponse processingStatus(String overallStatus, int overallProgress, String currentStage) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.imeeting.support;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RedisValueSupport {
|
||||||
|
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public <T> T getJson(String key, Class<T> type) {
|
||||||
|
String raw = getString(key);
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(raw, type);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Failed to parse redis json, key={}", key, ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getString(String key) {
|
||||||
|
try {
|
||||||
|
return redisTemplate.opsForValue().get(key);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Failed to get redis value, key={}", key, ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJson(String key, Object value, long ttl, TimeUnit unit) {
|
||||||
|
try {
|
||||||
|
redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl, unit);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Failed to write redis json, key={}", key, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean setIfAbsent(String key, String value, long ttl, TimeUnit unit) {
|
||||||
|
try {
|
||||||
|
return redisTemplate.opsForValue().setIfAbsent(key, value, ttl, unit);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Failed to acquire redis lock, key={}", key, ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String key) {
|
||||||
|
try {
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Failed to delete redis key, key={}", key, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Collection<String> keys) {
|
||||||
|
try {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Failed to delete redis keys, keys={}", keys, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -369,6 +369,16 @@ const MeetingProgressDisplay: React.FC<{
|
||||||
}> = ({ meetingId, onComplete, onProgressUpdate, onProgressChange, compact, inline }) => {
|
}> = ({ meetingId, onComplete, onProgressUpdate, onProgressChange, compact, inline }) => {
|
||||||
const [progress, setProgress] = useState<MeetingProgress | null>(null);
|
const [progress, setProgress] = useState<MeetingProgress | null>(null);
|
||||||
|
|
||||||
|
const onCompleteRef = useRef(onComplete);
|
||||||
|
const onProgressUpdateRef = useRef(onProgressUpdate);
|
||||||
|
const onProgressChangeRef = useRef(onProgressChange);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onCompleteRef.current = onComplete;
|
||||||
|
onProgressUpdateRef.current = onProgressUpdate;
|
||||||
|
onProgressChangeRef.current = onProgressChange;
|
||||||
|
}, [onComplete, onProgressUpdate, onProgressChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let completed = false;
|
let completed = false;
|
||||||
|
|
||||||
|
|
@ -383,10 +393,10 @@ const MeetingProgressDisplay: React.FC<{
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (detailRes.data?.data) {
|
if (detailRes.data?.data) {
|
||||||
onProgressUpdate?.(detailRes.data.data);
|
onProgressUpdateRef.current?.(detailRes.data.data);
|
||||||
if (detailRes.data.data.status !== 1 && detailRes.data.data.status !== 2) {
|
if (detailRes.data.data.status !== 1 && detailRes.data.data.status !== 2) {
|
||||||
completed = true;
|
completed = true;
|
||||||
onComplete();
|
onCompleteRef.current?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -394,10 +404,10 @@ const MeetingProgressDisplay: React.FC<{
|
||||||
if (progressRes.data?.data) {
|
if (progressRes.data?.data) {
|
||||||
const nextProgress = progressRes.data.data;
|
const nextProgress = progressRes.data.data;
|
||||||
setProgress(nextProgress);
|
setProgress(nextProgress);
|
||||||
onProgressChange?.(nextProgress);
|
onProgressChangeRef.current?.(nextProgress);
|
||||||
if (nextProgress.percent === 100 || nextProgress.percent < 0) {
|
if (nextProgress.percent === 100 || nextProgress.percent < 0) {
|
||||||
completed = true;
|
completed = true;
|
||||||
onComplete();
|
onCompleteRef.current?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -411,7 +421,7 @@ const MeetingProgressDisplay: React.FC<{
|
||||||
completed = true;
|
completed = true;
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
};
|
};
|
||||||
}, [meetingId, onComplete, onProgressChange, onProgressUpdate]);
|
}, [meetingId]);
|
||||||
|
|
||||||
const percent = progress?.percent || 0;
|
const percent = progress?.percent || 0;
|
||||||
const isError = percent < 0;
|
const isError = percent < 0;
|
||||||
|
|
@ -2066,7 +2076,7 @@ const MeetingDetail: React.FC = () => {
|
||||||
meetingId={meeting.id}
|
meetingId={meeting.id}
|
||||||
onComplete={() => fetchData(meeting.id)}
|
onComplete={() => fetchData(meeting.id)}
|
||||||
onProgressUpdate={(updated) => {
|
onProgressUpdate={(updated) => {
|
||||||
if (updated.status === 2 || updated.status !== meeting.status) {
|
if (updated.status !== meeting.status) {
|
||||||
void fetchData(updated.id);
|
void fetchData(updated.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue