feat: 添加总结详细程度选项和优化用户认证逻辑
- 在 `MeetingCommandService` 中添加 `summaryDetailLevel` 参数 - 更新 `MeetingSummaryPromptAssembler` 以支持不同详细程度的总结 - 在前端 `useAuth` 钩子中添加 `profileVersion` 状态,同步用户资料更新 - 优化密码重置流程,保持登录状态并更新用户资料 - 在 `Meeting` 实体中添加 `summaryDetailLevel` 字段 - 更新相关服务和控制器以处理新的总结详细程度选项dev_na
parent
51190f330c
commit
f7480df565
|
|
@ -7,6 +7,9 @@ public final class MeetingConstants {
|
||||||
|
|
||||||
public static final String SOURCE_WEB = "WEB";
|
public static final String SOURCE_WEB = "WEB";
|
||||||
public static final String SOURCE_ANDROID = "ANDROID";
|
public static final String SOURCE_ANDROID = "ANDROID";
|
||||||
|
public static final String SUMMARY_DETAIL_DETAILED = "DETAILED";
|
||||||
|
public static final String SUMMARY_DETAIL_STANDARD = "STANDARD";
|
||||||
|
public static final String SUMMARY_DETAIL_BRIEF = "BRIEF";
|
||||||
|
|
||||||
private MeetingConstants() {
|
private MeetingConstants() {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,6 @@ public final class SysParamKeys {
|
||||||
public static final String MEETING_TRANSCRIPT_CLEANUP_FILLER_WORDS = "meeting.transcript.cleanup.filler_words";
|
public static final String MEETING_TRANSCRIPT_CLEANUP_FILLER_WORDS = "meeting.transcript.cleanup.filler_words";
|
||||||
public static final String MEETING_TRANSCRIPT_CLEANUP_REPLACEMENTS = "meeting.transcript.cleanup.replacements";
|
public static final String MEETING_TRANSCRIPT_CLEANUP_REPLACEMENTS = "meeting.transcript.cleanup.replacements";
|
||||||
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_REALTIME_ENABLED = "meeting.create.realtime_enabled";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,7 @@ public class AndroidMeetingRealtimeController {
|
||||||
createCommand.setAsrModelId(runtimeProfile.getResolvedAsrModelId());
|
createCommand.setAsrModelId(runtimeProfile.getResolvedAsrModelId());
|
||||||
createCommand.setSummaryModelId(runtimeProfile.getResolvedSummaryModelId());
|
createCommand.setSummaryModelId(runtimeProfile.getResolvedSummaryModelId());
|
||||||
createCommand.setPromptId(runtimeProfile.getResolvedPromptId());
|
createCommand.setPromptId(runtimeProfile.getResolvedPromptId());
|
||||||
|
createCommand.setSummaryDetailLevel(command == null ? null : command.getSummaryDetailLevel());
|
||||||
createCommand.setMode(runtimeProfile.getResolvedMode());
|
createCommand.setMode(runtimeProfile.getResolvedMode());
|
||||||
createCommand.setLanguage(runtimeProfile.getResolvedLanguage());
|
createCommand.setLanguage(runtimeProfile.getResolvedLanguage());
|
||||||
createCommand.setUseSpkId(runtimeProfile.getResolvedUseSpkId());
|
createCommand.setUseSpkId(runtimeProfile.getResolvedUseSpkId());
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ package com.imeeting.controller.biz;
|
||||||
|
|
||||||
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.dto.biz.CreateMeetingCommand;
|
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||||
|
import com.imeeting.dto.biz.MeetingCreateConfigVO;
|
||||||
import com.imeeting.dto.biz.MeetingResummaryDTO;
|
import com.imeeting.dto.biz.MeetingResummaryDTO;
|
||||||
import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO;
|
import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO;
|
||||||
import com.imeeting.dto.biz.MeetingSpeakerUpdateDTO;
|
import com.imeeting.dto.biz.MeetingSpeakerUpdateDTO;
|
||||||
|
|
@ -34,6 +36,7 @@ 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;
|
||||||
import com.unisbase.security.LoginUser;
|
import com.unisbase.security.LoginUser;
|
||||||
|
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;
|
||||||
|
|
@ -77,6 +80,7 @@ public class MeetingController {
|
||||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||||
private final MeetingAudioUploadSupport meetingAudioUploadSupport;
|
private final MeetingAudioUploadSupport meetingAudioUploadSupport;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final SysParamService sysParamService;
|
||||||
|
|
||||||
public MeetingController(MeetingQueryService meetingQueryService,
|
public MeetingController(MeetingQueryService meetingQueryService,
|
||||||
MeetingCommandService meetingCommandService,
|
MeetingCommandService meetingCommandService,
|
||||||
|
|
@ -87,7 +91,8 @@ public class MeetingController {
|
||||||
RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService,
|
RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService,
|
||||||
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
||||||
MeetingAudioUploadSupport meetingAudioUploadSupport,
|
MeetingAudioUploadSupport meetingAudioUploadSupport,
|
||||||
StringRedisTemplate redisTemplate) {
|
StringRedisTemplate redisTemplate,
|
||||||
|
SysParamService sysParamService) {
|
||||||
this.meetingQueryService = meetingQueryService;
|
this.meetingQueryService = meetingQueryService;
|
||||||
this.meetingCommandService = meetingCommandService;
|
this.meetingCommandService = meetingCommandService;
|
||||||
this.meetingAccessService = meetingAccessService;
|
this.meetingAccessService = meetingAccessService;
|
||||||
|
|
@ -98,6 +103,7 @@ public class MeetingController {
|
||||||
this.realtimeMeetingSessionStateService = realtimeMeetingSessionStateService;
|
this.realtimeMeetingSessionStateService = realtimeMeetingSessionStateService;
|
||||||
this.meetingAudioUploadSupport = meetingAudioUploadSupport;
|
this.meetingAudioUploadSupport = meetingAudioUploadSupport;
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
|
this.sysParamService = sysParamService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "查询会议处理进度")
|
@Operation(summary = "查询会议处理进度")
|
||||||
|
|
@ -139,6 +145,17 @@ public class MeetingController {
|
||||||
return ApiResponse.ok(meetingAudioUploadSupport.storeUploadedAudio(file));
|
return ApiResponse.ok(meetingAudioUploadSupport.storeUploadedAudio(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取会议创建配置")
|
||||||
|
@GetMapping("/create-config")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<MeetingCreateConfigVO> getCreateConfig() {
|
||||||
|
MeetingCreateConfigVO vo = new MeetingCreateConfigVO();
|
||||||
|
vo.setOfflineEnabled(resolveBooleanParam(SysParamKeys.MEETING_CREATE_OFFLINE_ENABLED, true));
|
||||||
|
vo.setRealtimeEnabled(resolveBooleanParam(SysParamKeys.MEETING_CREATE_REALTIME_ENABLED, true));
|
||||||
|
vo.setOfflineAudioMaxSizeMb(resolveLongParam(SysParamKeys.MEETING_OFFLINE_AUDIO_MAX_SIZE_MB, 1024L));
|
||||||
|
return ApiResponse.ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "创建离线会议")
|
@Operation(summary = "创建离线会议")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
|
@ -414,7 +431,14 @@ public class MeetingController {
|
||||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||||
dto.setMeetingId(id);
|
dto.setMeetingId(id);
|
||||||
assertPromptAvailable(dto.getPromptId(), loginUser);
|
assertPromptAvailable(dto.getPromptId(), loginUser);
|
||||||
meetingCommandService.reSummary(dto.getMeetingId(), dto.getSummaryModelId(), dto.getChapterModelId(), dto.getPromptId(), dto.getUserPrompt());
|
meetingCommandService.reSummary(
|
||||||
|
dto.getMeetingId(),
|
||||||
|
dto.getSummaryModelId(),
|
||||||
|
dto.getChapterModelId(),
|
||||||
|
dto.getPromptId(),
|
||||||
|
dto.getUserPrompt(),
|
||||||
|
dto.getSummaryDetailLevel()
|
||||||
|
);
|
||||||
return ApiResponse.ok(true);
|
return ApiResponse.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -501,4 +525,32 @@ public class MeetingController {
|
||||||
private String resolveCreatorName(LoginUser loginUser) {
|
private String resolveCreatorName(LoginUser loginUser) {
|
||||||
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean resolveBooleanParam(String key, boolean defaultValue) {
|
||||||
|
String rawValue = sysParamService.getCachedParamValue(key, String.valueOf(defaultValue));
|
||||||
|
if (rawValue == null || rawValue.isBlank()) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
String normalized = rawValue.trim().toLowerCase();
|
||||||
|
if ("1".equals(normalized) || "true".equals(normalized) || "yes".equals(normalized) || "on".equals(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ("0".equals(normalized) || "false".equals(normalized) || "no".equals(normalized) || "off".equals(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long resolveLongParam(String key, long defaultValue) {
|
||||||
|
String rawValue = sysParamService.getCachedParamValue(key, String.valueOf(defaultValue));
|
||||||
|
if (rawValue == null || rawValue.isBlank()) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
long parsed = Long.parseLong(rawValue.trim());
|
||||||
|
return parsed > 0 ? parsed : defaultValue;
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.imeeting.dto.android;
|
package com.imeeting.dto.android;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
@ -20,6 +21,8 @@ public class AndroidCreateRealtimeMeetingCommand {
|
||||||
private Long asrModelId;
|
private Long asrModelId;
|
||||||
private Long summaryModelId;
|
private Long summaryModelId;
|
||||||
private Long promptId;
|
private Long promptId;
|
||||||
|
@Schema(description = "总结详细程度:DETAILED=详细,STANDARD=标准,BRIEF=简洁")
|
||||||
|
private String summaryDetailLevel;
|
||||||
private String mode;
|
private String mode;
|
||||||
private String language;
|
private String language;
|
||||||
private Integer useSpkId;
|
private Integer useSpkId;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.imeeting.dto.biz;
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.imeeting.common.MeetingConstants;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
@ -9,6 +11,7 @@ import lombok.Data;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "创建离线会议请求")
|
||||||
@Data
|
@Data
|
||||||
public class CreateMeetingCommand {
|
public class CreateMeetingCommand {
|
||||||
@NotBlank(message = "标题不能为空")
|
@NotBlank(message = "标题不能为空")
|
||||||
|
|
@ -42,6 +45,16 @@ public class CreateMeetingCommand {
|
||||||
@Size(max = 2000, message = "userPrompt length must be <= 2000")
|
@Size(max = 2000, message = "userPrompt length must be <= 2000")
|
||||||
private String userPrompt;
|
private String userPrompt;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "总结详细程度:DETAILED=详细,STANDARD=标准,BRIEF=简洁",
|
||||||
|
allowableValues = {
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_DETAILED,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_BRIEF
|
||||||
|
}
|
||||||
|
)
|
||||||
|
private String summaryDetailLevel;
|
||||||
|
|
||||||
private Integer useSpkId;
|
private Integer useSpkId;
|
||||||
private Boolean enableTextRefine;
|
private Boolean enableTextRefine;
|
||||||
private List<String> hotWords;
|
private List<String> hotWords;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.imeeting.dto.biz;
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.imeeting.common.MeetingConstants;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
@ -9,6 +11,7 @@ import lombok.Data;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "创建实时会议请求")
|
||||||
@Data
|
@Data
|
||||||
public class CreateRealtimeMeetingCommand {
|
public class CreateRealtimeMeetingCommand {
|
||||||
@NotBlank(message = "标题不能为空")
|
@NotBlank(message = "标题不能为空")
|
||||||
|
|
@ -39,6 +42,16 @@ public class CreateRealtimeMeetingCommand {
|
||||||
@Size(max = 2000, message = "userPrompt length must be <= 2000")
|
@Size(max = 2000, message = "userPrompt length must be <= 2000")
|
||||||
private String userPrompt;
|
private String userPrompt;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "总结详细程度:DETAILED=详细,STANDARD=标准,BRIEF=简洁",
|
||||||
|
allowableValues = {
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_DETAILED,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_BRIEF
|
||||||
|
}
|
||||||
|
)
|
||||||
|
private String summaryDetailLevel;
|
||||||
|
|
||||||
private String mode;
|
private String mode;
|
||||||
private String language;
|
private String language;
|
||||||
private Integer useSpkId;
|
private Integer useSpkId;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package com.imeeting.dto.biz;
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
import lombok.Data;
|
import com.imeeting.common.MeetingConstants;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "重新生成会议摘要请求")
|
||||||
@Data
|
@Data
|
||||||
public class MeetingResummaryDTO {
|
public class MeetingResummaryDTO {
|
||||||
private Long meetingId;
|
private Long meetingId;
|
||||||
|
|
@ -12,4 +15,14 @@ public class MeetingResummaryDTO {
|
||||||
|
|
||||||
@Size(max = 2000, message = "userPrompt length must be <= 2000")
|
@Size(max = 2000, message = "userPrompt length must be <= 2000")
|
||||||
private String userPrompt;
|
private String userPrompt;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "总结详细程度:DETAILED=详细,STANDARD=标准,BRIEF=简洁",
|
||||||
|
allowableValues = {
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_DETAILED,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_BRIEF
|
||||||
|
}
|
||||||
|
)
|
||||||
|
private String summaryDetailLevel;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ public class MeetingVO {
|
||||||
private String meetingType;
|
private String meetingType;
|
||||||
@Schema(description = "会议来源")
|
@Schema(description = "会议来源")
|
||||||
private String meetingSource;
|
private String meetingSource;
|
||||||
|
@Schema(description = "总结详细程度")
|
||||||
|
private String summaryDetailLevel;
|
||||||
@Schema(description = "音频保存状态")
|
@Schema(description = "音频保存状态")
|
||||||
private String audioSaveStatus;
|
private String audioSaveStatus;
|
||||||
@Schema(description = "音频保存说明")
|
@Schema(description = "音频保存说明")
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ public class Meeting extends BaseEntity {
|
||||||
|
|
||||||
@Schema(description = "会议来源")
|
@Schema(description = "会议来源")
|
||||||
private String meetingSource;
|
private String meetingSource;
|
||||||
|
@Schema(description = "总结详细程度")
|
||||||
|
private String summaryDetailLevel;
|
||||||
|
|
||||||
@Schema(description = "音频保存状态")
|
@Schema(description = "音频保存状态")
|
||||||
private String audioSaveStatus;
|
private String audioSaveStatus;
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
||||||
resolveCreatorName(loginUser),
|
resolveCreatorName(loginUser),
|
||||||
loginUser.getUserId(),
|
loginUser.getUserId(),
|
||||||
resolveCreatorName(loginUser),
|
resolveCreatorName(loginUser),
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
meetingService.save(meeting);
|
meetingService.save(meeting);
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ public interface MeetingCommandService {
|
||||||
|
|
||||||
void updateSummaryContent(Long meetingId, String summaryContent);
|
void updateSummaryContent(Long meetingId, String summaryContent);
|
||||||
|
|
||||||
void reSummary(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt);
|
void reSummary(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt, String summaryDetailLevel);
|
||||||
|
|
||||||
void retryTranscription(Long meetingId);
|
void retryTranscription(Long meetingId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -940,7 +940,7 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
entity.setApiPath(dto.getApiPath());
|
entity.setApiPath(dto.getApiPath());
|
||||||
entity.setApiKey(dto.getApiKey());
|
entity.setApiKey(dto.getApiKey());
|
||||||
entity.setModelCode(dto.getModelCode());
|
entity.setModelCode(dto.getModelCode());
|
||||||
entity.setTemperature(dto.getTemperature() == null ? BigDecimal.valueOf(0.7) : dto.getTemperature());
|
entity.setTemperature(dto.getTemperature() == null ? BigDecimal.valueOf(0.2) : dto.getTemperature());
|
||||||
entity.setTopP(dto.getTopP() == null ? BigDecimal.valueOf(0.9) : dto.getTopP());
|
entity.setTopP(dto.getTopP() == null ? BigDecimal.valueOf(0.9) : dto.getTopP());
|
||||||
entity.setIsDefault(dto.getIsDefault());
|
entity.setIsDefault(dto.getIsDefault());
|
||||||
entity.setStatus(dto.getStatus());
|
entity.setStatus(dto.getStatus());
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
|
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
|
||||||
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
||||||
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
|
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
|
||||||
|
String summaryDetailLevel = resolveSummaryDetailLevel(command.getSummaryDetailLevel());
|
||||||
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
||||||
command.getAudioUrl(), MeetingConstants.TYPE_OFFLINE, meetingSource, tenantId, creatorId, creatorName, hostUserId, hostName, 0);
|
command.getAudioUrl(), MeetingConstants.TYPE_OFFLINE, meetingSource, tenantId, creatorId, creatorName, hostUserId, hostName, summaryDetailLevel, 0);
|
||||||
meetingService.save(meeting);
|
meetingService.save(meeting);
|
||||||
|
|
||||||
AiTask asrTask = new AiTask();
|
AiTask asrTask = new AiTask();
|
||||||
|
|
@ -118,14 +119,16 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
runtimeProfile.getResolvedSummaryModelId(),
|
runtimeProfile.getResolvedSummaryModelId(),
|
||||||
chapterModelId,
|
chapterModelId,
|
||||||
runtimeProfile.getResolvedPromptId(),
|
runtimeProfile.getResolvedPromptId(),
|
||||||
command.getUserPrompt()
|
command.getUserPrompt(),
|
||||||
|
summaryDetailLevel
|
||||||
);
|
);
|
||||||
if (Objects.equals(chapterModelId, runtimeProfile.getResolvedSummaryModelId())) {
|
if (Objects.equals(chapterModelId, runtimeProfile.getResolvedSummaryModelId())) {
|
||||||
meetingDomainSupport.createSummaryTask(
|
meetingDomainSupport.createSummaryTask(
|
||||||
meeting.getId(),
|
meeting.getId(),
|
||||||
runtimeProfile.getResolvedSummaryModelId(),
|
runtimeProfile.getResolvedSummaryModelId(),
|
||||||
runtimeProfile.getResolvedPromptId(),
|
runtimeProfile.getResolvedPromptId(),
|
||||||
command.getUserPrompt()
|
command.getUserPrompt(),
|
||||||
|
summaryDetailLevel
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
meetingDomainSupport.createSummaryTask(
|
meetingDomainSupport.createSummaryTask(
|
||||||
|
|
@ -133,7 +136,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
runtimeProfile.getResolvedSummaryModelId(),
|
runtimeProfile.getResolvedSummaryModelId(),
|
||||||
chapterModelId,
|
chapterModelId,
|
||||||
runtimeProfile.getResolvedPromptId(),
|
runtimeProfile.getResolvedPromptId(),
|
||||||
command.getUserPrompt()
|
command.getUserPrompt(),
|
||||||
|
summaryDetailLevel
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
meeting.setAudioUrl(meetingDomainSupport.relocateAudioUrl(meeting.getId(), command.getAudioUrl()));
|
meeting.setAudioUrl(meetingDomainSupport.relocateAudioUrl(meeting.getId(), command.getAudioUrl()));
|
||||||
|
|
@ -152,8 +156,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
|
RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId);
|
||||||
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
||||||
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
|
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
|
||||||
|
String summaryDetailLevel = resolveSummaryDetailLevel(command.getSummaryDetailLevel());
|
||||||
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(),
|
||||||
null, MeetingConstants.TYPE_REALTIME, meetingSource, tenantId, creatorId, creatorName, hostUserId, hostName, 0);
|
null, MeetingConstants.TYPE_REALTIME, meetingSource, tenantId, creatorId, creatorName, hostUserId, hostName, summaryDetailLevel, 0);
|
||||||
meetingService.save(meeting);
|
meetingService.save(meeting);
|
||||||
Long chapterModelId = command.getChapterModelId() != null ? command.getChapterModelId() : runtimeProfile.getResolvedSummaryModelId();
|
Long chapterModelId = command.getChapterModelId() != null ? command.getChapterModelId() : runtimeProfile.getResolvedSummaryModelId();
|
||||||
meetingDomainSupport.createChapterTask(
|
meetingDomainSupport.createChapterTask(
|
||||||
|
|
@ -161,14 +166,16 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
runtimeProfile.getResolvedSummaryModelId(),
|
runtimeProfile.getResolvedSummaryModelId(),
|
||||||
chapterModelId,
|
chapterModelId,
|
||||||
runtimeProfile.getResolvedPromptId(),
|
runtimeProfile.getResolvedPromptId(),
|
||||||
command.getUserPrompt()
|
command.getUserPrompt(),
|
||||||
|
summaryDetailLevel
|
||||||
);
|
);
|
||||||
if (Objects.equals(chapterModelId, runtimeProfile.getResolvedSummaryModelId())) {
|
if (Objects.equals(chapterModelId, runtimeProfile.getResolvedSummaryModelId())) {
|
||||||
meetingDomainSupport.createSummaryTask(
|
meetingDomainSupport.createSummaryTask(
|
||||||
meeting.getId(),
|
meeting.getId(),
|
||||||
runtimeProfile.getResolvedSummaryModelId(),
|
runtimeProfile.getResolvedSummaryModelId(),
|
||||||
runtimeProfile.getResolvedPromptId(),
|
runtimeProfile.getResolvedPromptId(),
|
||||||
command.getUserPrompt()
|
command.getUserPrompt(),
|
||||||
|
summaryDetailLevel
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
meetingDomainSupport.createSummaryTask(
|
meetingDomainSupport.createSummaryTask(
|
||||||
|
|
@ -176,7 +183,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
runtimeProfile.getResolvedSummaryModelId(),
|
runtimeProfile.getResolvedSummaryModelId(),
|
||||||
chapterModelId,
|
chapterModelId,
|
||||||
runtimeProfile.getResolvedPromptId(),
|
runtimeProfile.getResolvedPromptId(),
|
||||||
command.getUserPrompt()
|
command.getUserPrompt(),
|
||||||
|
summaryDetailLevel
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
realtimeMeetingSessionStateService.initSessionIfAbsent(meeting.getId(), tenantId, creatorId);
|
realtimeMeetingSessionStateService.initSessionIfAbsent(meeting.getId(), tenantId, creatorId);
|
||||||
|
|
@ -582,7 +590,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
summaryModelId,
|
summaryModelId,
|
||||||
chapterModelId,
|
chapterModelId,
|
||||||
promptId,
|
promptId,
|
||||||
command.getUserPrompt()
|
command.getUserPrompt(),
|
||||||
|
meeting.getSummaryDetailLevel()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -627,14 +636,16 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
meeting.getId(),
|
meeting.getId(),
|
||||||
summaryModelId,
|
summaryModelId,
|
||||||
promptId,
|
promptId,
|
||||||
userPrompt
|
userPrompt,
|
||||||
|
meeting.getSummaryDetailLevel()
|
||||||
)
|
)
|
||||||
: meetingDomainSupport.createSummaryTask(
|
: meetingDomainSupport.createSummaryTask(
|
||||||
meeting.getId(),
|
meeting.getId(),
|
||||||
summaryModelId,
|
summaryModelId,
|
||||||
chapterModelId,
|
chapterModelId,
|
||||||
promptId,
|
promptId,
|
||||||
userPrompt
|
userPrompt,
|
||||||
|
meeting.getSummaryDetailLevel()
|
||||||
);
|
);
|
||||||
meeting.setLatestSummaryTaskId(createdSummaryTask.getId());
|
meeting.setLatestSummaryTaskId(createdSummaryTask.getId());
|
||||||
meeting.setStatus(2);
|
meeting.setStatus(2);
|
||||||
|
|
@ -803,31 +814,35 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void reSummary(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) {
|
public void reSummary(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt, String summaryDetailLevel) {
|
||||||
Meeting meeting = meetingService.getById(meetingId);
|
Meeting meeting = meetingService.getById(meetingId);
|
||||||
if (meeting == null) {
|
if (meeting == null) {
|
||||||
throw new RuntimeException("会议不存在");
|
throw new RuntimeException("会议不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String effectiveSummaryDetailLevel = resolveSummaryDetailLevel(summaryDetailLevel != null ? summaryDetailLevel : meeting.getSummaryDetailLevel());
|
||||||
Long effectiveChapterModelId = chapterModelId != null ? chapterModelId : summaryModelId;
|
Long effectiveChapterModelId = chapterModelId != null ? chapterModelId : summaryModelId;
|
||||||
meetingDomainSupport.createChapterTask(
|
meetingDomainSupport.createChapterTask(
|
||||||
meetingId,
|
meetingId,
|
||||||
summaryModelId,
|
summaryModelId,
|
||||||
effectiveChapterModelId,
|
effectiveChapterModelId,
|
||||||
promptId,
|
promptId,
|
||||||
userPrompt
|
userPrompt,
|
||||||
|
effectiveSummaryDetailLevel
|
||||||
);
|
);
|
||||||
if (Objects.equals(effectiveChapterModelId, summaryModelId)) {
|
if (Objects.equals(effectiveChapterModelId, summaryModelId)) {
|
||||||
meetingDomainSupport.createSummaryTask(meetingId, summaryModelId, promptId, userPrompt);
|
meetingDomainSupport.createSummaryTask(meetingId, summaryModelId, promptId, userPrompt, effectiveSummaryDetailLevel);
|
||||||
} else {
|
} else {
|
||||||
meetingDomainSupport.createSummaryTask(
|
meetingDomainSupport.createSummaryTask(
|
||||||
meetingId,
|
meetingId,
|
||||||
summaryModelId,
|
summaryModelId,
|
||||||
effectiveChapterModelId,
|
effectiveChapterModelId,
|
||||||
promptId,
|
promptId,
|
||||||
userPrompt
|
userPrompt,
|
||||||
|
effectiveSummaryDetailLevel
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
meeting.setSummaryDetailLevel(effectiveSummaryDetailLevel);
|
||||||
meeting.setStatus(2);
|
meeting.setStatus(2);
|
||||||
meetingService.updateById(meeting);
|
meetingService.updateById(meeting);
|
||||||
if ("EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) {
|
if ("EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) {
|
||||||
|
|
@ -890,7 +905,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
summaryModelId,
|
summaryModelId,
|
||||||
chapterModelId != null ? chapterModelId : summaryModelId,
|
chapterModelId != null ? chapterModelId : summaryModelId,
|
||||||
promptId,
|
promptId,
|
||||||
userPrompt
|
userPrompt,
|
||||||
|
meeting.getSummaryDetailLevel()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
resetAiTask(chapterTask, new HashMap<>(chapterTask.getTaskConfig()));
|
resetAiTask(chapterTask, new HashMap<>(chapterTask.getTaskConfig()));
|
||||||
|
|
@ -1150,4 +1166,16 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveSummaryDetailLevel(String requestedSummaryDetailLevel) {
|
||||||
|
if (requestedSummaryDetailLevel == null || requestedSummaryDetailLevel.isBlank()) {
|
||||||
|
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||||
|
}
|
||||||
|
String normalized = requestedSummaryDetailLevel.trim().toUpperCase();
|
||||||
|
if (MeetingConstants.SUMMARY_DETAIL_DETAILED.equals(normalized)
|
||||||
|
|| MeetingConstants.SUMMARY_DETAIL_BRIEF.equals(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
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.imeeting.common.MeetingConstants;
|
||||||
import com.imeeting.entity.biz.AiTask;
|
import com.imeeting.entity.biz.AiTask;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.entity.biz.MeetingTranscript;
|
import com.imeeting.entity.biz.MeetingTranscript;
|
||||||
|
|
@ -52,7 +53,7 @@ public class MeetingDomainSupport {
|
||||||
public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags,
|
public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags,
|
||||||
String audioUrl, String meetingType, String meetingSource,
|
String audioUrl, String meetingType, String meetingSource,
|
||||||
Long tenantId, Long creatorId, String creatorName,
|
Long tenantId, Long creatorId, String creatorName,
|
||||||
Long hostUserId, String hostName, int status) {
|
Long hostUserId, String hostName, String summaryDetailLevel, int status) {
|
||||||
Meeting meeting = new Meeting();
|
Meeting meeting = new Meeting();
|
||||||
meeting.setTitle(title);
|
meeting.setTitle(title);
|
||||||
meeting.setMeetingTime(meetingTime);
|
meeting.setMeetingTime(meetingTime);
|
||||||
|
|
@ -66,31 +67,87 @@ public class MeetingDomainSupport {
|
||||||
meeting.setHostName(hostName);
|
meeting.setHostName(hostName);
|
||||||
meeting.setTenantId(tenantId != null ? tenantId : 0L);
|
meeting.setTenantId(tenantId != null ? tenantId : 0L);
|
||||||
meeting.setAudioUrl(audioUrl);
|
meeting.setAudioUrl(audioUrl);
|
||||||
|
meeting.setSummaryDetailLevel(normalizeSummaryDetailLevel(summaryDetailLevel));
|
||||||
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_NONE);
|
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_NONE);
|
||||||
meeting.setStatus(status);
|
meeting.setStatus(status);
|
||||||
return meeting;
|
return meeting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long promptId, String userPrompt) {
|
public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long promptId, String userPrompt) {
|
||||||
return createSummaryTask(meetingId, summaryModelId, summaryModelId, promptId, userPrompt);
|
return createSummaryTask(
|
||||||
|
meetingId,
|
||||||
|
summaryModelId,
|
||||||
|
summaryModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long promptId,
|
||||||
|
String userPrompt, String summaryDetailLevel) {
|
||||||
|
return createSummaryTask(
|
||||||
|
meetingId,
|
||||||
|
summaryModelId,
|
||||||
|
summaryModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
summaryDetailLevel
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AiTask createChapterTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) {
|
public AiTask createChapterTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) {
|
||||||
|
return createChapterTask(
|
||||||
|
meetingId,
|
||||||
|
summaryModelId,
|
||||||
|
chapterModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AiTask createChapterTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId,
|
||||||
|
String userPrompt, String summaryDetailLevel) {
|
||||||
AiTask chapterTask = new AiTask();
|
AiTask chapterTask = new AiTask();
|
||||||
chapterTask.setMeetingId(meetingId);
|
chapterTask.setMeetingId(meetingId);
|
||||||
chapterTask.setTaskType("CHAPTER");
|
chapterTask.setTaskType("CHAPTER");
|
||||||
chapterTask.setStatus(0);
|
chapterTask.setStatus(0);
|
||||||
chapterTask.setTaskConfig(meetingSummaryPromptAssembler.buildTaskConfig(summaryModelId, chapterModelId, promptId, userPrompt));
|
chapterTask.setTaskConfig(meetingSummaryPromptAssembler.buildTaskConfig(
|
||||||
|
summaryModelId,
|
||||||
|
chapterModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
normalizeSummaryDetailLevel(summaryDetailLevel)
|
||||||
|
));
|
||||||
aiTaskService.save(chapterTask);
|
aiTaskService.save(chapterTask);
|
||||||
return chapterTask;
|
return chapterTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) {
|
public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) {
|
||||||
|
return createSummaryTask(
|
||||||
|
meetingId,
|
||||||
|
summaryModelId,
|
||||||
|
chapterModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AiTask createSummaryTask(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId,
|
||||||
|
String userPrompt, String summaryDetailLevel) {
|
||||||
AiTask sumTask = new AiTask();
|
AiTask sumTask = new AiTask();
|
||||||
sumTask.setMeetingId(meetingId);
|
sumTask.setMeetingId(meetingId);
|
||||||
sumTask.setTaskType("SUMMARY");
|
sumTask.setTaskType("SUMMARY");
|
||||||
sumTask.setStatus(0);
|
sumTask.setStatus(0);
|
||||||
sumTask.setTaskConfig(meetingSummaryPromptAssembler.buildTaskConfig(summaryModelId, chapterModelId, promptId, userPrompt));
|
sumTask.setTaskConfig(meetingSummaryPromptAssembler.buildTaskConfig(
|
||||||
|
summaryModelId,
|
||||||
|
chapterModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
normalizeSummaryDetailLevel(summaryDetailLevel)
|
||||||
|
));
|
||||||
aiTaskService.save(sumTask);
|
aiTaskService.save(sumTask);
|
||||||
return sumTask;
|
return sumTask;
|
||||||
}
|
}
|
||||||
|
|
@ -306,6 +363,7 @@ public class MeetingDomainSupport {
|
||||||
}
|
}
|
||||||
vo.setMeetingType(meeting.getMeetingType());
|
vo.setMeetingType(meeting.getMeetingType());
|
||||||
vo.setMeetingSource(meeting.getMeetingSource());
|
vo.setMeetingSource(meeting.getMeetingSource());
|
||||||
|
vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel()));
|
||||||
vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
|
vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
|
||||||
vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
|
vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
|
||||||
vo.setAccessPassword(meeting.getAccessPassword());
|
vo.setAccessPassword(meeting.getAccessPassword());
|
||||||
|
|
@ -415,6 +473,18 @@ public class MeetingDomainSupport {
|
||||||
return normalized.isEmpty() ? null : normalized;
|
return normalized.isEmpty() ? null : normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String normalizeSummaryDetailLevel(String summaryDetailLevel) {
|
||||||
|
if (summaryDetailLevel == null || summaryDetailLevel.isBlank()) {
|
||||||
|
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||||
|
}
|
||||||
|
String normalized = summaryDetailLevel.trim().toUpperCase();
|
||||||
|
if (MeetingConstants.SUMMARY_DETAIL_DETAILED.equals(normalized)
|
||||||
|
|| MeetingConstants.SUMMARY_DETAIL_BRIEF.equals(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||||
|
}
|
||||||
|
|
||||||
private record AudioRelocationPlan(Path sourcePath, Path targetPath, Path backupPath, String relocatedUrl) {
|
private record AudioRelocationPlan(Path sourcePath, Path targetPath, Path backupPath, String relocatedUrl) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,8 +147,18 @@ public class MeetingQueryServiceImpl implements MeetingQueryService {
|
||||||
String userPrompt = request != null && request.getUserPrompt() != null
|
String userPrompt = request != null && request.getUserPrompt() != null
|
||||||
? request.getUserPrompt()
|
? request.getUserPrompt()
|
||||||
: stringValue(latestSummaryTask.getTaskConfig().get("userPrompt"));
|
: stringValue(latestSummaryTask.getTaskConfig().get("userPrompt"));
|
||||||
|
String summaryDetailLevel = stringValue(latestSummaryTask.getTaskConfig().get("summaryDetailLevel"));
|
||||||
|
if (summaryDetailLevel == null) {
|
||||||
|
summaryDetailLevel = meeting.getSummaryDetailLevel();
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> taskConfig = meetingSummaryPromptAssembler.buildTaskConfig(summaryModelId, chapterModelId, promptId, userPrompt);
|
Map<String, Object> taskConfig = meetingSummaryPromptAssembler.buildTaskConfig(
|
||||||
|
summaryModelId,
|
||||||
|
chapterModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
summaryDetailLevel
|
||||||
|
);
|
||||||
MeetingSummaryPromptContextVO context = new MeetingSummaryPromptContextVO();
|
MeetingSummaryPromptContextVO context = new MeetingSummaryPromptContextVO();
|
||||||
context.setPromptSchemaVersion(String.valueOf(taskConfig.get("promptSchemaVersion")));
|
context.setPromptSchemaVersion(String.valueOf(taskConfig.get("promptSchemaVersion")));
|
||||||
context.setSystemMessage(meetingSummaryPromptAssembler.buildSystemMessage(taskConfig));
|
context.setSystemMessage(meetingSummaryPromptAssembler.buildSystemMessage(taskConfig));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.imeeting.service.biz.impl;
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
|
import com.imeeting.common.MeetingConstants;
|
||||||
import com.imeeting.common.SysParamKeys;
|
import com.imeeting.common.SysParamKeys;
|
||||||
import com.imeeting.dto.biz.MeetingSummarySource;
|
import com.imeeting.dto.biz.MeetingSummarySource;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
|
@ -26,15 +27,33 @@ public class MeetingSummaryPromptAssembler {
|
||||||
private final SysParamService sysParamService;
|
private final SysParamService sysParamService;
|
||||||
|
|
||||||
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long promptId, String userPrompt) {
|
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long promptId, String userPrompt) {
|
||||||
return buildTaskConfig(summaryModelId, summaryModelId, promptId, userPrompt);
|
return buildTaskConfig(
|
||||||
|
summaryModelId,
|
||||||
|
summaryModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) {
|
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) {
|
||||||
|
return buildTaskConfig(
|
||||||
|
summaryModelId,
|
||||||
|
chapterModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long chapterModelId, Long promptId,
|
||||||
|
String userPrompt, String summaryDetailLevel) {
|
||||||
Map<String, Object> taskConfig = new HashMap<>();
|
Map<String, Object> taskConfig = new HashMap<>();
|
||||||
taskConfig.put("summaryModelId", summaryModelId);
|
taskConfig.put("summaryModelId", summaryModelId);
|
||||||
taskConfig.put("chapterModelId", chapterModelId != null ? chapterModelId : summaryModelId);
|
taskConfig.put("chapterModelId", chapterModelId != null ? chapterModelId : summaryModelId);
|
||||||
taskConfig.put("promptSchemaVersion", PROMPT_SCHEMA_VERSION);
|
taskConfig.put("promptSchemaVersion", PROMPT_SCHEMA_VERSION);
|
||||||
taskConfig.put("effectiveSystemPrompt", resolveSystemPrompt());
|
taskConfig.put("effectiveSystemPrompt", resolveSystemPrompt());
|
||||||
|
taskConfig.put("summaryDetailLevel", normalizeSummaryDetailLevel(summaryDetailLevel));
|
||||||
|
|
||||||
String templatePrompt = resolveTemplatePrompt(promptId);
|
String templatePrompt = resolveTemplatePrompt(promptId);
|
||||||
taskConfig.put("effectiveTemplatePrompt", templatePrompt);
|
taskConfig.put("effectiveTemplatePrompt", templatePrompt);
|
||||||
|
|
@ -59,11 +78,13 @@ public class MeetingSummaryPromptAssembler {
|
||||||
stringValue(taskConfig, "promptContent"),
|
stringValue(taskConfig, "promptContent"),
|
||||||
"请输出结构清晰、信息完整、适合直接阅读和导出的正式会议纪要。"
|
"请输出结构清晰、信息完整、适合直接阅读和导出的正式会议纪要。"
|
||||||
);
|
);
|
||||||
|
String summaryDetailLevel = normalizeSummaryDetailLevel(stringValue(taskConfig, "summaryDetailLevel"));
|
||||||
|
|
||||||
return String.join("\n\n",
|
return String.join("\n\n",
|
||||||
"你是一名擅长中文会议纪要、结构化分析和待办提取的助手。",
|
"你是一名擅长中文会议纪要、结构化分析和待办提取的助手。",
|
||||||
"系统提示词(最高优先级):\n" + systemPrompt,
|
"系统提示词(最高优先级):\n" + systemPrompt,
|
||||||
"模板提示词(结构和风格要求):\n" + templatePrompt,
|
"模板提示词(结构和风格要求):\n" + templatePrompt,
|
||||||
|
"总结详细程度要求:\n" + buildSummaryDetailInstruction(summaryDetailLevel),
|
||||||
"""
|
"""
|
||||||
输出要求:
|
输出要求:
|
||||||
1. 最终只允许输出一个 JSON 对象,不要输出 Markdown 代码块、解释说明或额外前后缀。
|
1. 最终只允许输出一个 JSON 对象,不要输出 Markdown 代码块、解释说明或额外前后缀。
|
||||||
|
|
@ -193,4 +214,27 @@ public class MeetingSummaryPromptAssembler {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String normalizeSummaryDetailLevel(String summaryDetailLevel) {
|
||||||
|
if (!StringUtils.hasText(summaryDetailLevel)) {
|
||||||
|
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||||
|
}
|
||||||
|
String normalized = summaryDetailLevel.trim().toUpperCase();
|
||||||
|
if (MeetingConstants.SUMMARY_DETAIL_DETAILED.equals(normalized)
|
||||||
|
|| MeetingConstants.SUMMARY_DETAIL_BRIEF.equals(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildSummaryDetailInstruction(String summaryDetailLevel) {
|
||||||
|
return switch (normalizeSummaryDetailLevel(summaryDetailLevel)) {
|
||||||
|
case MeetingConstants.SUMMARY_DETAIL_DETAILED ->
|
||||||
|
"DETAILED(详细):尽量完整覆盖会议背景、讨论过程、分歧、结论、负责人、时间点和风险,适当展开章节内容。";
|
||||||
|
case MeetingConstants.SUMMARY_DETAIL_BRIEF ->
|
||||||
|
"BRIEF(简洁):只保留核心结论、关键决策、待办事项和必要上下文,避免冗长铺陈。";
|
||||||
|
default ->
|
||||||
|
"STANDARD(标准):保持信息完整和篇幅平衡,覆盖核心过程、结论和待办,不做过度展开。";
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.imeeting.service.biz.impl;
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
|
import com.imeeting.common.MeetingConstants;
|
||||||
import com.imeeting.common.SysParamKeys;
|
import com.imeeting.common.SysParamKeys;
|
||||||
import com.imeeting.dto.biz.MeetingSummarySource;
|
import com.imeeting.dto.biz.MeetingSummarySource;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
|
@ -22,7 +23,7 @@ import static org.mockito.Mockito.when;
|
||||||
class MeetingSummaryPromptAssemblerTest {
|
class MeetingSummaryPromptAssemblerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void buildTaskConfigShouldCaptureEffectivePromptsAndUserPrompt() {
|
void buildTaskConfigShouldCaptureEffectivePromptsUserPromptAndDetailLevel() {
|
||||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||||
SysParamService sysParamService = mock(SysParamService.class);
|
SysParamService sysParamService = mock(SysParamService.class);
|
||||||
PromptTemplate template = new PromptTemplate();
|
PromptTemplate template = new PromptTemplate();
|
||||||
|
|
@ -33,20 +34,27 @@ class MeetingSummaryPromptAssemblerTest {
|
||||||
|
|
||||||
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(promptTemplateService, sysParamService);
|
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(promptTemplateService, sysParamService);
|
||||||
|
|
||||||
Map<String, Object> taskConfig = assembler.buildTaskConfig(2L, 3L, " 关注风险项 ");
|
Map<String, Object> taskConfig = assembler.buildTaskConfig(
|
||||||
|
2L,
|
||||||
|
5L,
|
||||||
|
3L,
|
||||||
|
" 关注风险项 ",
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_DETAILED
|
||||||
|
);
|
||||||
|
|
||||||
assertEquals(2L, taskConfig.get("summaryModelId"));
|
assertEquals(2L, taskConfig.get("summaryModelId"));
|
||||||
assertEquals(2L, taskConfig.get("chapterModelId"));
|
assertEquals(5L, taskConfig.get("chapterModelId"));
|
||||||
assertEquals(3L, taskConfig.get("promptId"));
|
assertEquals(3L, taskConfig.get("promptId"));
|
||||||
assertEquals("v2", taskConfig.get("promptSchemaVersion"));
|
assertEquals("v2", taskConfig.get("promptSchemaVersion"));
|
||||||
assertEquals("系统提示词", taskConfig.get("effectiveSystemPrompt"));
|
assertEquals("系统提示词", taskConfig.get("effectiveSystemPrompt"));
|
||||||
assertEquals("模板提示词", taskConfig.get("effectiveTemplatePrompt"));
|
assertEquals("模板提示词", taskConfig.get("effectiveTemplatePrompt"));
|
||||||
assertEquals("关注风险项", taskConfig.get("userPrompt"));
|
assertEquals("关注风险项", taskConfig.get("userPrompt"));
|
||||||
assertEquals("模板提示词", taskConfig.get("promptContent"));
|
assertEquals("模板提示词", taskConfig.get("promptContent"));
|
||||||
|
assertEquals(MeetingConstants.SUMMARY_DETAIL_DETAILED, taskConfig.get("summaryDetailLevel"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void buildSystemMessageShouldFallbackToLegacyPromptContent() {
|
void buildSystemMessageShouldContainSummaryDetailInstruction() {
|
||||||
SysParamService sysParamService = mock(SysParamService.class);
|
SysParamService sysParamService = mock(SysParamService.class);
|
||||||
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT), eq("")))
|
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT), eq("")))
|
||||||
.thenReturn("系统提示词");
|
.thenReturn("系统提示词");
|
||||||
|
|
@ -55,9 +63,14 @@ class MeetingSummaryPromptAssemblerTest {
|
||||||
sysParamService
|
sysParamService
|
||||||
);
|
);
|
||||||
|
|
||||||
String systemMessage = assembler.buildSystemMessage(Map.of("promptContent", "旧模板提示词"));
|
String systemMessage = assembler.buildSystemMessage(Map.of(
|
||||||
|
"promptContent", "旧模板提示词",
|
||||||
|
"summaryDetailLevel", MeetingConstants.SUMMARY_DETAIL_BRIEF
|
||||||
|
));
|
||||||
|
|
||||||
assertTrue(systemMessage.contains("旧模板提示词"));
|
assertTrue(systemMessage.contains("旧模板提示词"));
|
||||||
|
assertTrue(systemMessage.contains("BRIEF"));
|
||||||
|
assertTrue(systemMessage.contains("简洁"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -89,8 +102,8 @@ class MeetingSummaryPromptAssemblerTest {
|
||||||
meeting.setParticipants("Alice,Bob");
|
meeting.setParticipants("Alice,Bob");
|
||||||
|
|
||||||
MeetingSummarySource source = MeetingSummarySource.builder()
|
MeetingSummarySource source = MeetingSummarySource.builder()
|
||||||
.chapterOutlineText("第1章 预算评审\n章节摘要:讨论320万预算")
|
.chapterOutlineText("第1章 预算评审\n章节摘要:讨论 20 万预算")
|
||||||
.rawTranscriptText("Alice: 今天讨论320万预算,时间定在2026年5月7日15:30。")
|
.rawTranscriptText("Alice: 今天讨论 320 万预算,时间定在 2026 年 5 月 8 日 15:30。")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
String userMessage = assembler.buildUserMessage(meeting, source, "关注预算结论");
|
String userMessage = assembler.buildUserMessage(meeting, source, "关注预算结论");
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,14 @@ import axios from "axios";
|
||||||
const MEETING_UPLOAD_FLOW_TIMEOUT = 600000;
|
const MEETING_UPLOAD_FLOW_TIMEOUT = 600000;
|
||||||
const MEETING_DETAIL_TIMEOUT = 120000;
|
const MEETING_DETAIL_TIMEOUT = 120000;
|
||||||
|
|
||||||
|
export type SummaryDetailLevel = "DETAILED" | "STANDARD" | "BRIEF";
|
||||||
|
|
||||||
|
export interface MeetingCreateConfig {
|
||||||
|
offlineEnabled: boolean;
|
||||||
|
realtimeEnabled: boolean;
|
||||||
|
offlineAudioMaxSizeMb: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MeetingVO {
|
export interface MeetingVO {
|
||||||
id: number;
|
id: number;
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
|
|
@ -20,6 +28,7 @@ export interface MeetingVO {
|
||||||
playbackAudioUrl?: string;
|
playbackAudioUrl?: string;
|
||||||
meetingType?: "OFFLINE" | "REALTIME";
|
meetingType?: "OFFLINE" | "REALTIME";
|
||||||
meetingSource?: "WEB" | "ANDROID";
|
meetingSource?: "WEB" | "ANDROID";
|
||||||
|
summaryDetailLevel?: SummaryDetailLevel;
|
||||||
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
|
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
|
||||||
audioSaveMessage?: string;
|
audioSaveMessage?: string;
|
||||||
accessPassword?: string;
|
accessPassword?: string;
|
||||||
|
|
@ -80,6 +89,7 @@ export interface CreateMeetingCommand {
|
||||||
promptId: number;
|
promptId: number;
|
||||||
hotWordGroupId?: number;
|
hotWordGroupId?: number;
|
||||||
userPrompt?: string;
|
userPrompt?: string;
|
||||||
|
summaryDetailLevel?: SummaryDetailLevel;
|
||||||
useSpkId?: number;
|
useSpkId?: number;
|
||||||
enableTextRefine?: boolean;
|
enableTextRefine?: boolean;
|
||||||
hotWords?: string[];
|
hotWords?: string[];
|
||||||
|
|
@ -99,6 +109,7 @@ export interface CreateRealtimeMeetingCommand {
|
||||||
promptId: number;
|
promptId: number;
|
||||||
hotWordGroupId?: number;
|
hotWordGroupId?: number;
|
||||||
userPrompt?: string;
|
userPrompt?: string;
|
||||||
|
summaryDetailLevel?: SummaryDetailLevel;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
useSpkId?: number;
|
useSpkId?: number;
|
||||||
|
|
@ -285,6 +296,12 @@ export const getMeetingDetail = (id: number, options?: { suppressErrorToast?: bo
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMeetingCreateConfig = () => {
|
||||||
|
return http.get<{ code: string; data: MeetingCreateConfig; msg: string }>(
|
||||||
|
"/api/biz/meeting/create-config"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getTranscripts = (id: number) => {
|
export const getTranscripts = (id: number) => {
|
||||||
return http.get<{ code: string; data: MeetingTranscriptVO[]; msg: string }>(
|
return http.get<{ code: string; data: MeetingTranscriptVO[]; msg: string }>(
|
||||||
`/api/biz/meeting/${id}/transcripts`
|
`/api/biz/meeting/${id}/transcripts`
|
||||||
|
|
@ -345,6 +362,7 @@ export interface MeetingResummaryDTO {
|
||||||
summaryModelId: number;
|
summaryModelId: number;
|
||||||
promptId: number;
|
promptId: number;
|
||||||
userPrompt?: string;
|
userPrompt?: string;
|
||||||
|
summaryDetailLevel?: SummaryDetailLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reSummary = (params: MeetingResummaryDTO) => {
|
export const reSummary = (params: MeetingResummaryDTO) => {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ import {
|
||||||
PermissionNode
|
PermissionNode
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
|
export interface RoleAuthorizationPayload {
|
||||||
|
permissionIds?: number[];
|
||||||
|
dataScope?: RoleDataScope;
|
||||||
|
}
|
||||||
|
|
||||||
export async function pageParams(params: SysParamQuery) {
|
export async function pageParams(params: SysParamQuery) {
|
||||||
const resp = await http.get("/sys/api/params/page", { params });
|
const resp = await http.get("/sys/api/params/page", { params });
|
||||||
return resp.data.data as PageResult<SysParamVO[]>;
|
return resp.data.data as PageResult<SysParamVO[]>;
|
||||||
|
|
@ -189,6 +194,11 @@ export async function saveRolePermissions(roleId: number, permIds: number[]) {
|
||||||
return resp.data.data as boolean;
|
return resp.data.data as boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveRoleAuthorization(roleId: number, payload: RoleAuthorizationPayload) {
|
||||||
|
const resp = await http.post(`/sys/api/roles/${roleId}/authorization`, payload);
|
||||||
|
return resp.data.data as boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRoleDataScope(roleId: number) {
|
export async function getRoleDataScope(roleId: number) {
|
||||||
const resp = await http.get(`/sys/api/roles/${roleId}/data-scope`);
|
const resp = await http.get(`/sys/api/roles/${roleId}/data-scope`);
|
||||||
return resp.data.data as RoleDataScope;
|
return resp.data.data as RoleDataScope;
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,65 @@
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { Drawer, Form, Input, Select, DatePicker, Switch, Upload, Progress, Space, Avatar, Row, Col, Radio, Typography, Tooltip, App, Tag, Button, Collapse } from 'antd';
|
import {
|
||||||
import { UserOutlined, CloudUploadOutlined, AudioOutlined, QuestionCircleOutlined, CheckOutlined, LinkOutlined, SettingOutlined } from '@ant-design/icons';
|
App,
|
||||||
import dayjs from 'dayjs';
|
Avatar,
|
||||||
import { useNavigate } from 'react-router-dom';
|
Button,
|
||||||
|
Col,
|
||||||
|
Collapse,
|
||||||
|
DatePicker,
|
||||||
|
Drawer,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Progress,
|
||||||
|
Radio,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Skeleton,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
Upload,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
AudioOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
CloudUploadOutlined,
|
||||||
|
QuestionCircleOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel';
|
import { listUsers } from "../../api";
|
||||||
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
|
import { getAiModelDefault, getAiModelPage, type AiModelVO } from "../../api/business/aimodel";
|
||||||
import { getHotWordPage, HotWordVO } from '../../api/business/hotword';
|
import { getHotWordPage, type HotWordVO } from "../../api/business/hotword";
|
||||||
import { getHotWordGroupOptions, HotWordGroupVO } from '../../api/business/hotwordGroup';
|
import { getHotWordGroupOptions, type HotWordGroupVO } from "../../api/business/hotwordGroup";
|
||||||
import { listUsers, pageParams } from '../../api';
|
import {
|
||||||
import { createMeeting, createRealtimeMeeting, uploadAudio, CreateRealtimeMeetingCommand } from '../../api/business/meeting';
|
createMeeting,
|
||||||
import { SysUser } from '../../types';
|
createRealtimeMeeting,
|
||||||
|
getMeetingCreateConfig,
|
||||||
|
type CreateRealtimeMeetingCommand,
|
||||||
|
type MeetingCreateConfig,
|
||||||
|
type SummaryDetailLevel,
|
||||||
|
uploadAudio,
|
||||||
|
} from "../../api/business/meeting";
|
||||||
|
import { getPromptPage, type PromptTemplateVO } from "../../api/business/prompt";
|
||||||
|
import type { SysUser } from "../../types";
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
const { Dragger } = Upload;
|
const { Dragger } = Upload;
|
||||||
|
const { Option } = Select;
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
export type MeetingCreateType = 'upload' | 'realtime';
|
export type MeetingCreateType = "upload" | "realtime";
|
||||||
|
|
||||||
const DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB = 1024;
|
const DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB = 1024;
|
||||||
const OFFLINE_AUDIO_MAX_SIZE_PARAM_KEY = 'meeting.offline_audio.max_size_mb';
|
const DEFAULT_CREATE_CONFIG: MeetingCreateConfig = {
|
||||||
|
offlineEnabled: true,
|
||||||
|
realtimeEnabled: true,
|
||||||
|
offlineAudioMaxSizeMb: DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB,
|
||||||
|
};
|
||||||
|
|
||||||
interface MeetingCreateDrawerProps {
|
interface MeetingCreateDrawerProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -44,29 +84,54 @@ type RealtimeMeetingSessionDraft = {
|
||||||
hotwords: Array<{ hotword: string; weight: number }>;
|
hotwords: Array<{ hotword: string; weight: number }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveAvailableCreateTypes(config: MeetingCreateConfig): MeetingCreateType[] {
|
||||||
|
const types: MeetingCreateType[] = [];
|
||||||
|
if (config.offlineEnabled) {
|
||||||
|
types.push("upload");
|
||||||
|
}
|
||||||
|
if (config.realtimeEnabled) {
|
||||||
|
types.push("realtime");
|
||||||
|
}
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAvailableCreateType(initialType: MeetingCreateType, config: MeetingCreateConfig): MeetingCreateType {
|
||||||
|
const availableTypes = resolveAvailableCreateTypes(config);
|
||||||
|
if (availableTypes.includes(initialType)) {
|
||||||
|
return initialType;
|
||||||
|
}
|
||||||
|
return availableTypes[0] || initialType;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveWsUrl(model?: AiModelVO | null) {
|
function resolveWsUrl(model?: AiModelVO | null) {
|
||||||
if (model?.wsUrl) return model.wsUrl;
|
if (model?.wsUrl) return model.wsUrl;
|
||||||
if (model?.baseUrl) return model.baseUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
|
if (model?.baseUrl) return model.baseUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRealtimeProxyPreviewUrl() {
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
||||||
return `${protocol}://${window.location.host}/ws/meeting/realtime`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSessionKey(meetingId: number) {
|
function getSessionKey(meetingId: number) {
|
||||||
return `realtimeMeetingSession:${meetingId}`;
|
return `realtimeMeetingSession:${meetingId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open, initialType = 'upload', onCancel, onSuccess }) => {
|
export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
||||||
|
open,
|
||||||
|
initialType = "upload",
|
||||||
|
onCancel,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const [type, setType] = useState<MeetingCreateType>(initialType);
|
const [type, setType] = useState<MeetingCreateType>(initialType);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [configLoaded, setConfigLoaded] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [createConfig, setCreateConfig] = useState<MeetingCreateConfig>({
|
||||||
|
offlineEnabled: false,
|
||||||
|
realtimeEnabled: false,
|
||||||
|
offlineAudioMaxSizeMb: DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB,
|
||||||
|
});
|
||||||
|
|
||||||
const [asrModels, setAsrModels] = useState<AiModelVO[]>([]);
|
const [asrModels, setAsrModels] = useState<AiModelVO[]>([]);
|
||||||
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
||||||
|
|
@ -74,76 +139,83 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
const [hotwordList, setHotwordList] = useState<HotWordVO[]>([]);
|
const [hotwordList, setHotwordList] = useState<HotWordVO[]>([]);
|
||||||
const [hotWordGroups, setHotWordGroups] = useState<HotWordGroupVO[]>([]);
|
const [hotWordGroups, setHotWordGroups] = useState<HotWordGroupVO[]>([]);
|
||||||
const [userList, setUserList] = useState<SysUser[]>([]);
|
const [userList, setUserList] = useState<SysUser[]>([]);
|
||||||
const [hotWordGroupTouched, setHotWordGroupTouched] = useState(false);
|
|
||||||
|
|
||||||
const [audioUrl, setAudioUrl] = useState('');
|
const [audioUrl, setAudioUrl] = useState("");
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [fileList, setFileList] = useState<any[]>([]);
|
const [fileList, setFileList] = useState<any[]>([]);
|
||||||
const [offlineAudioMaxSizeMb, setOfflineAudioMaxSizeMb] = useState(DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB);
|
|
||||||
|
|
||||||
const watchedAsrModelId = Form.useWatch("asrModelId", form);
|
const watchedAsrModelId = Form.useWatch("asrModelId", form);
|
||||||
|
const watchedSummaryModelId = Form.useWatch("summaryModelId", form);
|
||||||
const watchedPromptId = Form.useWatch("promptId", form);
|
const watchedPromptId = Form.useWatch("promptId", form);
|
||||||
const watchedHotWordGroupId = Form.useWatch("hotWordGroupId", form);
|
const watchedHotWordGroupId = Form.useWatch("hotWordGroupId", form);
|
||||||
const watchedSummaryModelId = Form.useWatch("summaryModelId", form);
|
|
||||||
|
|
||||||
const selectedAsrModel = useMemo(() => asrModels.find((item) => item.id === watchedAsrModelId) || null, [asrModels, watchedAsrModelId]);
|
const selectedAsrModel = useMemo(
|
||||||
const selectedSummaryModel = useMemo(() => llmModels.find((item) => item.id === watchedSummaryModelId) || null, [llmModels, watchedSummaryModelId]);
|
() => asrModels.find((item) => item.id === watchedAsrModelId) || null,
|
||||||
const selectedPrompt = useMemo(() => prompts.find((item) => item.id === watchedPromptId) || null, [prompts, watchedPromptId]);
|
[asrModels, watchedAsrModelId]
|
||||||
const offlineAudioMaxSizeBytes = useMemo(() => offlineAudioMaxSizeMb * 1024 * 1024, [offlineAudioMaxSizeMb]);
|
);
|
||||||
|
const selectedSummaryModel = useMemo(
|
||||||
|
() => llmModels.find((item) => item.id === watchedSummaryModelId) || null,
|
||||||
|
[llmModels, watchedSummaryModelId]
|
||||||
|
);
|
||||||
|
const selectedPrompt = useMemo(
|
||||||
|
() => prompts.find((item) => item.id === watchedPromptId) || null,
|
||||||
|
[prompts, watchedPromptId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableTypes = useMemo(() => resolveAvailableCreateTypes(createConfig), [createConfig]);
|
||||||
|
const offlineAudioMaxSizeBytes = useMemo(
|
||||||
|
() => (createConfig.offlineAudioMaxSizeMb || DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB) * 1024 * 1024,
|
||||||
|
[createConfig]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (!open) {
|
||||||
setType(initialType);
|
|
||||||
setHotWordGroupTouched(false);
|
|
||||||
loadInitialData();
|
|
||||||
setAudioUrl('');
|
|
||||||
setUploadProgress(0);
|
|
||||||
setFileList([]);
|
|
||||||
}
|
|
||||||
}, [open, initialType]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open || hotWordGroupTouched) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
form.setFieldValue('hotWordGroupId', selectedPrompt?.hotWordGroupId);
|
setAudioUrl("");
|
||||||
}, [form, hotWordGroupTouched, open, selectedPrompt]);
|
setUploadProgress(0);
|
||||||
|
setFileList([]);
|
||||||
|
void loadInitialData();
|
||||||
|
}, [open, initialType]);
|
||||||
|
|
||||||
const loadInitialData = async () => {
|
const loadInitialData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [asrRes, llmRes, promptRes, hotwordRes, hotWordGroupRes, users, defaultAsr, defaultLlm] = await Promise.all([
|
const [asrRes, llmRes, promptRes, hotwordRes, hotWordGroupRes, users, defaultAsr, defaultLlm, createConfigRes] = await Promise.all([
|
||||||
getAiModelPage({ current: 1, size: 100, type: 'ASR' }),
|
getAiModelPage({ current: 1, size: 100, type: "ASR" }),
|
||||||
getAiModelPage({ current: 1, size: 100, type: 'LLM' }),
|
getAiModelPage({ current: 1, size: 100, type: "LLM" }),
|
||||||
getPromptPage({ current: 1, size: 100 }),
|
getPromptPage({ current: 1, size: 100 }),
|
||||||
getHotWordPage({ current: 1, size: 1000 }),
|
getHotWordPage({ current: 1, size: 1000 }),
|
||||||
getHotWordGroupOptions(),
|
getHotWordGroupOptions(),
|
||||||
listUsers(),
|
listUsers(),
|
||||||
getAiModelDefault("ASR"),
|
getAiModelDefault("ASR"),
|
||||||
getAiModelDefault("LLM"),
|
getAiModelDefault("LLM"),
|
||||||
|
getMeetingCreateConfig(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const activeAsrModels = asrRes.data.data.records.filter((m: AiModelVO) => m.status === 1);
|
const nextConfig = createConfigRes.data.data || DEFAULT_CREATE_CONFIG;
|
||||||
const activeLlmModels = llmRes.data.data.records.filter((m: AiModelVO) => m.status === 1);
|
const nextType = resolveAvailableCreateType(initialType, nextConfig);
|
||||||
const activePrompts = promptRes.data.data.records.filter((p: PromptTemplateVO) => p.status === 1);
|
const activePrompts = promptRes.data.data.records.filter((item: PromptTemplateVO) => item.status === 1);
|
||||||
const activeHotwords = hotwordRes.data.data.records.filter((h: HotWordVO) => h.status === 1);
|
|
||||||
|
|
||||||
setAsrModels(activeAsrModels);
|
setCreateConfig(nextConfig);
|
||||||
setLlmModels(activeLlmModels);
|
setConfigLoaded(true);
|
||||||
|
setType(nextType);
|
||||||
|
setAsrModels(asrRes.data.data.records.filter((item: AiModelVO) => item.status === 1));
|
||||||
|
setLlmModels(llmRes.data.data.records.filter((item: AiModelVO) => item.status === 1));
|
||||||
setPrompts(activePrompts);
|
setPrompts(activePrompts);
|
||||||
setHotwordList(activeHotwords);
|
setHotwordList(hotwordRes.data.data.records.filter((item: HotWordVO) => item.status === 1));
|
||||||
setHotWordGroups((hotWordGroupRes.data.data || []).filter((item: HotWordGroupVO) => item.status === 1));
|
setHotWordGroups((hotWordGroupRes.data.data || []).filter((item: HotWordGroupVO) => item.status === 1));
|
||||||
setUserList(users || []);
|
setUserList(users || []);
|
||||||
setOfflineAudioMaxSizeMb(await loadOfflineAudioMaxSizeMb());
|
|
||||||
const defaultPrompt = activePrompts.length > 0 ? activePrompts[0] : undefined;
|
|
||||||
|
|
||||||
|
const defaultPrompt = activePrompts[0];
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
title: type === 'upload' ? `文件会议 ${dayjs().format("MM-DD HH:mm")}` : `实时会议 ${dayjs().format("MM-DD HH:mm")}`,
|
title: nextType === "upload" ? `文件会议 ${dayjs().format("MM-DD HH:mm")}` : `实时会议 ${dayjs().format("MM-DD HH:mm")}`,
|
||||||
meetingTime: dayjs(),
|
meetingTime: dayjs(),
|
||||||
asrModelId: defaultAsr.data.data?.id,
|
asrModelId: defaultAsr.data.data?.id,
|
||||||
summaryModelId: defaultLlm.data.data?.id,
|
summaryModelId: defaultLlm.data.data?.id,
|
||||||
promptId: defaultPrompt?.id,
|
promptId: defaultPrompt?.id,
|
||||||
hotWordGroupId: defaultPrompt?.hotWordGroupId,
|
hotWordGroupId: defaultPrompt?.hotWordGroupId ?? 0,
|
||||||
|
summaryDetailLevel: "STANDARD",
|
||||||
useSpkId: 1,
|
useSpkId: 1,
|
||||||
enableTextRefine: false,
|
enableTextRefine: false,
|
||||||
mode: "2pass",
|
mode: "2pass",
|
||||||
|
|
@ -152,24 +224,36 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
enableItn: true,
|
enableItn: true,
|
||||||
saveAudio: false,
|
saveAudio: false,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch {
|
||||||
message.error("加载配置失败");
|
message.error("加载配置失败");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sync title when type changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) {
|
||||||
const currentTitle = form.getFieldValue('title');
|
return;
|
||||||
if (currentTitle && (currentTitle.startsWith('文件会议') || currentTitle.startsWith('实时会议'))) {
|
}
|
||||||
|
const currentTitle = form.getFieldValue("title");
|
||||||
|
if (typeof currentTitle !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentTitle.startsWith("文件会议") || currentTitle.startsWith("实时会议")) {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
title: type === 'upload' ? `文件会议 ${dayjs().format("MM-DD HH:mm")}` : `实时会议 ${dayjs().format("MM-DD HH:mm")}`,
|
title: type === "upload" ? `文件会议 ${dayjs().format("MM-DD HH:mm")}` : `实时会议 ${dayjs().format("MM-DD HH:mm")}`,
|
||||||
useSpkId: 1
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [type, form, open]);
|
}, [form, open, type]);
|
||||||
|
|
||||||
|
const beforeAudioUpload = (file: File) => {
|
||||||
|
if (file.size > offlineAudioMaxSizeBytes) {
|
||||||
|
message.error(`录音文件大小不能超过 ${createConfig.offlineAudioMaxSizeMb}MB`);
|
||||||
|
setUploadProgress(0);
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const customUpload = async (options: any) => {
|
const customUpload = async (options: any) => {
|
||||||
const { file, onSuccess: uploadSuccess, onError, onProgress } = options;
|
const { file, onSuccess: uploadSuccess, onError, onProgress } = options;
|
||||||
|
|
@ -177,62 +261,45 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
try {
|
try {
|
||||||
const res = await uploadAudio(file, (progressEvent) => {
|
const res = await uploadAudio(file, (progressEvent) => {
|
||||||
if (progressEvent.total) {
|
if (progressEvent.total) {
|
||||||
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
const percent = Math.min(99, Math.round((progressEvent.loaded * 100) / progressEvent.total));
|
||||||
// Only show up to 99% during upload, save 100% for actual completion
|
setUploadProgress(percent);
|
||||||
const displayPercent = percentCompleted > 99 ? 99 : percentCompleted;
|
onProgress({ percent });
|
||||||
setUploadProgress(displayPercent);
|
|
||||||
onProgress({ percent: displayPercent });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
onProgress({ percent: 100 });
|
onProgress({ percent: 100 });
|
||||||
setAudioUrl(res.data.data);
|
setAudioUrl(res.data.data);
|
||||||
uploadSuccess(res.data.data);
|
uploadSuccess(res.data.data);
|
||||||
message.success('录音上传成功');
|
message.success("录音上传成功");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
onError(err);
|
onError(err);
|
||||||
if (!(err instanceof Error) || !err.message) {
|
message.error("文件上传失败");
|
||||||
message.error('文件上传失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const beforeAudioUpload = (file: File) => {
|
|
||||||
if (file.size > offlineAudioMaxSizeBytes) {
|
|
||||||
message.error(`录音文件大小不能超过 ${offlineAudioMaxSizeMb}MB`);
|
|
||||||
setUploadProgress(0);
|
|
||||||
return Upload.LIST_IGNORE;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadOfflineAudioMaxSizeMb = async () => {
|
|
||||||
try {
|
|
||||||
const result = await pageParams({
|
|
||||||
paramKey: OFFLINE_AUDIO_MAX_SIZE_PARAM_KEY,
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
|
||||||
const matched = (result.records || []).find((item) => item.paramKey === OFFLINE_AUDIO_MAX_SIZE_PARAM_KEY);
|
|
||||||
const parsed = Number(matched?.paramValue);
|
|
||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB;
|
|
||||||
} catch {
|
|
||||||
return DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
if (type === 'upload' && !audioUrl) {
|
if (availableTypes.length === 0) {
|
||||||
message.error('请先上传录音文件');
|
message.error("当前系统参数已关闭全部会议创建入口");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!availableTypes.includes(type)) {
|
||||||
|
const fallbackType = availableTypes[0];
|
||||||
|
if (fallbackType) {
|
||||||
|
setType(fallbackType);
|
||||||
|
}
|
||||||
|
message.warning("当前入口已关闭,已切换到可用创建方式");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === "upload" && !audioUrl) {
|
||||||
|
message.error("请先上传录音文件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
if (type === "realtime") {
|
||||||
if (type === 'realtime') {
|
|
||||||
const wsUrl = resolveWsUrl(selectedAsrModel);
|
const wsUrl = resolveWsUrl(selectedAsrModel);
|
||||||
if (!wsUrl) {
|
if (!wsUrl) {
|
||||||
message.error("当前 ASR 模型没有配置 WebSocket 地址");
|
message.error("当前 ASR 模型未配置 WebSocket 地址");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -247,20 +314,23 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
.map((item) => item.word)
|
.map((item) => item.word)
|
||||||
.filter((word) => !!word?.trim());
|
.filter((word) => !!word?.trim());
|
||||||
|
|
||||||
if (type === 'upload') {
|
if (type === "upload") {
|
||||||
await createMeeting({
|
await createMeeting({
|
||||||
...meetingValues,
|
...meetingValues,
|
||||||
...(hostUserId != null ? { hostUserId } : {}),
|
...(hostUserId != null ? { hostUserId } : {}),
|
||||||
meetingTime: meetingValues.meetingTime.format('YYYY-MM-DD HH:mm:ss'),
|
meetingTime: meetingValues.meetingTime.format("YYYY-MM-DD HH:mm:ss"),
|
||||||
audioUrl,
|
audioUrl,
|
||||||
participants: meetingValues.participants?.join(','),
|
participants: meetingValues.participants?.join(","),
|
||||||
tags: meetingValues.tags?.join(','),
|
tags: meetingValues.tags?.join(","),
|
||||||
hotWords: selectedHotWords
|
summaryDetailLevel: meetingValues.summaryDetailLevel as SummaryDetailLevel,
|
||||||
|
hotWords: selectedHotWords,
|
||||||
});
|
});
|
||||||
message.success('会议发起成功');
|
message.success("会议发起成功");
|
||||||
onSuccess();
|
onSuccess();
|
||||||
onCancel();
|
onCancel();
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const selectedHotwords = hotwordList
|
const selectedHotwords = hotwordList
|
||||||
.filter((item) => item.hotWordGroupId === meetingValues.hotWordGroupId && meetingValues.hotWordGroupId !== 0)
|
.filter((item) => item.hotWordGroupId === meetingValues.hotWordGroupId && meetingValues.hotWordGroupId !== 0)
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
|
|
@ -274,6 +344,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
meetingTime: meetingValues.meetingTime.format("YYYY-MM-DD HH:mm:ss"),
|
meetingTime: meetingValues.meetingTime.format("YYYY-MM-DD HH:mm:ss"),
|
||||||
participants: meetingValues.participants?.join(",") || "",
|
participants: meetingValues.participants?.join(",") || "",
|
||||||
tags: meetingValues.tags?.join(",") || "",
|
tags: meetingValues.tags?.join(",") || "",
|
||||||
|
summaryDetailLevel: meetingValues.summaryDetailLevel as SummaryDetailLevel,
|
||||||
mode: meetingValues.mode || "2pass",
|
mode: meetingValues.mode || "2pass",
|
||||||
language: meetingValues.language || "auto",
|
language: meetingValues.language || "auto",
|
||||||
useSpkId: meetingValues.useSpkId == null ? 1 : (meetingValues.useSpkId ? 1 : 0),
|
useSpkId: meetingValues.useSpkId == null ? 1 : (meetingValues.useSpkId ? 1 : 0),
|
||||||
|
|
@ -286,7 +357,6 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
|
|
||||||
const res = await createRealtimeMeeting(payload);
|
const res = await createRealtimeMeeting(payload);
|
||||||
const createdMeeting = res.data.data;
|
const createdMeeting = res.data.data;
|
||||||
|
|
||||||
const sessionDraft: RealtimeMeetingSessionDraft = {
|
const sessionDraft: RealtimeMeetingSessionDraft = {
|
||||||
meetingId: createdMeeting.id,
|
meetingId: createdMeeting.id,
|
||||||
meetingTitle: createdMeeting.title,
|
meetingTitle: createdMeeting.title,
|
||||||
|
|
@ -308,9 +378,8 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
onSuccess();
|
onSuccess();
|
||||||
onCancel();
|
onCancel();
|
||||||
navigate(`/meeting-live-session/${createdMeeting.id}`);
|
navigate(`/meeting-live-session/${createdMeeting.id}`);
|
||||||
}
|
} catch {
|
||||||
} catch (err) {
|
message.error(type === "upload" ? "创建会议失败" : "创建实时会议失败");
|
||||||
message.error(type === 'upload' ? '创建会议失败' : '创建实时会议失败');
|
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -327,47 +396,59 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
placement="right"
|
placement="right"
|
||||||
closable={false}
|
closable={false}
|
||||||
footer={
|
footer={
|
||||||
<div style={{ textAlign: 'right', padding: '16px 32px' }}>
|
configLoaded ? (
|
||||||
|
<div style={{ textAlign: "right", padding: "16px 32px" }}>
|
||||||
<Space size={16}>
|
<Space size={16}>
|
||||||
<Button onClick={onCancel} size="large" style={{ borderRadius: 8, minWidth: 120 }}>取消</Button>
|
<Button onClick={onCancel} size="large" style={{ borderRadius: 8, minWidth: 120 }}>取消</Button>
|
||||||
<Button type="primary" onClick={handleOk} loading={submitting} size="large" style={{ borderRadius: 8, minWidth: 140, fontWeight: 500 }}>
|
<Button type="primary" onClick={handleOk} loading={submitting} size="large" style={{ borderRadius: 8, minWidth: 140, fontWeight: 500 }}>
|
||||||
{type === 'upload' ? (audioUrl ? '开始分析' : '创建并上传') : '创建并进入识别'}
|
{type === "upload" ? (audioUrl ? "完成并分析" : "上传并分析") : "创建并进入识别"}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
styles={{
|
styles={{
|
||||||
header: { display: 'none' },
|
header: { display: "none" },
|
||||||
body: { padding: 0, display: 'flex', flexDirection: 'column', background: 'var(--app-bg-layout)' },
|
body: { padding: 0, display: "flex", flexDirection: "column", background: "var(--app-bg-layout)" },
|
||||||
footer: { padding: 0, borderTop: '1px solid var(--app-border-color)', background: 'var(--app-bg-surface)' }
|
footer: { padding: 0, borderTop: "1px solid var(--app-border-color)", background: "var(--app-bg-surface)", minHeight: configLoaded ? 72 : 0 }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ background: 'var(--app-bg-surface)', padding: '24px 32px', borderBottom: '1px solid var(--app-border-color)' }}>
|
{!configLoaded ? (
|
||||||
|
<div style={{ padding: "40px" }}>
|
||||||
|
<Skeleton active paragraph={{ rows: 12 }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ background: "var(--app-bg-surface)", padding: "24px 32px", borderBottom: "1px solid var(--app-border-color)" }}>
|
||||||
<Row justify="space-between" align="middle">
|
<Row justify="space-between" align="middle">
|
||||||
<Col>
|
<Col>
|
||||||
<Space size={16}>
|
<Space size={16}>
|
||||||
<div style={{ width: 48, height: 48, borderRadius: 12, background: 'var(--app-bg-surface-strong)', color: 'var(--app-text-main)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 24, border: '1px solid var(--app-border-color)' }}>
|
<div style={{ width: 48, height: 48, borderRadius: 12, background: "var(--app-bg-surface-strong)", color: "var(--app-text-main)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 24, border: "1px solid var(--app-border-color)" }}>
|
||||||
{type === 'upload' ? <CloudUploadOutlined /> : <AudioOutlined />}
|
{type === "upload" ? <CloudUploadOutlined /> : <AudioOutlined />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Title level={4} style={{ margin: 0, fontWeight: 600 }}>{type === 'upload' ? '上传录音分析' : '创建实时会议'}</Title>
|
<Title level={4} style={{ margin: 0, fontWeight: 600 }}>{type === "upload" ? "上传录音发起分析" : "创建实时会议"}</Title>
|
||||||
<Text type="secondary" style={{ fontSize: 13 }}>{type === 'upload' ? '上传已有录音文件进行转写和总结' : '创建会议实时进行语音转写和内容分析'}</Text>
|
<Text type="secondary" style={{ fontSize: 13 }}>{type === "upload" ? "上传已有音频文件并由 AI 进行转写与总结分析" : "实时采集语音并进行流式转写与实时纪要生成"}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Radio.Group value={type} onChange={e => setType(e.target.value)} optionType="button" buttonStyle="solid" size="large">
|
<Radio.Group value={type} onChange={e => setType(e.target.value)} optionType="button" buttonStyle="solid" size="large">
|
||||||
<Radio.Button value="upload" style={{ padding: '0 24px' }}><CloudUploadOutlined style={{ marginRight: 6 }} /> 上传录音</Radio.Button>
|
{createConfig.offlineEnabled && (
|
||||||
<Radio.Button value="realtime" style={{ padding: '0 24px' }}><AudioOutlined style={{ marginRight: 6 }} /> 实时识别</Radio.Button>
|
<Radio.Button value="upload" style={{ padding: "0 24px" }}><CloudUploadOutlined style={{ marginRight: 6 }} /> 上传录音</Radio.Button>
|
||||||
|
)}
|
||||||
|
{createConfig.realtimeEnabled && (
|
||||||
|
<Radio.Button value="realtime" style={{ padding: "0 24px" }}><AudioOutlined style={{ marginRight: 6 }} /> 实时会议</Radio.Button>
|
||||||
|
)}
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ padding: '32px 40px', flex: 1, overflowY: 'auto', background: 'var(--app-bg-layout)' }}>
|
<div style={{ padding: "32px 40px", flex: 1, overflowY: "auto", background: "var(--app-bg-layout)" }}>
|
||||||
<Form form={form} layout="vertical" disabled={loading}>
|
<Form form={form} layout="vertical" disabled={loading}>
|
||||||
<div style={{ marginBottom: 24, display: 'flex', alignItems: 'center' }}>
|
<div style={{ marginBottom: 24, display: "flex", alignItems: "center" }}>
|
||||||
<div style={{ width: 4, height: 16, background: '#1890ff', borderRadius: 2, marginRight: 8 }} />
|
<div style={{ width: 4, height: 16, background: "#1890ff", borderRadius: 2, marginRight: 8 }} />
|
||||||
<Title level={5} style={{ margin: 0 }}>基础信息</Title>
|
<Title level={5} style={{ margin: 0 }}>基础信息</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -379,7 +460,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="meetingTime" label="会议时间" rules={[{ required: true }]}>
|
<Form.Item name="meetingTime" label="会议时间" rules={[{ required: true }]}>
|
||||||
<DatePicker showTime style={{ width: '100%' }} size="large" />
|
<DatePicker showTime style={{ width: "100%" }} size="large" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
@ -387,14 +468,14 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
<Row gutter={32}>
|
<Row gutter={32}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="participants" label="参会人员">
|
<Form.Item name="participants" label="参会人员">
|
||||||
<Select mode="multiple" placeholder="选择人员" showSearch optionFilterProp="children" size="large">
|
<Select mode="multiple" placeholder="选择参会人员" showSearch optionFilterProp="children" size="large">
|
||||||
{userList.map(u => (<Option key={u.userId} value={u.userId}><Space><Avatar size="small" icon={<UserOutlined />} />{u.displayName || u.username}</Space></Option>))}
|
{userList.map(u => (<Option key={u.userId} value={u.userId}><Space><Avatar size="small" icon={<UserOutlined />} />{u.displayName || u.username}</Space></Option>))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="hostUserId" label="会议主持人">
|
<Form.Item name="hostUserId" label="会议主持人">
|
||||||
<Select allowClear placeholder="不选择则默认为创建人" showSearch optionFilterProp="children" size="large">
|
<Select allowClear placeholder="默认为创建人" showSearch optionFilterProp="children" size="large">
|
||||||
{userList.map(u => (<Option key={u.userId} value={u.userId}><Space><Avatar size="small" icon={<UserOutlined />} />{u.displayName || u.username}</Space></Option>))}
|
{userList.map(u => (<Option key={u.userId} value={u.userId}><Space><Avatar size="small" icon={<UserOutlined />} />{u.displayName || u.username}</Space></Option>))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -409,21 +490,21 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<div style={{ margin: '32px 0', borderTop: '1px solid var(--app-border-color)' }} />
|
<div style={{ margin: "32px 0", borderTop: "1px solid var(--app-border-color)" }} />
|
||||||
|
|
||||||
<div style={{ marginBottom: 24, display: 'flex', alignItems: 'center' }}>
|
<div style={{ marginBottom: 24, display: "flex", alignItems: "center" }}>
|
||||||
<div style={{ width: 4, height: 16, background: '#1890ff', borderRadius: 2, marginRight: 8 }} />
|
<div style={{ width: 4, height: 16, background: "#1890ff", borderRadius: 2, marginRight: 8 }} />
|
||||||
<Title level={5} style={{ margin: 0 }}>AI 模型与分析配置</Title>
|
<Title level={5} style={{ margin: 0 }}>模型与纪要配置</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Row gutter={32}>
|
<Row gutter={32}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="asrModelId" label="语音识别 (ASR)" rules={[{ required: true }]}>
|
<Form.Item name="asrModelId" label="语音识别模型 (ASR)" rules={[{ required: true }]}>
|
||||||
<Select placeholder="选择 ASR 模型" size="large">{asrModels.map(m => (<Option key={m.id} value={m.id}>{m.modelName}</Option>))}</Select>
|
<Select placeholder="选择 ASR 模型" size="large">{asrModels.map(m => (<Option key={m.id} value={m.id}>{m.modelName}</Option>))}</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="summaryModelId" label="内容总结 (LLM)" rules={[{ required: true }]}>
|
<Form.Item name="summaryModelId" label="总结模型 (LLM)" rules={[{ required: true }]}>
|
||||||
<Select placeholder="选择总结模型" size="large">{llmModels.map(m => (<Option key={m.id} value={m.id}>{m.modelName}</Option>))}</Select>
|
<Select placeholder="选择总结模型" size="large">{llmModels.map(m => (<Option key={m.id} value={m.id}>{m.modelName}</Option>))}</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
@ -431,19 +512,19 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
|
|
||||||
<Form.Item name="promptId" label="总结模板" rules={[{ required: true }]}>
|
<Form.Item name="promptId" label="总结模板" rules={[{ required: true }]}>
|
||||||
{prompts.length > 15 ? (
|
{prompts.length > 15 ? (
|
||||||
<Select placeholder="请选择模板" showSearch optionFilterProp="children" size="large">
|
<Select placeholder="请选择总结模板" showSearch optionFilterProp="children" size="large">
|
||||||
{prompts.map(p => <Option key={p.id} value={p.id}>{p.templateName}</Option>)}
|
{prompts.map(p => <Option key={p.id} value={p.id}>{p.templateName}</Option>)}
|
||||||
</Select>
|
</Select>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: '2px' }}>
|
<div style={{ padding: "2px" }}>
|
||||||
<Row gutter={[12, 12]}>
|
<Row gutter={[12, 12]}>
|
||||||
{prompts.map(p => {
|
{prompts.map(p => {
|
||||||
const isSelected = watchedPromptId === p.id;
|
const isSelected = watchedPromptId === p.id;
|
||||||
return (
|
return (
|
||||||
<Col span={8} key={p.id}>
|
<Col span={8} key={p.id}>
|
||||||
<div onClick={() => form.setFieldsValue({ promptId: p.id })} style={{ padding: '12px 16px', borderRadius: 8, border: `1px solid ${isSelected ? '#1890ff' : 'var(--app-border-color)'}`, background: isSelected ? '#e6f7ff' : 'var(--app-bg-surface)', cursor: 'pointer', position: 'relative', transition: 'all 0.2s', display: 'flex', alignItems: 'center', height: '100%' }}>
|
<div onClick={() => form.setFieldsValue({ promptId: p.id })} style={{ padding: "12px 16px", borderRadius: 8, border: `1px solid ${isSelected ? "#1890ff" : "var(--app-border-color)"}`, background: isSelected ? "#e6f7ff" : "var(--app-bg-surface)", cursor: "pointer", position: "relative", transition: "all 0.2s", display: "flex", alignItems: "center", height: "100%" }}>
|
||||||
<div style={{ fontSize: '14px', color: isSelected ? '#1890ff' : 'var(--app-text-main)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: isSelected ? 500 : 400 }}>{p.templateName}</div>
|
<div style={{ fontSize: "14px", color: isSelected ? "#1890ff" : "var(--app-text-main)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontWeight: isSelected ? 500 : 400 }}>{p.templateName}</div>
|
||||||
{isSelected && <div style={{ position: 'absolute', top: -1, right: -1, width: 20, height: 20, background: '#1890ff', borderRadius: '0 8px 0 8px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><CheckOutlined style={{ color: '#fff', fontSize: 12 }} /></div>}
|
{isSelected && <div style={{ position: "absolute", top: -1, right: -1, width: 20, height: 20, background: "#1890ff", borderRadius: "0 8px 0 8px", display: "flex", alignItems: "center", justifyContent: "center" }}><CheckOutlined style={{ color: "#fff", fontSize: 12 }} /></div>}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
|
@ -453,83 +534,35 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{/*
|
<Row gutter={32}>
|
||||||
<Form.Item
|
<Col span={12}>
|
||||||
name="hotWordGroupId"
|
<Form.Item name="hotWordGroupId" label="热词组" tooltip={selectedPrompt?.hotWordGroupName ? `默认跟随模板:${selectedPrompt.hotWordGroupName}` : "模板未绑定热词组时可手动选择"} extra={watchedHotWordGroupId != null ? "创建会议时会优先使用这里选中的热词组" : undefined}>
|
||||||
label="热词组"
|
<Select placeholder={selectedPrompt?.hotWordGroupId ? "默认已带出模板热词组,可按需修改" : "请选择热词组"} size="large" options={[{ label: "不使用热词组", value: 0 }, ...hotWordGroups.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))]} />
|
||||||
tooltip={selectedPrompt?.hotWordGroupName ? `默认跟随模板:${selectedPrompt.hotWordGroupName}` : '模板未绑定热词组时可手动选择'}
|
|
||||||
extra={watchedHotWordGroupId != null ? '创建会议时会优先使用这里选中的热词组' : undefined}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder={selectedPrompt?.hotWordGroupId ? '默认已带出模板热词组,可按需修改' : '请选择热词组'}
|
|
||||||
size="large"
|
|
||||||
options={[
|
|
||||||
{ label: '不使用热词组', value: 0 },
|
|
||||||
...hotWordGroups.map((item) => ({
|
|
||||||
label: `${item.groupName} (${item.hotWordCount}/200)`,
|
|
||||||
value: item.id,
|
|
||||||
})),
|
|
||||||
]}
|
|
||||||
onChange={() => setHotWordGroupTouched(true)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
*/}
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
{/*
|
<Form.Item name="summaryDetailLevel" label="总结详细程度" rules={[{ required: true }]}>
|
||||||
|
<Radio.Group size="large">
|
||||||
<Form.Item
|
<Radio value="DETAILED">详细</Radio>
|
||||||
name="hotWordGroupId"
|
<Radio value="STANDARD">标准</Radio>
|
||||||
label="热词组"
|
<Radio value="BRIEF">简洁</Radio>
|
||||||
tooltip={selectedPrompt?.hotWordGroupName ? `默认跟随模板:${selectedPrompt.hotWordGroupName}` : '模板未绑定热词组时可手动选择'}
|
</Radio.Group>
|
||||||
extra={watchedHotWordGroupId != null ? '创建会议时会优先使用这里选中的热词组' : undefined}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder={selectedPrompt?.hotWordGroupId ? '默认已带出模板热词组,可按需修改' : '请选择热词组'}
|
|
||||||
size="large"
|
|
||||||
options={[
|
|
||||||
{ label: '不使用热词组', value: 0 },
|
|
||||||
...hotWordGroups.map((item) => ({
|
|
||||||
label: `${item.groupName} (${item.hotWordCount}/200)`,
|
|
||||||
value: item.id,
|
|
||||||
})),
|
|
||||||
]}
|
|
||||||
onChange={() => setHotWordGroupTouched(true)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="hotWordGroupId"
|
|
||||||
label={"\u70ed\u8bcd\u7ec4"}
|
|
||||||
tooltip={selectedPrompt?.hotWordGroupName ? `\u9ed8\u8ba4\u8ddf\u968f\u6a21\u677f\uff1a${selectedPrompt.hotWordGroupName}` : '\u6a21\u677f\u672a\u7ed1\u5b9a\u70ed\u8bcd\u7ec4\u65f6\u53ef\u624b\u52a8\u9009\u62e9'}
|
|
||||||
extra={watchedHotWordGroupId != null ? '\u521b\u5efa\u4f1a\u8bae\u65f6\u4f1a\u4f18\u5148\u4f7f\u7528\u8fd9\u91cc\u9009\u4e2d\u7684\u70ed\u8bcd\u7ec4' : undefined}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder={selectedPrompt?.hotWordGroupId ? '\u9ed8\u8ba4\u5df2\u5e26\u51fa\u6a21\u677f\u70ed\u8bcd\u7ec4\uff0c\u53ef\u6309\u9700\u4fee\u6539' : '\u8bf7\u9009\u62e9\u70ed\u8bcd\u7ec4'}
|
|
||||||
size="large"
|
|
||||||
options={[
|
|
||||||
{ label: '\u4e0d\u4f7f\u7528\u70ed\u8bcd\u7ec4', value: 0 },
|
|
||||||
...hotWordGroups.map((item) => ({
|
|
||||||
label: `${item.groupName} (${item.hotWordCount}/200)`,
|
|
||||||
value: item.id,
|
|
||||||
})),
|
|
||||||
]}
|
|
||||||
onChange={() => setHotWordGroupTouched(true)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Collapse
|
<Collapse
|
||||||
ghost
|
ghost
|
||||||
expandIconPosition="end"
|
expandIconPosition="end"
|
||||||
style={{ marginBottom: 24, background: 'var(--app-bg-surface)', border: '1px solid var(--app-border-color)', borderRadius: 8, overflow: 'hidden' }}
|
style={{ marginBottom: 24, background: "var(--app-bg-surface)", border: "1px solid var(--app-border-color)", borderRadius: 8, overflow: "hidden" }}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: 'advanced',
|
key: "advanced",
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
label: (
|
label: (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', width: '100%', height: '32px' }}>
|
<div style={{ display: "flex", alignItems: "center", width: "100%", height: "32px" }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', fontWeight: 600, color: 'var(--app-text-main)', fontSize: 15 }}>
|
<div style={{ display: "flex", alignItems: "center", fontWeight: 600, color: "var(--app-text-main)", fontSize: 15 }}>
|
||||||
<div style={{ width: 32, height: 32, borderRadius: 8, background: '#f0f5ff', color: '#1677ff', display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 12 }}>
|
<div style={{ width: 32, height: 32, borderRadius: 8, background: "#f0f5ff", color: "#1677ff", display: "flex", alignItems: "center", justifyContent: "center", marginRight: 12 }}>
|
||||||
<SettingOutlined style={{ fontSize: 16 }} />
|
<SettingOutlined style={{ fontSize: 16 }} />
|
||||||
</div>
|
</div>
|
||||||
高级设置
|
高级设置
|
||||||
|
|
@ -537,40 +570,38 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
children: (
|
children: (
|
||||||
<div style={{ paddingTop: 8, borderTop: '1px dashed var(--app-border-color)' }}>
|
<div style={{ paddingTop: 8, borderTop: "1px dashed var(--app-border-color)" }}>
|
||||||
<Row gutter={32}>
|
<Row gutter={32}>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item name="useSpkId" label={<span>声纹区分 <Tooltip title="开启后将尝试区分不同发言人"><QuestionCircleOutlined /></Tooltip></span>} valuePropName="checked" getValueProps={(v) => ({ checked: !!v })} normalize={(v) => (v ? 1 : 0)}>
|
<Form.Item name="useSpkId" label={<span>说话人区分 <Tooltip title="自动识别录音中的不同发言者并进行角色分离"><QuestionCircleOutlined /></Tooltip></span>} valuePropName="checked" getValueProps={(v) => ({ checked: !!v })} normalize={(v) => (v ? 1 : 0)}>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item name="enableTextRefine" label={<span>文本修正 <Tooltip title="开启后将尝试对识别文本进行修正"><QuestionCircleOutlined /></Tooltip></span>} valuePropName="checked">
|
<Form.Item name="enableTextRefine" label={<span>文本修正 <Tooltip title="自动识别并修正语音转写中的口语词、语气助词等"><QuestionCircleOutlined /></Tooltip></span>} valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
{type === 'realtime' && (
|
{type === "realtime" && (
|
||||||
|
<>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
|
<Form.Item name="saveAudio" label="保存录音" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
<Form.Item name="mode" label="识别模式">
|
<Form.Item name="mode" label="识别模式">
|
||||||
<Select size="large">
|
<Radio.Group>
|
||||||
<Option value="2pass">2pass (流式+离线修正)</Option>
|
<Radio value="2pass">2pass(流式 + 离线修正)</Radio>
|
||||||
<Option value="online">online (纯流式)</Option>
|
<Radio value="online">online(纯流式)</Radio>
|
||||||
</Select>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item
|
<Form.Item name="userPrompt" label="用户提示词" extra="提供补充信息以指导 AI 在生成会议总结时更侧重于特定方面或遵循特定风格">
|
||||||
name="userPrompt"
|
<Input.TextArea placeholder="请输入额外说明,例如:请重点关注会议中的待办事项、以摘要形式输出、或者采用严谨的公文风格等" autoSize={{ minRows: 3, maxRows: 6 }} showCount maxLength={1000} />
|
||||||
label="用户提示词"
|
|
||||||
extra="可选,用于补充本次会议总结的关注重点、表达偏好或输出侧重点"
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
|
||||||
placeholder="例如:请重点关注待办事项、风险点,并用适合汇报的表达方式输出"
|
|
||||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
|
||||||
showCount
|
|
||||||
maxLength={1000}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -578,20 +609,11 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{type === 'realtime' && (
|
{type === "upload" && (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="language" hidden><Input /></Form.Item>
|
<div style={{ margin: "32px 0", borderTop: "1px solid var(--app-border-color)" }} />
|
||||||
<Form.Item name="enablePunctuation" hidden valuePropName="checked"><Switch /></Form.Item>
|
<div style={{ marginBottom: 24, display: "flex", alignItems: "center" }}>
|
||||||
<Form.Item name="enableItn" hidden valuePropName="checked"><Switch /></Form.Item>
|
<div style={{ width: 4, height: 16, background: "#1890ff", borderRadius: 2, marginRight: 8 }} />
|
||||||
<Form.Item name="saveAudio" hidden valuePropName="checked"><Switch /></Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type === 'upload' && (
|
|
||||||
<>
|
|
||||||
<div style={{ margin: '32px 0', borderTop: '1px solid var(--app-border-color)' }} />
|
|
||||||
<div style={{ marginBottom: 24, display: 'flex', alignItems: 'center' }}>
|
|
||||||
<div style={{ width: 4, height: 16, background: '#1890ff', borderRadius: 2, marginRight: 8 }} />
|
|
||||||
<Title level={5} style={{ margin: 0 }}>上传录音文件</Title>
|
<Title level={5} style={{ margin: 0 }}>上传录音文件</Title>
|
||||||
</div>
|
</div>
|
||||||
<Dragger
|
<Dragger
|
||||||
|
|
@ -601,25 +623,22 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
customRequest={customUpload}
|
customRequest={customUpload}
|
||||||
onChange={info => setFileList(info.fileList.slice(-1))}
|
onChange={info => setFileList(info.fileList.slice(-1))}
|
||||||
maxCount={1}
|
maxCount={1}
|
||||||
style={{ borderRadius: 12, padding: '32px 0', background: 'var(--app-bg-surface)', border: '1px dashed var(--app-border-color)' }}
|
style={{ borderRadius: 12, padding: "32px 0", background: "var(--app-bg-surface)", border: "1px dashed var(--app-border-color)" }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="ant-upload-drag-icon" style={{ marginBottom: 16 }}><CloudUploadOutlined style={{ fontSize: 56, color: '#1890ff' }} /></p>
|
<p className="ant-upload-drag-icon" style={{ marginBottom: 16 }}><CloudUploadOutlined style={{ fontSize: 56, color: "#1890ff" }} /></p>
|
||||||
<p className="ant-upload-text" style={{ fontSize: 18, fontWeight: 500, color: 'var(--app-text-main)' }}>点击或拖拽录音文件到此处</p>
|
<p className="ant-upload-text" style={{ fontSize: 18, fontWeight: 500, color: "var(--app-text-main)" }}>点击或拖拽录音文件到此处</p>
|
||||||
<p className="ant-upload-hint" style={{ fontSize: 14, marginTop: 12, color: 'var(--app-text-secondary)' }}>支持 .mp3、.wav,以及 AAC 编码的 .m4a 音频</p>
|
<p className="ant-upload-hint" style={{ fontSize: 14, marginTop: 12, color: "var(--app-text-secondary)" }}>支持 mp3、wav、m4a 等格式,大小不超过 {createConfig.offlineAudioMaxSizeMb}MB</p>
|
||||||
<p className="ant-upload-hint" style={{ fontSize: 13, marginTop: 8, color: 'var(--app-text-secondary)' }}>文件大小不超过 {offlineAudioMaxSizeMb}MB,取值来自系统参数配置</p>
|
|
||||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||||
<div style={{ width: '60%', margin: '32px auto 0' }}>
|
<div style={{ width: "60%", margin: "32px auto 0" }}>
|
||||||
<Progress percent={uploadProgress} size="small" />
|
<Progress percent={uploadProgress} size="small" />
|
||||||
<div style={{ fontSize: 13, color: '#1890ff', marginTop: 8 }}>文件传输中,请稍候...</div>
|
<div style={{ fontSize: 13, color: "#1890ff", marginTop: 8 }}>文件上传中,请稍候...</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{audioUrl && (
|
{audioUrl && (
|
||||||
<Tag color="processing" style={{ marginTop: 24, padding: '6px 16px', fontSize: 14, borderRadius: 6, maxWidth: '90%', display: 'inline-flex', alignItems: 'center' }}>
|
<Tag color="processing" style={{ marginTop: 24, padding: "6px 16px", fontSize: 14, borderRadius: 6, maxWidth: "90%", display: "inline-flex", alignItems: "center" }}>
|
||||||
<span style={{ flexShrink: 0 }}>就绪:</span>
|
<span>已上传:</span>
|
||||||
<span style={{ marginLeft: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
<span style={{ marginLeft: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{audioUrl.split("/").pop()}</span>
|
||||||
{audioUrl.split('/').pop()}
|
|
||||||
</span>
|
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -628,6 +647,8 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -3,17 +3,23 @@ import { UserProfile } from "../types";
|
||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(() => localStorage.getItem("accessToken"));
|
const [accessToken, setAccessToken] = useState<string | null>(() => localStorage.getItem("accessToken"));
|
||||||
|
const [profileVersion, setProfileVersion] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => setAccessToken(localStorage.getItem("accessToken"));
|
const syncAuthState = () => setAccessToken(localStorage.getItem("accessToken"));
|
||||||
window.addEventListener("storage", handler);
|
const syncProfileState = () => setProfileVersion((value) => value + 1);
|
||||||
return () => window.removeEventListener("storage", handler);
|
window.addEventListener("storage", syncAuthState);
|
||||||
|
window.addEventListener("user-profile-updated", syncProfileState);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("storage", syncAuthState);
|
||||||
|
window.removeEventListener("user-profile-updated", syncProfileState);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const profile = useMemo<UserProfile | null>(() => {
|
const profile = useMemo<UserProfile | null>(() => {
|
||||||
const data = sessionStorage.getItem("userProfile");
|
const data = sessionStorage.getItem("userProfile");
|
||||||
return data ? JSON.parse(data) : null;
|
return data ? JSON.parse(data) : null;
|
||||||
}, [accessToken]);
|
}, [accessToken, profileVersion]);
|
||||||
|
|
||||||
const isAuthed = !!accessToken;
|
const isAuthed = !!accessToken;
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,13 @@ import {
|
||||||
listTenants,
|
listTenants,
|
||||||
listUsers,
|
listUsers,
|
||||||
pageRoles,
|
pageRoles,
|
||||||
saveRoleDataScope,
|
saveRoleAuthorization,
|
||||||
saveRolePermissions,
|
|
||||||
unbindUserFromRole,
|
unbindUserFromRole,
|
||||||
updateRole
|
updateRole
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
import PageContainer from "@/components/shared/PageContainer";
|
|
||||||
import { getStandardPagination } from "@/utils/pagination";
|
import { getStandardPagination } from "@/utils/pagination";
|
||||||
import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
|
import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
@ -192,6 +190,8 @@ export default function Roles() {
|
||||||
const [dataScopeType, setDataScopeType] = useState("SELF");
|
const [dataScopeType, setDataScopeType] = useState("SELF");
|
||||||
const [scopeOrgIds, setScopeOrgIds] = useState<number[]>([]);
|
const [scopeOrgIds, setScopeOrgIds] = useState<number[]>([]);
|
||||||
const [scopeOrgTree, setScopeOrgTree] = useState<DataNode[]>([]);
|
const [scopeOrgTree, setScopeOrgTree] = useState<DataNode[]>([]);
|
||||||
|
const [permissionsDirty, setPermissionsDirty] = useState(false);
|
||||||
|
const [dataScopeDirty, setDataScopeDirty] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const isPlatformMode = useMemo(() => {
|
const isPlatformMode = useMemo(() => {
|
||||||
|
|
@ -246,6 +246,8 @@ export default function Roles() {
|
||||||
setDataScopeType(dataScope?.scopeType || role.dataScopeType || "SELF");
|
setDataScopeType(dataScope?.scopeType || role.dataScopeType || "SELF");
|
||||||
setScopeOrgIds((dataScope?.orgIds || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)));
|
setScopeOrgIds((dataScope?.orgIds || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)));
|
||||||
setScopeOrgTree(toOrgTreeData(buildOrgTree(orgs || [])));
|
setScopeOrgTree(toOrgTreeData(buildOrgTree(orgs || [])));
|
||||||
|
setPermissionsDirty(false);
|
||||||
|
setDataScopeDirty(false);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingUsers(false);
|
setLoadingUsers(false);
|
||||||
}
|
}
|
||||||
|
|
@ -272,6 +274,8 @@ export default function Roles() {
|
||||||
setDataScopeType("SELF");
|
setDataScopeType("SELF");
|
||||||
setScopeOrgIds([]);
|
setScopeOrgIds([]);
|
||||||
setScopeOrgTree([]);
|
setScopeOrgTree([]);
|
||||||
|
setPermissionsDirty(false);
|
||||||
|
setDataScopeDirty(false);
|
||||||
} else if (!selectedRole) {
|
} else if (!selectedRole) {
|
||||||
await selectRole(roles[0], permissionList);
|
await selectRole(roles[0], permissionList);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -376,65 +380,57 @@ export default function Roles() {
|
||||||
setRolePage((prev) => ({ ...prev, current: page, size: pageSize }));
|
setRolePage((prev) => ({ ...prev, current: page, size: pageSize }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const savePermissions = async () => {
|
const saveAuthorizationChanges = async () => {
|
||||||
if (!selectedRole) return;
|
if (!selectedRole || (!permissionsDirty && !dataScopeDirty)) return;
|
||||||
setSaving(true);
|
if (dataScopeDirty && dataScopeType === "CUSTOM" && scopeOrgIds.length === 0) {
|
||||||
try {
|
|
||||||
await saveRolePermissions(selectedRole.roleId, Array.from(new Set([...selectedPermIds, ...halfCheckedIds])));
|
|
||||||
message.success("操作成功");
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveDataScope = async () => {
|
|
||||||
if (!selectedRole) return;
|
|
||||||
if (dataScopeType === "CUSTOM" && scopeOrgIds.length === 0) {
|
|
||||||
message.warning("请选择至少一个部门");
|
message.warning("请选择至少一个部门");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const payload: RoleDataScope = {
|
await saveRoleAuthorization(selectedRole.roleId, {
|
||||||
|
permissionIds: Array.from(new Set([...selectedPermIds, ...halfCheckedIds])),
|
||||||
|
dataScope: {
|
||||||
roleId: selectedRole.roleId,
|
roleId: selectedRole.roleId,
|
||||||
scopeType: dataScopeType,
|
scopeType: dataScopeType,
|
||||||
orgIds: dataScopeType === "CUSTOM" ? scopeOrgIds : []
|
orgIds: dataScopeType === "CUSTOM" ? scopeOrgIds : []
|
||||||
};
|
}
|
||||||
await saveRoleDataScope(selectedRole.roleId, payload);
|
});
|
||||||
message.success("操作成功");
|
setPermissionsDirty(false);
|
||||||
|
setDataScopeDirty(false);
|
||||||
setSelectedRole((prev) => (prev ? { ...prev, dataScopeType } : prev));
|
setSelectedRole((prev) => (prev ? { ...prev, dataScopeType } : prev));
|
||||||
setData((prev) => prev.map((item) => item.roleId === selectedRole.roleId ? { ...item, dataScopeType } : item));
|
setData((prev) => prev.map((item) => item.roleId === selectedRole.roleId ? { ...item, dataScopeType } : item));
|
||||||
|
await selectRole({ ...selectedRole, dataScopeType });
|
||||||
|
message.success("操作成功");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrimarySave = () => {
|
const handlePrimarySave = () => {
|
||||||
if (activeTab === "permissions") {
|
if (activeTab === "users") return;
|
||||||
void savePermissions();
|
void saveAuthorizationChanges();
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activeTab === "dataScope") {
|
|
||||||
void saveDataScope();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveDisabled = !selectedRole || activeTab === "users" || (activeTab === "permissions" && !can("sys:role:permission:save")) || (activeTab === "dataScope" && !can("sys:role:update"));
|
const needsSave = permissionsDirty || dataScopeDirty;
|
||||||
const saveLabel = activeTab === "dataScope" ? "保存数据权限" : "保存";
|
const saveDisabled = !selectedRole
|
||||||
|
|| activeTab === "users"
|
||||||
|
|| !needsSave
|
||||||
|
|| (permissionsDirty && !can("sys:role:permission:save"))
|
||||||
|
|| (dataScopeDirty && !can("sys:role:update"));
|
||||||
|
const saveLabel = "同步保存";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<div className="app-page roles-page-v2">
|
||||||
title="角色管理"
|
<PageHeader title="角色管理" subtitle="维护角色基础信息、功能权限、数据权限与成员绑定" />
|
||||||
subtitle="维护角色基础信息、功能权限、数据权限与成员绑定"
|
|
||||||
headerExtra={
|
<div className="app-page__page-actions">
|
||||||
can("sys:role:create") && (
|
{can("sys:role:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{"新增角色"}</Button>}
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
</div>
|
||||||
新增角色
|
|
||||||
</Button>
|
<div className="roles-layout">
|
||||||
)
|
<Row gutter={24} className="roles-layout__row">
|
||||||
}
|
|
||||||
>
|
|
||||||
<Row gutter={24} className="roles-layout__row" style={{ flex: 1, minHeight: 0 }}>
|
|
||||||
<Col span={7} className="roles-layout__side">
|
<Col span={7} className="roles-layout__side">
|
||||||
<Card title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>} bordered={false} className="app-page__panel-card roles-side-card">
|
<Card title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>} bordered={false} className="app-page__panel-card roles-side-card">
|
||||||
<div className="role-search-panel">
|
<div className="role-search-panel">
|
||||||
|
|
@ -526,6 +522,7 @@ export default function Roles() {
|
||||||
const halfChecked = info.halfCheckedKeys || [];
|
const halfChecked = info.halfCheckedKeys || [];
|
||||||
setSelectedPermIds(checked.map((key) => Number(key)));
|
setSelectedPermIds(checked.map((key) => Number(key)));
|
||||||
setHalfCheckedIds(halfChecked.map((key) => Number(key)));
|
setHalfCheckedIds(halfChecked.map((key) => Number(key)));
|
||||||
|
setPermissionsDirty(true);
|
||||||
}}
|
}}
|
||||||
defaultExpandAll
|
defaultExpandAll
|
||||||
/>
|
/>
|
||||||
|
|
@ -535,7 +532,15 @@ export default function Roles() {
|
||||||
<Tabs.TabPane tab={<Space><ApartmentOutlined />{"数据权限"}</Space>} key="dataScope">
|
<Tabs.TabPane tab={<Space><ApartmentOutlined />{"数据权限"}</Space>} key="dataScope">
|
||||||
<div className="role-detail-pane">
|
<div className="role-detail-pane">
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Radio.Group value={dataScopeType} onChange={(event) => setDataScopeType(event.target.value)} optionType="button" buttonStyle="solid">
|
<Radio.Group
|
||||||
|
value={dataScopeType}
|
||||||
|
onChange={(event) => {
|
||||||
|
setDataScopeType(event.target.value);
|
||||||
|
setDataScopeDirty(true);
|
||||||
|
}}
|
||||||
|
optionType="button"
|
||||||
|
buttonStyle="solid"
|
||||||
|
>
|
||||||
{DATA_SCOPE_OPTIONS.map((item) => (
|
{DATA_SCOPE_OPTIONS.map((item) => (
|
||||||
<Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button>
|
<Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button>
|
||||||
))}
|
))}
|
||||||
|
|
@ -552,6 +557,7 @@ export default function Roles() {
|
||||||
onCheck={(keys) => {
|
onCheck={(keys) => {
|
||||||
const checked = Array.isArray(keys) ? keys : keys.checked;
|
const checked = Array.isArray(keys) ? keys : keys.checked;
|
||||||
setScopeOrgIds(checked.map((key) => Number(key)));
|
setScopeOrgIds(checked.map((key) => Number(key)));
|
||||||
|
setDataScopeDirty(true);
|
||||||
}}
|
}}
|
||||||
defaultExpandAll
|
defaultExpandAll
|
||||||
/>
|
/>
|
||||||
|
|
@ -609,6 +615,7 @@ export default function Roles() {
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnClose>
|
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnClose>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
|
@ -636,6 +643,6 @@ export default function Roles() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</PageContainer>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Button, Card, Form, Input, Layout, Typography, message } from "antd";
|
||||||
import { LockOutlined, LogoutOutlined } from "@ant-design/icons";
|
import { LockOutlined, LogoutOutlined } from "@ant-design/icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { updateMyPassword } from "@/api";
|
import { getCurrentUser, updateMyPassword } from "@/api";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -18,9 +18,14 @@ export default function ResetPassword() {
|
||||||
const [form] = Form.useForm<ResetPasswordFormValues>();
|
const [form] = Form.useForm<ResetPasswordFormValues>();
|
||||||
|
|
||||||
const goToLogin = () => {
|
const goToLogin = () => {
|
||||||
localStorage.clear();
|
localStorage.removeItem("accessToken");
|
||||||
sessionStorage.clear();
|
localStorage.removeItem("refreshToken");
|
||||||
navigate("/login");
|
localStorage.removeItem("displayName");
|
||||||
|
localStorage.removeItem("username");
|
||||||
|
localStorage.removeItem("availableTenants");
|
||||||
|
localStorage.removeItem("activeTenantId");
|
||||||
|
sessionStorage.removeItem("userProfile");
|
||||||
|
navigate("/login", { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinish = async (values: ResetPasswordFormValues) => {
|
const onFinish = async (values: ResetPasswordFormValues) => {
|
||||||
|
|
@ -30,8 +35,11 @@ export default function ResetPassword() {
|
||||||
oldPassword: values.oldPassword,
|
oldPassword: values.oldPassword,
|
||||||
newPassword: values.newPassword
|
newPassword: values.newPassword
|
||||||
});
|
});
|
||||||
message.success("密码已更新,请重新登录");
|
const profile = await getCurrentUser();
|
||||||
goToLogin();
|
sessionStorage.setItem("userProfile", JSON.stringify(profile));
|
||||||
|
window.dispatchEvent(new Event("user-profile-updated"));
|
||||||
|
message.success("密码已更新");
|
||||||
|
navigate("/", { replace: true });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +53,7 @@ export default function ResetPassword() {
|
||||||
<Title level={3} style={{ marginTop: 16 }}>
|
<Title level={3} style={{ marginTop: 16 }}>
|
||||||
首次登录请修改密码
|
首次登录请修改密码
|
||||||
</Title>
|
</Title>
|
||||||
<Text type="secondary">当前账号被要求更新初始密码,提交成功后会跳转到登录页。</Text>
|
<Text type="secondary">当前账号需要先更新初始密码,提交成功后会直接进入系统。</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form form={form} layout="vertical" onFinish={onFinish}>
|
<Form form={form} layout="vertical" onFinish={onFinish}>
|
||||||
|
|
@ -77,7 +85,7 @@ export default function ResetPassword() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Button type="primary" htmlType="submit" block size="large" loading={loading} style={{ marginTop: 8 }}>
|
<Button type="primary" htmlType="submit" block size="large" loading={loading} style={{ marginTop: 8 }}>
|
||||||
提交并重新登录
|
提交并保持登录
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button type="link" block icon={<LogoutOutlined />} onClick={goToLogin} style={{ marginTop: 8 }}>
|
<Button type="link" block icon={<LogoutOutlined />} onClick={goToLogin} style={{ marginTop: 8 }}>
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ const AiModels: React.FC = () => {
|
||||||
isDefaultChecked: false,
|
isDefaultChecked: false,
|
||||||
statusChecked: true,
|
statusChecked: true,
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
temperature: 0.7,
|
temperature: 0.2,
|
||||||
topP: 0.9,
|
topP: 0.9,
|
||||||
apiPath: "/v1/chat/completions",
|
apiPath: "/v1/chat/completions",
|
||||||
svThreshold: 0.45,
|
svThreshold: 0.45,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Alert, Avatar, Button, Card, Col, Divider, Drawer, Empty, Form, Input, List, Modal, Popover, Progress, QRCode, Row, Select, Skeleton, Space, Switch, Tag, Typography, App, Dropdown } from 'antd';
|
import { Alert, Avatar, Button, Card, Col, Divider, Drawer, Empty, Form, Input, List, Modal, Popover, Progress, QRCode, Radio, Row, Select, Skeleton, Space, Switch, Tag, Typography, App, Dropdown } from 'antd';
|
||||||
import {
|
import {
|
||||||
AudioOutlined,
|
AudioOutlined,
|
||||||
CaretRightFilled,
|
CaretRightFilled,
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
CheckCircleFilled,
|
CheckCircleFilled,
|
||||||
FilePdfOutlined,
|
FilePdfOutlined,
|
||||||
FileWordOutlined,
|
FileWordOutlined,
|
||||||
ShareAltOutlined,
|
ShareAltOutlined, InfoCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
@ -34,6 +34,7 @@ import {
|
||||||
getTranscripts,
|
getTranscripts,
|
||||||
MeetingChapterVO,
|
MeetingChapterVO,
|
||||||
MeetingProgress,
|
MeetingProgress,
|
||||||
|
SummaryDetailLevel,
|
||||||
MeetingTranscriptVO,
|
MeetingTranscriptVO,
|
||||||
MeetingVO,
|
MeetingVO,
|
||||||
reSummary,
|
reSummary,
|
||||||
|
|
@ -52,7 +53,7 @@ import { listUsers } from '../../api';
|
||||||
import { useDict } from '../../hooks/useDict';
|
import { useDict } from '../../hooks/useDict';
|
||||||
import { SysUser } from '../../types';
|
import { SysUser } from '../../types';
|
||||||
import PageHeader from '../../components/shared/PageHeader';
|
import PageHeader from '../../components/shared/PageHeader';
|
||||||
import PageContainer from "@/components/shared/PageContainer";
|
import PageContainer from "../../components/shared/PageContainer";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
@ -615,7 +616,7 @@ const ActiveTranscriptRow = React.memo<ActiveTranscriptRowProps>(({
|
||||||
highlightKeyword = '',
|
highlightKeyword = '',
|
||||||
}) => {
|
}) => {
|
||||||
const [draftValue, setDraftValue] = useState(item.content);
|
const [draftValue, setDraftValue] = useState(item.content);
|
||||||
const rowRef = useRef<HTMLDivElement>(null);
|
const rowRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
|
|
@ -751,7 +752,7 @@ const MeetingDetail: React.FC = () => {
|
||||||
const [savingTranscriptId, setSavingTranscriptId] = useState<number | null>(null);
|
const [savingTranscriptId, setSavingTranscriptId] = useState<number | null>(null);
|
||||||
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
||||||
const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
|
const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
|
||||||
const [, setUserList] = useState<SysUser[]>([]);
|
const [userList, setUserList] = useState<SysUser[]>([]);
|
||||||
const { items: speakerLabels } = useDict('biz_speaker_label');
|
const { items: speakerLabels } = useDict('biz_speaker_label');
|
||||||
const [sharePopoverOpen, setSharePopoverOpen] = useState(false);
|
const [sharePopoverOpen, setSharePopoverOpen] = useState(false);
|
||||||
const [shareSaving, setShareSaving] = useState(false);
|
const [shareSaving, setShareSaving] = useState(false);
|
||||||
|
|
@ -839,24 +840,24 @@ const MeetingDetail: React.FC = () => {
|
||||||
);
|
);
|
||||||
const catalogChapterLinks = useMemo<ChapterTranscriptLink[]>(() => {
|
const catalogChapterLinks = useMemo<ChapterTranscriptLink[]>(() => {
|
||||||
const transcriptIdToIndex = new Map(transcripts.map((item, index) => [item.id, index]));
|
const transcriptIdToIndex = new Map(transcripts.map((item, index) => [item.id, index]));
|
||||||
const sourceChapters = meetingChapters.length
|
const sourceChapters: MeetingChapterVO[] = meetingChapters.length
|
||||||
? meetingChapters
|
? meetingChapters
|
||||||
: analysis.chapters.map((item) => ({
|
: analysis.chapters.map((item) => ({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
time: item.time,
|
time: item.time,
|
||||||
}));
|
} as MeetingChapterVO));
|
||||||
|
|
||||||
return sourceChapters.map((chapter, index) => {
|
return sourceChapters.map((chapter, index) => {
|
||||||
let matchedTranscripts: MeetingTranscriptVO[] = [];
|
let matchedTranscripts: MeetingTranscriptVO[] = [];
|
||||||
const sourceTranscriptIds = Array.isArray(chapter.sourceTranscriptIds)
|
const sourceTranscriptIds = Array.isArray(chapter.sourceTranscriptIds)
|
||||||
? chapter.sourceTranscriptIds
|
? chapter.sourceTranscriptIds
|
||||||
.map((item) => Number(item))
|
.map((item: number) => Number(item))
|
||||||
.filter((item) => Number.isFinite(item) && transcriptIdToIndex.has(item))
|
.filter((item: number) => Number.isFinite(item) && transcriptIdToIndex.has(item))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if (sourceTranscriptIds.length) {
|
if (sourceTranscriptIds.length) {
|
||||||
matchedTranscripts = sourceTranscriptIds
|
matchedTranscripts = sourceTranscriptIds
|
||||||
.map((item) => transcripts[transcriptIdToIndex.get(item)!])
|
.map((item: number) => transcripts[transcriptIdToIndex.get(item)!])
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
} else if (chapter.startTranscriptId && chapter.endTranscriptId) {
|
} else if (chapter.startTranscriptId && chapter.endTranscriptId) {
|
||||||
const startIndex = transcriptIdToIndex.get(Number(chapter.startTranscriptId));
|
const startIndex = transcriptIdToIndex.get(Number(chapter.startTranscriptId));
|
||||||
|
|
@ -868,7 +869,7 @@ const MeetingDetail: React.FC = () => {
|
||||||
const startMs = typeof chapter.startTime === 'number' ? chapter.startTime : parseChapterTimeToMs(chapter.time);
|
const startMs = typeof chapter.startTime === 'number' ? chapter.startTime : parseChapterTimeToMs(chapter.time);
|
||||||
const nextChapterStartMs = sourceChapters
|
const nextChapterStartMs = sourceChapters
|
||||||
.slice(index + 1)
|
.slice(index + 1)
|
||||||
.map((item) => (typeof item.startTime === 'number' ? item.startTime : parseChapterTimeToMs(item.time)))
|
.map((item: MeetingChapterVO) => (typeof item.startTime === 'number' ? item.startTime : parseChapterTimeToMs(item.time)))
|
||||||
.find((item): item is number => item !== null && startMs !== null && item > startMs);
|
.find((item): item is number => item !== null && startMs !== null && item > startMs);
|
||||||
|
|
||||||
if (startMs !== null) {
|
if (startMs !== null) {
|
||||||
|
|
@ -946,7 +947,12 @@ const MeetingDetail: React.FC = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return {
|
||||||
|
key: 'general-failure',
|
||||||
|
title: '会议处理异常',
|
||||||
|
description: '会议在处理过程中遇到了问题。您可以尝试重新发起识别或总结。',
|
||||||
|
hasFallbackContent,
|
||||||
|
};
|
||||||
}, [meeting, meetingChapters.length]);
|
}, [meeting, meetingChapters.length]);
|
||||||
const emptyTranscriptFailureNotice = useMemo(() => {
|
const emptyTranscriptFailureNotice = useMemo(() => {
|
||||||
if (!meeting || meeting.status !== 4 || transcripts.length > 0) {
|
if (!meeting || meeting.status !== 4 || transcripts.length > 0) {
|
||||||
|
|
@ -962,24 +968,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
};
|
};
|
||||||
}, [canRetryTranscription, meeting, transcripts.length]);
|
}, [canRetryTranscription, meeting, transcripts.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!generationFailureNotice) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const acknowledgedKey = `meeting-failure-ack:${generationFailureNotice.key}`;
|
|
||||||
if (sessionStorage.getItem(acknowledgedKey) === '1') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Modal.warning({
|
|
||||||
title: generationFailureNotice.title,
|
|
||||||
content: generationFailureNotice.description,
|
|
||||||
okText: '我知道了',
|
|
||||||
onOk: () => {
|
|
||||||
sessionStorage.setItem(acknowledgedKey, '1');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [generationFailureNotice]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!playbackAudioUrl) {
|
if (!playbackAudioUrl) {
|
||||||
setShowFloatingTranscriptPlayer(false);
|
setShowFloatingTranscriptPlayer(false);
|
||||||
|
|
@ -1174,6 +1162,7 @@ const MeetingDetail: React.FC = () => {
|
||||||
summaryModelId: values.summaryModelId,
|
summaryModelId: values.summaryModelId,
|
||||||
promptId: values.promptId,
|
promptId: values.promptId,
|
||||||
userPrompt: values.userPrompt,
|
userPrompt: values.userPrompt,
|
||||||
|
summaryDetailLevel: values.summaryDetailLevel as SummaryDetailLevel,
|
||||||
});
|
});
|
||||||
message.success('已重新发起总结任务');
|
message.success('已重新发起总结任务');
|
||||||
setSummaryVisible(false);
|
setSummaryVisible(false);
|
||||||
|
|
@ -1193,6 +1182,10 @@ const MeetingDetail: React.FC = () => {
|
||||||
llmModels[0]?.id,
|
llmModels[0]?.id,
|
||||||
promptId: summaryForm.getFieldValue('promptId') ?? prompts[0]?.id,
|
promptId: summaryForm.getFieldValue('promptId') ?? prompts[0]?.id,
|
||||||
userPrompt: meeting?.lastUserPrompt ?? '',
|
userPrompt: meeting?.lastUserPrompt ?? '',
|
||||||
|
summaryDetailLevel:
|
||||||
|
summaryForm.getFieldValue('summaryDetailLevel') ??
|
||||||
|
meeting?.summaryDetailLevel ??
|
||||||
|
'STANDARD',
|
||||||
});
|
});
|
||||||
setSummaryVisible(true);
|
setSummaryVisible(true);
|
||||||
};
|
};
|
||||||
|
|
@ -1860,50 +1853,35 @@ const MeetingDetail: React.FC = () => {
|
||||||
<Row gutter={24} style={{ height: '100%' }}>
|
<Row gutter={24} style={{ height: '100%' }}>
|
||||||
<Col xs={24} xl={13} style={{ height: '100%' }}>
|
<Col xs={24} xl={13} style={{ height: '100%' }}>
|
||||||
<div className="detail-side-column detail-left-column">
|
<div className="detail-side-column detail-left-column">
|
||||||
<Card className="left-flow-card summary-panel" variant="borderless">
|
{(generationFailureNotice || emptyTranscriptFailureNotice || meeting.audioSaveStatus === 'FAILED') && (
|
||||||
<div className="summary-head">
|
|
||||||
<div className="summary-title">
|
|
||||||
<RobotOutlined style={{ color: '#5f51ff', fontSize: 20 }} />
|
|
||||||
<span>AI 智能总结</span>
|
|
||||||
</div>
|
|
||||||
<div className="summary-head-actions">
|
|
||||||
{meeting.summaryContent && (
|
|
||||||
<span className="summary-head-link summary-head-link--static">
|
|
||||||
<ClockCircleOutlined /> 总结记录
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{meeting.summaryContent && isOwner && (
|
|
||||||
<span
|
|
||||||
className="summary-head-link"
|
|
||||||
onClick={() => {
|
|
||||||
if (isEditingSummary) {
|
|
||||||
handleSaveSummary();
|
|
||||||
} else {
|
|
||||||
setSummaryDraft(meeting.summaryContent || '');
|
|
||||||
setIsEditingSummary(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isEditingSummary ? <><CheckCircleFilled /> 保存</> : <><EditOutlined /> 编辑</>}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isEditingSummary && (
|
|
||||||
<span className="summary-head-link" onClick={() => setIsEditingSummary(false)}>
|
|
||||||
取消
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{generationFailureNotice && (
|
|
||||||
<Alert
|
<Alert
|
||||||
type="warning"
|
type="warning"
|
||||||
showIcon
|
showIcon
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16, borderRadius: 12 }}
|
||||||
message={generationFailureNotice.title}
|
message={
|
||||||
description={generationFailureNotice.description}
|
<div style={{ fontWeight: 600 }}>
|
||||||
|
{generationFailureNotice?.title || emptyTranscriptFailureNotice?.title || '会议处理提醒'}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<div style={{ fontSize: 13, marginTop: 4 }}>
|
||||||
|
<div>{generationFailureNotice?.description || emptyTranscriptFailureNotice?.description}</div>
|
||||||
|
{meeting.audioSaveStatus === 'FAILED' && (
|
||||||
|
<div style={{ marginTop: 4, color: '#d48806' }}>
|
||||||
|
<InfoCircleOutlined style={{ marginRight: 4 }} />
|
||||||
|
{meeting.audioSaveMessage || '录音保存失败,当前无法播放'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{canRetryTranscription && (
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
{emptyTranscriptFailureNotice?.hint}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Card className="left-flow-card summary-panel" variant="borderless">
|
||||||
|
|
||||||
{meeting.status === 2 ? (
|
{meeting.status === 2 ? (
|
||||||
<div className="summary-progress-shell">
|
<div className="summary-progress-shell">
|
||||||
|
|
@ -2029,14 +2007,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
<div className="empty-transcript-inline-note__text">{emptyTranscriptFailureNotice.description}</div>
|
<div className="empty-transcript-inline-note__text">{emptyTranscriptFailureNotice.description}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{meeting.audioSaveStatus === 'FAILED' && (
|
|
||||||
<Alert
|
|
||||||
type="warning"
|
|
||||||
showIcon
|
|
||||||
style={{ marginBottom: 16 }}
|
|
||||||
message={meeting.audioSaveMessage || '实时会议已完成,但音频保存失败,当前无法播放会议录音。转写和总结不受影响。'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<List
|
<List
|
||||||
className="transcript-list"
|
className="transcript-list"
|
||||||
dataSource={transcripts}
|
dataSource={transcripts}
|
||||||
|
|
@ -2160,14 +2130,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
<div className="empty-transcript-inline-note__text">{emptyTranscriptFailureNotice.description}</div>
|
<div className="empty-transcript-inline-note__text">{emptyTranscriptFailureNotice.description}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{meeting.audioSaveStatus === 'FAILED' && (
|
|
||||||
<Alert
|
|
||||||
type="warning"
|
|
||||||
showIcon
|
|
||||||
style={{ margin: '0 18px 16px' }}
|
|
||||||
message={meeting.audioSaveMessage || '实时会议已完成,但音频保存失败,当前无法播放会议录音。转写和总结不受影响。'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<List
|
<List
|
||||||
className="transcript-list"
|
className="transcript-list"
|
||||||
dataSource={transcripts}
|
dataSource={transcripts}
|
||||||
|
|
@ -3560,7 +3522,18 @@ const MeetingDetail: React.FC = () => {
|
||||||
<Form.Item name="tags" label="业务标签">
|
<Form.Item name="tags" label="业务标签">
|
||||||
<Select mode="tags" placeholder="输入标签后回车" />
|
<Select mode="tags" placeholder="输入标签后回车" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Text type="warning">参会人员 ID 当前不支持在这里直接修改,如需调整请联系管理员。</Text>
|
<Form.Item name="participantIds" label="参会人员">
|
||||||
|
<Select mode="multiple" placeholder="请选择参会人" showSearch optionFilterProp="children">
|
||||||
|
{userList.map((u) => (
|
||||||
|
<Option key={u.userId} value={u.userId}>
|
||||||
|
<Space>
|
||||||
|
<Avatar size="small" icon={<UserOutlined />} />
|
||||||
|
{u.displayName || u.username}
|
||||||
|
</Space>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
@ -3647,6 +3620,13 @@ const MeetingDetail: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="summaryDetailLevel" label="总结详细程度" rules={[{ required: true }]}>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value="DETAILED">详细</Radio>
|
||||||
|
<Radio value="STANDARD">标准</Radio>
|
||||||
|
<Radio value="BRIEF">简洁</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="userPrompt"
|
name="userPrompt"
|
||||||
label="用户提示词"
|
label="用户提示词"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,7 +8,7 @@ import { Button, Empty, Skeleton, Tag, Typography } from "antd";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { getRecentTasks } from "@/api/business/dashboard";
|
import { getRecentTasks } from "@/api/business/dashboard";
|
||||||
import type { MeetingVO } from "@/api/business/meeting";
|
import { getMeetingCreateConfig, type MeetingCreateConfig, type MeetingVO } from "@/api/business/meeting";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
import RightVisual from "./RightVisual";
|
import RightVisual from "./RightVisual";
|
||||||
import { MeetingCreateDrawer, MeetingCreateType } from "@/components/business/MeetingCreateDrawer";
|
import { MeetingCreateDrawer, MeetingCreateType } from "@/components/business/MeetingCreateDrawer";
|
||||||
|
|
@ -76,6 +76,12 @@ export default function HomePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [recentTasks, setRecentTasks] = useState<MeetingVO[]>([]);
|
const [recentTasks, setRecentTasks] = useState<MeetingVO[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [configLoaded, setConfigLoaded] = useState(false);
|
||||||
|
const [createConfig, setCreateConfig] = useState<MeetingCreateConfig>({
|
||||||
|
offlineEnabled: false,
|
||||||
|
realtimeEnabled: false,
|
||||||
|
offlineAudioMaxSizeMb: 1024,
|
||||||
|
});
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [drawerType, setDrawerType] = useState<MeetingCreateType>('realtime');
|
const [drawerType, setDrawerType] = useState<MeetingCreateType>('realtime');
|
||||||
const [readCardIds, setReadCardIds] = useState<string[]>(() => {
|
const [readCardIds, setReadCardIds] = useState<string[]>(() => {
|
||||||
|
|
@ -108,11 +114,21 @@ export default function HomePage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRecentTasks = async () => {
|
const fetchRecentTasks = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await getRecentTasks();
|
const [response, configResponse] = await Promise.all([
|
||||||
|
getRecentTasks(),
|
||||||
|
getMeetingCreateConfig(),
|
||||||
|
]);
|
||||||
const payload: any = (response as any).data || response;
|
const payload: any = (response as any).data || response;
|
||||||
setRecentTasks(payload?.data || payload || []);
|
setRecentTasks(payload?.data || payload || []);
|
||||||
|
setCreateConfig(configResponse.data.data || {
|
||||||
|
offlineEnabled: true,
|
||||||
|
realtimeEnabled: true,
|
||||||
|
offlineAudioMaxSizeMb: 1024,
|
||||||
|
});
|
||||||
|
setConfigLoaded(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Home recent tasks load failed", error);
|
console.error("Home recent tasks load failed", error);
|
||||||
|
setConfigLoaded(true);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -129,9 +145,10 @@ export default function HomePage() {
|
||||||
window.localStorage.setItem(RECENT_CARD_READ_STORAGE_KEY, JSON.stringify(readCardIds));
|
window.localStorage.setItem(RECENT_CARD_READ_STORAGE_KEY, JSON.stringify(readCardIds));
|
||||||
}, [readCardIds]);
|
}, [readCardIds]);
|
||||||
|
|
||||||
const quickEntries = useMemo<QuickEntry[]>(
|
const quickEntries = useMemo<(QuickEntry & { key: string })[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
|
key: "realtime",
|
||||||
title: "开启实时记录",
|
title: "开启实时记录",
|
||||||
icon: <AudioOutlined />,
|
icon: <AudioOutlined />,
|
||||||
description: ["实时语音转文字", "同步翻译,智能总结要点"],
|
description: ["实时语音转文字", "同步翻译,智能总结要点"],
|
||||||
|
|
@ -143,6 +160,7 @@ export default function HomePage() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "offline",
|
||||||
title: "上传音视频",
|
title: "上传音视频",
|
||||||
icon: <VideoCameraAddOutlined />,
|
icon: <VideoCameraAddOutlined />,
|
||||||
description: ["音视频转文字", "区分发言人,一键导出"],
|
description: ["音视频转文字", "区分发言人,一键导出"],
|
||||||
|
|
@ -157,7 +175,13 @@ export default function HomePage() {
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const visibleQuickEntries = useMemo(
|
||||||
|
() => quickEntries.filter((item) => (item.key === "realtime" ? createConfig.realtimeEnabled : createConfig.offlineEnabled)),
|
||||||
|
[quickEntries, createConfig]
|
||||||
|
);
|
||||||
|
|
||||||
const recentCards = useMemo(() => buildRecentCards(recentTasks), [recentTasks]);
|
const recentCards = useMemo(() => buildRecentCards(recentTasks), [recentTasks]);
|
||||||
|
const recentTaskMap = useMemo(() => new Map(recentTasks.map((item) => [String(item.id), item])), [recentTasks]);
|
||||||
|
|
||||||
const handleRecentCardClick = (card: RecentCard) => {
|
const handleRecentCardClick = (card: RecentCard) => {
|
||||||
const cardId = String(card.id);
|
const cardId = String(card.id);
|
||||||
|
|
@ -195,7 +219,8 @@ export default function HomePage() {
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<div className="home-quick-actions">
|
<div className="home-quick-actions">
|
||||||
{quickEntries.map((entry) => (
|
{configLoaded ? (
|
||||||
|
visibleQuickEntries.length > 0 ? visibleQuickEntries.map((entry) => (
|
||||||
<div
|
<div
|
||||||
className={`home-action-item home-action-item--${entry.accent}`}
|
className={`home-action-item home-action-item--${entry.accent}`}
|
||||||
onClick={entry.onClick}
|
onClick={entry.onClick}
|
||||||
|
|
@ -216,7 +241,28 @@ export default function HomePage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)) : (
|
||||||
|
<div className="home-action-item home-action-item--cyan" style={{ cursor: "default", opacity: 0.7 }}>
|
||||||
|
<div className="home-action-badge">系统参数</div>
|
||||||
|
<div className="home-action-icon-wrapper">
|
||||||
|
<div className="home-action-icon"><AudioOutlined /></div>
|
||||||
|
<div className="home-action-icon-glow" />
|
||||||
|
<div className="home-action-icon-circle" />
|
||||||
|
</div>
|
||||||
|
<Title level={3} className="home-action-title">会议创建已关闭</Title>
|
||||||
|
<div className="home-action-desc">
|
||||||
|
<Text className="home-action-line">当前系统参数已关闭离线上传和实时会议入口</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
// Loading state for quick actions to prevent jump
|
||||||
|
[...Array(2)].map((_, i) => (
|
||||||
|
<div key={i} className="home-action-item home-action-item--loading" style={{ background: 'rgba(255,255,255,0.4)', border: '1px dashed rgba(0,0,0,0.05)' }}>
|
||||||
|
<Skeleton active avatar paragraph={{ rows: 2 }} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -260,6 +306,11 @@ export default function HomePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="home-recent-card-tags">
|
<div className="home-recent-card-tags">
|
||||||
|
{recentTaskMap.get(String(card.id))?.meetingSource && (
|
||||||
|
<Tag key={`${card.id}-source`} className="home-recent-card-tag" bordered={false}>
|
||||||
|
{recentTaskMap.get(String(card.id))?.meetingSource === "ANDROID" ? "安卓端" : "Web端"}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
{card.tags.slice(0, 4).map((tag) => (
|
{card.tags.slice(0, 4).map((tag) => (
|
||||||
<Tag key={`${card.id}-${tag}`} className="home-recent-card-tag" bordered={false}>
|
<Tag key={`${card.id}-${tag}`} className="home-recent-card-tag" bordered={false}>
|
||||||
{tag}
|
{tag}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { generateMyBotCredential, getCurrentUser, getMyBotCredential, updateMyPassword, updateMyProfile, uploadPlatformAsset } from "@/api";
|
import { generateMyBotCredential, getCurrentUser, getMyBotCredential, updateMyPassword, updateMyProfile, uploadPlatformAsset } from "@/api";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
import PageContainer from "@/components/shared/PageContainer";
|
|
||||||
import type { BotCredential, UserProfile } from "@/types";
|
import type { BotCredential, UserProfile } from "@/types";
|
||||||
import AvatarCropDialog, { type CropModalState } from "./AvatarCropDialog";
|
import AvatarCropDialog, { type CropModalState } from "./AvatarCropDialog";
|
||||||
|
|
||||||
|
|
@ -106,6 +105,10 @@ export default function Profile() {
|
||||||
const values = await pwdForm.validateFields();
|
const values = await pwdForm.validateFields();
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
await updateMyPassword(values);
|
await updateMyPassword(values);
|
||||||
|
const profile = await getCurrentUser();
|
||||||
|
setUser(profile);
|
||||||
|
sessionStorage.setItem("userProfile", JSON.stringify(profile));
|
||||||
|
window.dispatchEvent(new Event("user-profile-updated"));
|
||||||
message.success(t("common.success"));
|
message.success(t("common.success"));
|
||||||
pwdForm.resetFields();
|
pwdForm.resetFields();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -129,11 +132,9 @@ export default function Profile() {
|
||||||
const avatarUrl = avatarUrlValue?.trim() || undefined;
|
const avatarUrl = avatarUrlValue?.trim() || undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<div className="app-page app-page--contained" style={{ maxWidth: 1024, width: "100%", margin: "0 auto" }}>
|
||||||
title={t("profile.title")}
|
<PageHeader title={t("profile.title")} subtitle={t("profile.subtitle")} />
|
||||||
subtitle={t("profile.subtitle")}
|
|
||||||
style={{ maxWidth: 1024, width: "100%", margin: "0 auto" }}
|
|
||||||
>
|
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col xs={24} lg={8}>
|
<Col xs={24} lg={8}>
|
||||||
<Card className="app-page__content-card text-center" loading={loading}>
|
<Card className="app-page__content-card text-center" loading={loading}>
|
||||||
|
|
@ -307,8 +308,7 @@ export default function Profile() {
|
||||||
onCancel={() => setCropState((prev) => ({ ...prev, open: false, src: "" }))}
|
onCancel={() => setCropState((prev) => ({ ...prev, open: false, src: "" }))}
|
||||||
onConfirm={handleUploadCroppedImage}
|
onConfirm={handleUploadCroppedImage}
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue