diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java index a5aa612..1bb4e89 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java @@ -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 create(@RequestBody CreateMeetingCommand command) { + public ApiResponse 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 createRealtime(@RequestBody CreateRealtimeMeetingCommand command) { + public ApiResponse 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> getRealtimeSessionStatuses(@RequestBody List ids) { + LoginUser loginUser = currentLoginUser(); + Map result = new LinkedHashMap<>(); + if (ids == null || ids.isEmpty()) { + return ApiResponse.ok(result); + } + + Map 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 appendRealtimeTranscripts(@PathVariable Long id, @RequestBody List items) { diff --git a/backend/src/main/java/com/imeeting/dto/biz/CreateMeetingCommand.java b/backend/src/main/java/com/imeeting/dto/biz/CreateMeetingCommand.java index 812db64..26ec77e 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/CreateMeetingCommand.java +++ b/backend/src/main/java/com/imeeting/dto/biz/CreateMeetingCommand.java @@ -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; diff --git a/backend/src/main/java/com/imeeting/dto/biz/CreateRealtimeMeetingCommand.java b/backend/src/main/java/com/imeeting/dto/biz/CreateRealtimeMeetingCommand.java index f86f647..cac6005 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/CreateRealtimeMeetingCommand.java +++ b/backend/src/main/java/com/imeeting/dto/biz/CreateRealtimeMeetingCommand.java @@ -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 hotWords; } diff --git a/backend/src/main/java/com/imeeting/service/biz/RealtimeMeetingSessionStateService.java b/backend/src/main/java/com/imeeting/service/biz/RealtimeMeetingSessionStateService.java index 51aa4d0..7f4c273 100644 --- a/backend/src/main/java/com/imeeting/service/biz/RealtimeMeetingSessionStateService.java +++ b/backend/src/main/java/com/imeeting/service/biz/RealtimeMeetingSessionStateService.java @@ -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 getStatuses(List meetingIds); + RealtimeMeetingSessionStatusVO pause(Long meetingId); void pauseByDisconnect(Long meetingId, String connectionId); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java index b9fbfa1..7cb8b3e 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java @@ -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. } } -} \ No newline at end of file + + 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> resolveRealtimeHotwords(List selectedWords, Long tenantId) { + List tenantHotwords = hotWordService.list(new LambdaQueryWrapper() + .eq(HotWord::getTenantId, tenantId) + .eq(HotWord::getStatus, 1)); + if (tenantHotwords == null || tenantHotwords.isEmpty()) { + return List.of(); + } + + List 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 toRealtimeHotword(HotWord hotWord) { + Map 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; + } +} diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java index b97b86b..8a84f49 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java @@ -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) { + } } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/RealtimeMeetingSessionStateServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/RealtimeMeetingSessionStateServiceImpl.java index bd9dc4e..40b769c 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/RealtimeMeetingSessionStateServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/RealtimeMeetingSessionStateServiceImpl.java @@ -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 getStatuses(List meetingIds) { + Map 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); diff --git a/frontend/src/api/business/meeting.ts b/frontend/src/api/business/meeting.ts index 20d2a9d..85c63f2 100644 --- a/frontend/src/api/business/meeting.ts +++ b/frontend/src/api/business/meeting.ts @@ -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( "/api/biz/meeting/realtime/start", data @@ -137,6 +155,13 @@ export const getRealtimeMeetingSessionStatus = (meetingId: number) => { ); }; +export const getRealtimeMeetingSessionStatuses = (meetingIds: number[]) => { + return http.post; msg: string }>( + "/api/biz/meeting/realtime/session-status/batch", + meetingIds + ); +}; + export const pauseRealtimeMeeting = (meetingId: number) => { return http.post( `/api/biz/meeting/${meetingId}/realtime/pause`, diff --git a/frontend/src/pages/business/Meetings.tsx b/frontend/src/pages/business/Meetings.tsx index 2328869..af1278d 100644 --- a/frontend/src/pages/business/Meetings.tsx +++ b/frontend/src/pages/business/Meetings.tsx @@ -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(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 = {}; + 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 }}>上传文件会议 - {can("meeting:create:realtime") && } + {can("menu:meeting") && } diff --git a/frontend/src/pages/business/RealtimeAsr.tsx b/frontend/src/pages/business/RealtimeAsr.tsx index b3ec9dc..06a2c31 100644 --- a/frontend/src/pages/business/RealtimeAsr.tsx +++ b/frontend/src/pages/business/RealtimeAsr.tsx @@ -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, }; diff --git a/frontend/src/pages/home/index.tsx b/frontend/src/pages/home/index.tsx index 9eef7b2..5121672 100644 --- a/frontend/src/pages/home/index.tsx +++ b/frontend/src/pages/home/index.tsx @@ -151,7 +151,7 @@ export default function HomePage() {
- 快速入门 / 最近活动 + 最近