feat: 添加热词组支持并优化相关逻辑
- 在 `RealtimeMeetingResumeConfig` 和 `MeetingVO` 中添加 `hotWordGroupId` 和 `hotWordGroupName` 字段 - 更新 `MeetingCommandServiceImpl` 以设置 `hotWordGroupId` 到 `resumeConfig` - 在 `MeetingDomainSupport` 中添加方法以解析和填充有效的热词组信息 - 更新前端 `meeting.ts` 和 `MeetingDetail.tsx` 以支持热词组显示dev_na
parent
202ce057fd
commit
69e6eff9d9
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ public class RealtimeMeetingResumeConfig {
|
|||
private Boolean saveAudio;
|
||||
@Schema(description = "热词列表")
|
||||
private List<Map<String, Object>> hotwords;
|
||||
@Schema(description = "最终生效热词组ID")
|
||||
private Long hotWordGroupId;
|
||||
@Schema(description = "腾讯说话人上下文 ID")
|
||||
private String speakerContextId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AiTask>()
|
||||
.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<AiTask>()
|
||||
.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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
</span>
|
||||
<span>{meeting.participants || '未指定'}</span>
|
||||
</div>
|
||||
<div className="meeting-detail-config-row">
|
||||
<span className="meeting-detail-config-item">
|
||||
<Text type="secondary">总结模型</Text>
|
||||
<strong>{summaryModelDisplayName}</strong>
|
||||
</span>
|
||||
<span className="meeting-detail-config-item">
|
||||
<Text type="secondary">会议模板</Text>
|
||||
<strong>{promptDisplayName}</strong>
|
||||
</span>
|
||||
<span className="meeting-detail-config-item">
|
||||
<Text type="secondary">热词组</Text>
|
||||
<strong>{hotWordGroupDisplayName}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue