diff --git a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java index e6426b9..603e04c 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java @@ -75,6 +75,12 @@ public class MeetingVO { @Schema(description = "总结模板ID") private Long promptId; + @Schema(description = "最终生效热词组ID") + private Long hotWordGroupId; + + @Schema(description = "最终生效热词组名称") + private String hotWordGroupName; + @Schema(description = "是否启用 AI 目录") private Boolean aiCatalogEnabled; diff --git a/backend/src/main/java/com/imeeting/dto/biz/RealtimeMeetingResumeConfig.java b/backend/src/main/java/com/imeeting/dto/biz/RealtimeMeetingResumeConfig.java index ab46c51..eafa669 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/RealtimeMeetingResumeConfig.java +++ b/backend/src/main/java/com/imeeting/dto/biz/RealtimeMeetingResumeConfig.java @@ -27,6 +27,8 @@ public class RealtimeMeetingResumeConfig { private Boolean saveAudio; @Schema(description = "热词列表") private List> hotwords; + @Schema(description = "最终生效热词组ID") + private Long hotWordGroupId; @Schema(description = "腾讯说话人上下文 ID") private String speakerContextId; } 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 17a61aa..789dcf4 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 @@ -1512,6 +1512,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { resumeConfig.setEnableTextRefine(runtimeProfile.getResolvedEnableTextRefine()); resumeConfig.setSaveAudio(runtimeProfile.getResolvedSaveAudio()); resumeConfig.setHotwords(resolveRealtimeHotwords(runtimeProfile.getResolvedHotWords(), runtimeProfile.getResolvedHotWordGroupId())); + resumeConfig.setHotWordGroupId(runtimeProfile.getResolvedHotWordGroupId()); return resumeConfig; } 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 0789854..0d06c1f 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 @@ -5,12 +5,15 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.imeeting.common.MeetingConstants; import com.imeeting.common.SysParamKeys; import com.imeeting.entity.biz.AiTask; +import com.imeeting.entity.biz.HotWordGroup; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.event.MeetingCreatedEvent; import com.imeeting.mapper.biz.MeetingTranscriptMapper; import com.imeeting.service.biz.AiTaskService; +import com.imeeting.service.biz.HotWordGroupService; import com.imeeting.service.biz.MeetingPointsService; +import com.imeeting.service.biz.RealtimeMeetingSessionStateService; import com.imeeting.service.biz.MeetingSummaryFileService; import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService; import com.unisbase.entity.SysUser; @@ -57,6 +60,8 @@ public class MeetingDomainSupport { private final ApplicationEventPublisher eventPublisher; private final MeetingSummaryFileService meetingSummaryFileService; private final MeetingPlaybackAudioResolver meetingPlaybackAudioResolver; + private final HotWordGroupService hotWordGroupService; + private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService; private final SysParamService sysParamService; @Value("${unisbase.app.upload-path}") @@ -441,9 +446,42 @@ public class MeetingDomainSupport { vo.setSummaryContent(meetingSummaryFileService.loadSummaryContent(meeting)); vo.setAnalysis(meetingSummaryFileService.loadSummaryAnalysis(meeting)); vo.setLastUserPrompt(resolveLastSummaryUserPrompt(meeting)); + fillEffectiveHotWordGroup(meeting, vo); } } + private void fillEffectiveHotWordGroup(Meeting meeting, com.imeeting.dto.biz.MeetingVO vo) { + Long hotWordGroupId = resolveEffectiveHotWordGroupId(meeting); + vo.setHotWordGroupId(hotWordGroupId); + if (hotWordGroupId == null) { + return; + } + HotWordGroup hotWordGroup = hotWordGroupService.getById(hotWordGroupId); + if (hotWordGroup != null) { + vo.setHotWordGroupName(hotWordGroup.getGroupName()); + } + } + + private Long resolveEffectiveHotWordGroupId(Meeting meeting) { + AiTask latestAsrTask = resolveLatestAsrTask(meeting); + Long asrHotWordGroupId = longValue( + latestAsrTask == null || latestAsrTask.getTaskConfig() == null + ? null + : latestAsrTask.getTaskConfig().get("hotWordGroupId") + ); + if (asrHotWordGroupId != null) { + return asrHotWordGroupId; + } + if (!MeetingConstants.TYPE_REALTIME.equalsIgnoreCase(meeting.getMeetingType())) { + return null; + } + var sessionStatus = realtimeMeetingSessionStateService.getStatus(meeting.getId()); + if (sessionStatus == null || sessionStatus.getResumeConfig() == null) { + return null; + } + return sessionStatus.getResumeConfig().getHotWordGroupId(); + } + private String resolveLastSummaryUserPrompt(Meeting meeting) { AiTask latestSummaryTask = resolveLatestSummaryTask(meeting); if (latestSummaryTask == null || latestSummaryTask.getTaskConfig() == null) { @@ -477,6 +515,17 @@ public class MeetingDomainSupport { return aiTaskService.getOne(new LambdaQueryWrapper() .eq(AiTask::getMeetingId, meeting.getId()) .eq(AiTask::getTaskType, "SUMMARY") + .orderByDesc(AiTask::getId) + .last("LIMIT 1")); + } + + private AiTask resolveLatestAsrTask(Meeting meeting) { + if (meeting == null || meeting.getId() == null) { + return null; + } + return aiTaskService.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meeting.getId()) + .eq(AiTask::getTaskType, "ASR") .orderByDesc(AiTask::getId) .last("LIMIT 1")); } @@ -517,6 +566,17 @@ public class MeetingDomainSupport { return normalized.isEmpty() ? null : normalized; } + private Long longValue(Object value) { + if (value == null) { + return null; + } + try { + return Long.parseLong(String.valueOf(value).trim()); + } catch (Exception ignored) { + return null; + } + } + private String normalizeSummaryDetailLevel(String summaryDetailLevel) { if (summaryDetailLevel == null || summaryDetailLevel.isBlank()) { return MeetingConstants.SUMMARY_DETAIL_STANDARD; diff --git a/frontend/src/api/business/meeting.ts b/frontend/src/api/business/meeting.ts index df798ec..aa3e6d9 100644 --- a/frontend/src/api/business/meeting.ts +++ b/frontend/src/api/business/meeting.ts @@ -36,6 +36,8 @@ export interface MeetingVO { summaryDetailLevel?: SummaryDetailLevel; summaryModelId: number; promptId?: number; + hotWordGroupId?: number; + hotWordGroupName?: string; aiCatalogEnabled?: boolean; audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED"; audioSaveMessage?: string; diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index f2cb36a..d156327 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -1389,6 +1389,35 @@ const MeetingDetail: React.FC = () => { const meetingId = meeting?.id ?? (id ? Number(id) : NaN); return buildMeetingPreviewUrl(meetingShareBaseUrl, meetingId, previewAccessPassword); }, [meetingShareBaseUrl, meeting?.id, id, previewAccessPassword]); + const summaryModelDisplayName = useMemo(() => { + const matchedModel = llmModels.find((item) => item.id === meeting?.summaryModelId); + if (matchedModel?.modelName) { + return matchedModel.modelName; + } + if (meeting?.summaryModelId) { + return `模型 #${meeting.summaryModelId}`; + } + return '未配置'; + }, [llmModels, meeting?.summaryModelId]); + const promptDisplayName = useMemo(() => { + const matchedPrompt = prompts.find((item) => item.id === meeting?.promptId); + if (matchedPrompt?.templateName) { + return matchedPrompt.templateName; + } + if (meeting?.promptId) { + return `模板 #${meeting.promptId}`; + } + return '未配置'; + }, [meeting?.promptId, prompts]); + const hotWordGroupDisplayName = useMemo(() => { + if (meeting?.hotWordGroupName?.trim()) { + return meeting.hotWordGroupName.trim(); + } + if (meeting?.hotWordGroupId) { + return `热词组 #${meeting.hotWordGroupId}`; + } + return '未使用'; + }, [meeting?.hotWordGroupId, meeting?.hotWordGroupName]); const isOwner = useMemo(() => { if (!meeting) return false; @@ -2392,6 +2421,20 @@ const MeetingDetail: React.FC = () => { {meeting.participants || '未指定'} +
+ + 总结模型 + {summaryModelDisplayName} + + + 会议模板 + {promptDisplayName} + + + 热词组 + {hotWordGroupDisplayName} + +
)} @@ -3053,6 +3096,29 @@ const MeetingDetail: React.FC = () => { color: #6e7695; font-size: 13px; } + .meeting-detail-config-row { + display: flex; + flex-wrap: wrap; + gap: 10px 12px; + margin-top: 2px; + } + .meeting-detail-config-item { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 34px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(244, 246, 255, 0.92); + border: 1px solid rgba(227, 232, 247, 0.95); + color: #58627f; + font-size: 13px; + line-height: 1; + } + .meeting-detail-config-item strong { + color: #313a5a; + font-weight: 700; + } .meeting-detail-workspace { flex: 1; min-height: 0;