feat: 添加实时会议配置选项和会话状态批量查询功能
- 在 `CreateRealtimeMeetingCommand` 中添加 `mode`, `language`, `enablePunctuation`, `enableItn`, `enableTextRefine`, 和 `saveAudio` 字段 - 更新 `MeetingCommandServiceImpl` 以支持新的实时会议配置选项 - 添加 `getRealtimeSessionStatuses` 接口,支持批量查询实时会议会话状态 - 更新前端API和组件,支持新的配置选项和批量查询功能dev_na
parent
ff47c34349
commit
24c3835b79
|
|
@ -28,6 +28,7 @@ import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
|||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
|
@ -50,6 +51,7 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -141,7 +143,7 @@ public class MeetingController {
|
|||
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingVO> create(@RequestBody CreateMeetingCommand command) {
|
||||
public ApiResponse<MeetingVO> create(@Valid @RequestBody CreateMeetingCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
assertPromptAvailable(command.getPromptId(), loginUser);
|
||||
return ApiResponse.ok(meetingCommandService.createMeeting(
|
||||
|
|
@ -154,7 +156,7 @@ public class MeetingController {
|
|||
|
||||
@PostMapping("/realtime/start")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingVO> createRealtime(@RequestBody CreateRealtimeMeetingCommand command) {
|
||||
public ApiResponse<MeetingVO> createRealtime(@Valid @RequestBody CreateRealtimeMeetingCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
assertPromptAvailable(command.getPromptId(), loginUser);
|
||||
return ApiResponse.ok(meetingCommandService.createRealtimeMeeting(
|
||||
|
|
@ -235,6 +237,34 @@ public class MeetingController {
|
|||
return ApiResponse.ok(realtimeMeetingSessionStateService.getStatus(id));
|
||||
}
|
||||
|
||||
@PostMapping("/realtime/session-status/batch")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<Long, RealtimeMeetingSessionStatusVO>> getRealtimeSessionStatuses(@RequestBody List<Long> ids) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Map<Long, RealtimeMeetingSessionStatusVO> result = new LinkedHashMap<>();
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
Map<Long, RealtimeMeetingSessionStatusVO> statuses = realtimeMeetingSessionStateService.getStatuses(ids);
|
||||
for (Long id : ids) {
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanManageRealtimeMeeting(meeting, loginUser);
|
||||
RealtimeMeetingSessionStatusVO status = statuses.get(id);
|
||||
if (status != null) {
|
||||
result.put(id, status);
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
// Preserve previous per-item fallback behavior for inaccessible meetings.
|
||||
}
|
||||
}
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/realtime/transcripts")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> appendRealtimeTranscripts(@PathVariable Long id, @RequestBody List<RealtimeTranscriptItemDTO> items) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
|
@ -8,16 +10,22 @@ import java.util.List;
|
|||
|
||||
@Data
|
||||
public class CreateMeetingCommand {
|
||||
@NotBlank(message = "会议标题不能为空")
|
||||
private String title;
|
||||
|
||||
@NotNull(message = "会议时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime meetingTime;
|
||||
|
||||
private String participants;
|
||||
private String tags;
|
||||
@NotBlank(message = "音频地址不能为空")
|
||||
private String audioUrl;
|
||||
@NotNull(message = "识别模型不能为空")
|
||||
private Long asrModelId;
|
||||
@NotNull(message = "总结模型不能为空")
|
||||
private Long summaryModelId;
|
||||
@NotNull(message = "提示词模板不能为空")
|
||||
private Long promptId;
|
||||
private Integer useSpkId;
|
||||
private Boolean enableTextRefine;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
|
@ -8,16 +10,27 @@ import java.util.List;
|
|||
|
||||
@Data
|
||||
public class CreateRealtimeMeetingCommand {
|
||||
@NotBlank(message = "会议标题不能为空")
|
||||
private String title;
|
||||
|
||||
@NotNull(message = "会议时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime meetingTime;
|
||||
|
||||
private String participants;
|
||||
private String tags;
|
||||
@NotNull(message = "识别模型不能为空")
|
||||
private Long asrModelId;
|
||||
@NotNull(message = "总结模型不能为空")
|
||||
private Long summaryModelId;
|
||||
@NotNull(message = "提示词模板不能为空")
|
||||
private Long promptId;
|
||||
private String mode;
|
||||
private String language;
|
||||
private Integer useSpkId;
|
||||
private Boolean enablePunctuation;
|
||||
private Boolean enableItn;
|
||||
private Boolean enableTextRefine;
|
||||
private Boolean saveAudio;
|
||||
private List<String> hotWords;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.imeeting.service.biz;
|
|||
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface RealtimeMeetingSessionStateService {
|
||||
void initSessionIfAbsent(Long meetingId, Long tenantId, Long userId);
|
||||
|
||||
|
|
@ -14,6 +17,8 @@ public interface RealtimeMeetingSessionStateService {
|
|||
|
||||
RealtimeMeetingSessionStatusVO getStatus(Long meetingId);
|
||||
|
||||
Map<Long, RealtimeMeetingSessionStatusVO> getStatuses(List<Long> meetingIds);
|
||||
|
||||
RealtimeMeetingSessionStatusVO pause(Long meetingId);
|
||||
|
||||
void pauseByDisconnect(Long meetingId, String connectionId);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.imeeting.common.RedisKeys;
|
|||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
||||
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||
import com.imeeting.dto.biz.UpdateMeetingBasicCommand;
|
||||
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
|
||||
|
|
@ -25,9 +26,12 @@ import org.springframework.data.redis.core.StringRedisTemplate;
|
|||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -51,8 +55,6 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
||||
command.getAudioUrl(), tenantId, creatorId, creatorName, 0);
|
||||
meetingService.save(meeting);
|
||||
meeting.setAudioUrl(meetingDomainSupport.relocateAudioUrl(meeting.getId(), command.getAudioUrl()));
|
||||
meetingService.updateById(meeting);
|
||||
|
||||
AiTask asrTask = new AiTask();
|
||||
asrTask.setMeetingId(meeting.getId());
|
||||
|
|
@ -78,6 +80,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
aiTaskService.save(asrTask);
|
||||
|
||||
meetingDomainSupport.createSummaryTask(meeting.getId(), command.getSummaryModelId(), command.getPromptId());
|
||||
meeting.setAudioUrl(meetingDomainSupport.relocateAudioUrl(meeting.getId(), command.getAudioUrl()));
|
||||
meetingService.updateById(meeting);
|
||||
meetingDomainSupport.publishMeetingCreated(meeting.getId());
|
||||
|
||||
MeetingVO vo = new MeetingVO();
|
||||
|
|
@ -89,10 +93,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
@Transactional(rollbackFor = Exception.class)
|
||||
public MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName) {
|
||||
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
||||
null, tenantId, creatorId, creatorName, 1);
|
||||
null, tenantId, creatorId, creatorName, 0);
|
||||
meetingService.save(meeting);
|
||||
meetingDomainSupport.createSummaryTask(meeting.getId(), command.getSummaryModelId(), command.getPromptId());
|
||||
realtimeMeetingSessionStateService.initSessionIfAbsent(meeting.getId(), tenantId, creatorId);
|
||||
realtimeMeetingSessionStateService.rememberResumeConfig(meeting.getId(), buildRealtimeResumeConfig(command, tenantId));
|
||||
|
||||
MeetingVO vo = new MeetingVO();
|
||||
meetingDomainSupport.fillMeetingVO(meeting, vo, false);
|
||||
|
|
@ -270,4 +275,48 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
// Ignore progress write failures.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RealtimeMeetingResumeConfig buildRealtimeResumeConfig(CreateRealtimeMeetingCommand command, Long tenantId) {
|
||||
RealtimeMeetingResumeConfig resumeConfig = new RealtimeMeetingResumeConfig();
|
||||
resumeConfig.setAsrModelId(command.getAsrModelId());
|
||||
resumeConfig.setMode(command.getMode() == null || command.getMode().isBlank() ? "2pass" : command.getMode().trim());
|
||||
resumeConfig.setLanguage(command.getLanguage() == null || command.getLanguage().isBlank() ? "auto" : command.getLanguage().trim());
|
||||
resumeConfig.setUseSpkId(command.getUseSpkId() != null ? command.getUseSpkId() : 0);
|
||||
resumeConfig.setEnablePunctuation(command.getEnablePunctuation() != null ? command.getEnablePunctuation() : Boolean.TRUE);
|
||||
resumeConfig.setEnableItn(command.getEnableItn() != null ? command.getEnableItn() : Boolean.TRUE);
|
||||
resumeConfig.setEnableTextRefine(Boolean.TRUE.equals(command.getEnableTextRefine()));
|
||||
resumeConfig.setSaveAudio(Boolean.TRUE.equals(command.getSaveAudio()));
|
||||
resumeConfig.setHotwords(resolveRealtimeHotwords(command.getHotWords(), tenantId));
|
||||
return resumeConfig;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> resolveRealtimeHotwords(List<String> selectedWords, Long tenantId) {
|
||||
List<HotWord> tenantHotwords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getTenantId, tenantId)
|
||||
.eq(HotWord::getStatus, 1));
|
||||
if (tenantHotwords == null || tenantHotwords.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> effectiveWords = selectedWords == null
|
||||
? List.of()
|
||||
: selectedWords.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(word -> !word.isEmpty())
|
||||
.toList();
|
||||
|
||||
return tenantHotwords.stream()
|
||||
.filter(item -> effectiveWords.isEmpty() || effectiveWords.contains(item.getWord()))
|
||||
.map(this::toRealtimeHotword)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Map<String, Object> toRealtimeHotword(HotWord hotWord) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("hotword", hotWord.getWord());
|
||||
int rawWeight = hotWord.getWeight() == null ? 20 : hotWord.getWeight();
|
||||
item.put("weight", BigDecimal.valueOf(rawWeight).divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP).doubleValue());
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
|
@ -28,6 +30,7 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
|
|
@ -91,26 +94,89 @@ public class MeetingDomainSupport {
|
|||
}
|
||||
|
||||
try {
|
||||
String fileName = audioUrl.substring(audioUrl.lastIndexOf("/") + 1);
|
||||
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||
Path sourcePath = Paths.get(basePath, "audio", fileName);
|
||||
if (!Files.exists(sourcePath)) {
|
||||
AudioRelocationPlan plan = buildAudioRelocationPlan(meetingId, audioUrl);
|
||||
if (plan == null || !Files.exists(plan.sourcePath())) {
|
||||
return audioUrl;
|
||||
}
|
||||
|
||||
String ext = "";
|
||||
int dotIdx = fileName.lastIndexOf('.');
|
||||
if (dotIdx > 0) {
|
||||
ext = fileName.substring(dotIdx);
|
||||
Files.createDirectories(plan.targetPath().getParent());
|
||||
if (plan.backupPath() != null && Files.exists(plan.targetPath())) {
|
||||
Files.move(plan.targetPath(), plan.backupPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
Path targetDir = Paths.get(basePath, "meetings", String.valueOf(meetingId));
|
||||
Files.createDirectories(targetDir);
|
||||
Path targetPath = targetDir.resolve("source_audio" + ext);
|
||||
Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
return "/api/static/meetings/" + meetingId + "/source_audio" + ext;
|
||||
Files.move(plan.sourcePath(), plan.targetPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
registerAudioRelocationCompensation(meetingId, plan);
|
||||
return plan.relocatedUrl();
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed to move audio file for meeting {}", meetingId, ex);
|
||||
throw new RuntimeException("鏂囦欢澶勭悊澶辫触: " + ex.getMessage());
|
||||
throw new RuntimeException("File relocation failed: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private AudioRelocationPlan buildAudioRelocationPlan(Long meetingId, String audioUrl) {
|
||||
String fileName = audioUrl.substring(audioUrl.lastIndexOf("/") + 1);
|
||||
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||
Path sourcePath = Paths.get(basePath, "audio", fileName);
|
||||
|
||||
String ext = "";
|
||||
int dotIdx = fileName.lastIndexOf('.');
|
||||
if (dotIdx > 0) {
|
||||
ext = fileName.substring(dotIdx);
|
||||
}
|
||||
Path targetDir = Paths.get(basePath, "meetings", String.valueOf(meetingId));
|
||||
Path targetPath = targetDir.resolve("source_audio" + ext);
|
||||
Path backupPath = Files.exists(targetPath)
|
||||
? targetDir.resolve("source_audio" + ext + ".rollback-" + UUID.randomUUID() + ".bak")
|
||||
: null;
|
||||
return new AudioRelocationPlan(
|
||||
sourcePath,
|
||||
targetPath,
|
||||
backupPath,
|
||||
"/api/static/meetings/" + meetingId + "/source_audio" + ext
|
||||
);
|
||||
}
|
||||
|
||||
private void registerAudioRelocationCompensation(Long meetingId, AudioRelocationPlan plan) {
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
log.warn("Audio relocation compensation skipped because transaction synchronization is inactive, meetingId={}", meetingId);
|
||||
cleanupBackupFile(plan.backupPath(), meetingId);
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCompletion(int status) {
|
||||
if (status == STATUS_COMMITTED) {
|
||||
cleanupBackupFile(plan.backupPath(), meetingId);
|
||||
return;
|
||||
}
|
||||
compensateAudioRelocation(meetingId, plan);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void compensateAudioRelocation(Long meetingId, AudioRelocationPlan plan) {
|
||||
try {
|
||||
if (Files.exists(plan.targetPath())) {
|
||||
Files.createDirectories(plan.sourcePath().getParent());
|
||||
Files.move(plan.targetPath(), plan.sourcePath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
if (plan.backupPath() != null && Files.exists(plan.backupPath())) {
|
||||
Files.createDirectories(plan.targetPath().getParent());
|
||||
Files.move(plan.backupPath(), plan.targetPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed to compensate audio relocation for meeting {}", meetingId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupBackupFile(Path backupPath, Long meetingId) {
|
||||
if (backupPath == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(backupPath);
|
||||
} catch (Exception ex) {
|
||||
log.warn("Failed to clean audio relocation backup for meeting {}", meetingId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,4 +262,7 @@ public class MeetingDomainSupport {
|
|||
vo.setAnalysis(meetingSummaryFileService.loadSummaryAnalysis(meeting));
|
||||
}
|
||||
}
|
||||
|
||||
private record AudioRelocationPlan(Path sourcePath, Path targetPath, Path backupPath, String relocatedUrl) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import org.springframework.data.redis.core.StringRedisTemplate;
|
|||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
|
|
@ -124,6 +127,21 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
|||
return toStatusVO(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, RealtimeMeetingSessionStatusVO> getStatuses(List<Long> meetingIds) {
|
||||
Map<Long, RealtimeMeetingSessionStatusVO> statuses = new LinkedHashMap<>();
|
||||
if (meetingIds == null || meetingIds.isEmpty()) {
|
||||
return statuses;
|
||||
}
|
||||
for (Long meetingId : meetingIds) {
|
||||
if (meetingId == null) {
|
||||
continue;
|
||||
}
|
||||
statuses.put(meetingId, getStatus(meetingId));
|
||||
}
|
||||
return statuses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealtimeMeetingSessionStatusVO pause(Long meetingId) {
|
||||
RealtimeMeetingSessionState state = getOrCreateState(meetingId);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,24 @@ export interface CreateMeetingCommand {
|
|||
|
||||
export type MeetingDTO = CreateMeetingCommand;
|
||||
|
||||
export interface CreateRealtimeMeetingCommand {
|
||||
title: string;
|
||||
meetingTime: string;
|
||||
participants: string;
|
||||
tags: string;
|
||||
asrModelId: number;
|
||||
summaryModelId?: number;
|
||||
promptId: number;
|
||||
mode?: string;
|
||||
language?: string;
|
||||
useSpkId?: number;
|
||||
enablePunctuation?: boolean;
|
||||
enableItn?: boolean;
|
||||
enableTextRefine?: boolean;
|
||||
saveAudio?: boolean;
|
||||
hotWords?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateMeetingBasicCommand {
|
||||
meetingId: number;
|
||||
title?: string;
|
||||
|
|
@ -117,7 +135,7 @@ export interface RealtimeMeetingSessionStatus {
|
|||
resumeConfig?: RealtimeSocketSessionRequest;
|
||||
}
|
||||
|
||||
export const createRealtimeMeeting = (data: CreateMeetingCommand) => {
|
||||
export const createRealtimeMeeting = (data: CreateRealtimeMeetingCommand) => {
|
||||
return http.post<any, { code: string; data: MeetingVO; msg: string }>(
|
||||
"/api/biz/meeting/realtime/start",
|
||||
data
|
||||
|
|
@ -137,6 +155,13 @@ export const getRealtimeMeetingSessionStatus = (meetingId: number) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const getRealtimeMeetingSessionStatuses = (meetingIds: number[]) => {
|
||||
return http.post<any, { code: string; data: Record<number, RealtimeMeetingSessionStatus>; msg: string }>(
|
||||
"/api/biz/meeting/realtime/session-status/batch",
|
||||
meetingIds
|
||||
);
|
||||
};
|
||||
|
||||
export const pauseRealtimeMeeting = (meetingId: number) => {
|
||||
return http.post<any, { code: string; data: RealtimeMeetingSessionStatus; msg: string }>(
|
||||
`/api/biz/meeting/${meetingId}/realtime/pause`,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
import { getMeetingPage, deleteMeeting, MeetingVO, getMeetingProgress, MeetingProgress, createMeeting, uploadAudio, updateMeetingParticipants, getRealtimeMeetingSessionStatus } from '../../api/business/meeting';
|
||||
import { getMeetingPage, deleteMeeting, MeetingVO, getMeetingProgress, MeetingProgress, createMeeting, uploadAudio, updateMeetingParticipants, getRealtimeMeetingSessionStatus, getRealtimeMeetingSessionStatuses, RealtimeMeetingSessionStatus } from '../../api/business/meeting';
|
||||
import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel';
|
||||
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
|
||||
import { getHotWordPage, HotWordVO } from '../../api/business/hotword';
|
||||
|
|
@ -24,6 +24,37 @@ const { Dragger } = Upload;
|
|||
const { Option } = Select;
|
||||
const PAUSED_DISPLAY_STATUS = 5;
|
||||
|
||||
const applyRealtimeSessionStatus = (item: MeetingVO, sessionStatus?: RealtimeMeetingSessionStatus): MeetingVO => {
|
||||
if (!sessionStatus) {
|
||||
return item;
|
||||
}
|
||||
if (sessionStatus.status === 'PAUSED_EMPTY' || sessionStatus.status === 'PAUSED_RESUMABLE') {
|
||||
return {
|
||||
...item,
|
||||
displayStatus: PAUSED_DISPLAY_STATUS,
|
||||
realtimeSessionStatus: sessionStatus.status
|
||||
};
|
||||
}
|
||||
if (sessionStatus.status === 'ACTIVE') {
|
||||
return {
|
||||
...item,
|
||||
displayStatus: 1,
|
||||
realtimeSessionStatus: sessionStatus.status
|
||||
};
|
||||
}
|
||||
if (sessionStatus.status === 'IDLE' && !item.audioUrl) {
|
||||
return {
|
||||
...item,
|
||||
displayStatus: 0,
|
||||
realtimeSessionStatus: sessionStatus.status
|
||||
};
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
realtimeSessionStatus: sessionStatus.status
|
||||
};
|
||||
};
|
||||
|
||||
// --- 进度感知 Hook ---
|
||||
const useMeetingProgress = (meeting: MeetingVO, onComplete?: () => void) => {
|
||||
const [progress, setProgress] = useState<MeetingProgress | null>(null);
|
||||
|
|
@ -421,32 +452,12 @@ const Meetings: React.FC = () => {
|
|||
const res = await getMeetingPage({ current, size, title: searchTitle, viewType });
|
||||
if (res.data && res.data.data) {
|
||||
const records = res.data.data.records || [];
|
||||
const withDisplayStatus = await Promise.all(records.map(async (item) => {
|
||||
try {
|
||||
const sessionRes = await getRealtimeMeetingSessionStatus(item.id);
|
||||
const sessionStatus = sessionRes.data?.data;
|
||||
if (sessionStatus?.status === 'PAUSED_EMPTY' || sessionStatus?.status === 'PAUSED_RESUMABLE') {
|
||||
return {
|
||||
...item,
|
||||
displayStatus: PAUSED_DISPLAY_STATUS,
|
||||
realtimeSessionStatus: sessionStatus.status
|
||||
};
|
||||
}
|
||||
if (sessionStatus?.status === 'ACTIVE') {
|
||||
return {
|
||||
...item,
|
||||
displayStatus: 1,
|
||||
realtimeSessionStatus: sessionStatus.status
|
||||
};
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
realtimeSessionStatus: sessionStatus?.status
|
||||
};
|
||||
} catch {
|
||||
return item;
|
||||
}
|
||||
}));
|
||||
let statusMap: Record<number, RealtimeMeetingSessionStatus> = {};
|
||||
try {
|
||||
const sessionRes = await getRealtimeMeetingSessionStatuses(records.map((item) => item.id));
|
||||
statusMap = sessionRes.data?.data || {};
|
||||
} catch {}
|
||||
const withDisplayStatus = records.map((item) => applyRealtimeSessionStatus(item, statusMap[item.id]));
|
||||
setData(withDisplayStatus);
|
||||
setTotal(res.data.data.total);
|
||||
}
|
||||
|
|
@ -457,7 +468,12 @@ const Meetings: React.FC = () => {
|
|||
try {
|
||||
const res = await getRealtimeMeetingSessionStatus(meeting.id);
|
||||
const sessionStatus = res.data?.data;
|
||||
if (sessionStatus && (sessionStatus.status === 'PAUSED_EMPTY' || sessionStatus.status === 'PAUSED_RESUMABLE' || sessionStatus.status === 'ACTIVE')) {
|
||||
if (sessionStatus && !meeting.audioUrl && (
|
||||
sessionStatus.status === 'PAUSED_EMPTY'
|
||||
|| sessionStatus.status === 'PAUSED_RESUMABLE'
|
||||
|| sessionStatus.status === 'ACTIVE'
|
||||
|| sessionStatus.status === 'IDLE'
|
||||
)) {
|
||||
navigate(`/meeting-live-session/${meeting.id}`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -544,7 +560,7 @@ const Meetings: React.FC = () => {
|
|||
setFileList([]);
|
||||
setCreateDrawerVisible(true);
|
||||
}} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>上传文件会议</Button>
|
||||
{can("meeting:create:realtime") && <Button icon={<AudioOutlined />} onClick={() => navigate('/meeting-live-create')} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>实时识别会议</Button>}
|
||||
{can("menu:meeting") && <Button icon={<AudioOutlined />} onClick={() => navigate('/meeting-live-create')} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>实时识别会议</Button>}
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import { listUsers } from "../../api";
|
|||
import { getAiModelDefault, getAiModelPage, type AiModelVO } from "../../api/business/aimodel";
|
||||
import { getHotWordPage, type HotWordVO } from "../../api/business/hotword";
|
||||
import { getPromptPage, type PromptTemplateVO } from "../../api/business/prompt";
|
||||
import { createRealtimeMeeting, type MeetingDTO } from "../../api/business/meeting";
|
||||
import { createRealtimeMeeting, type CreateRealtimeMeetingCommand } from "../../api/business/meeting";
|
||||
import type { SysUser } from "../../types";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
|
@ -171,12 +171,18 @@ export default function RealtimeAsr() {
|
|||
weight: Number(item.weight || 2) / 10,
|
||||
}));
|
||||
|
||||
const payload: MeetingDTO = {
|
||||
const payload: CreateRealtimeMeetingCommand = {
|
||||
...values,
|
||||
meetingTime: values.meetingTime.format("YYYY-MM-DD HH:mm:ss"),
|
||||
participants: values.participants?.join(",") || "",
|
||||
tags: values.tags?.join(",") || "",
|
||||
mode: values.mode || "2pass",
|
||||
language: values.language || "auto",
|
||||
useSpkId: values.useSpkId ? 1 : 0,
|
||||
enablePunctuation: values.enablePunctuation !== false,
|
||||
enableItn: values.enableItn !== false,
|
||||
enableTextRefine: !!values.enableTextRefine,
|
||||
saveAudio: !!values.saveAudio,
|
||||
hotWords: values.hotWords,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ export default function HomePage() {
|
|||
|
||||
<section className="home-recent-section">
|
||||
<div className="home-section-header">
|
||||
<Title level={3}>快速入门 / 最近活动</Title>
|
||||
<Title level={3}>最近</Title>
|
||||
<Button type="link" onClick={() => navigate("/meetings")} className="home-view-all">
|
||||
查看全部 <ArrowRightOutlined />
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue