diff --git a/backend/src/main/java/com/imeeting/common/SysParamKeys.java b/backend/src/main/java/com/imeeting/common/SysParamKeys.java index 3617864..b38b01a 100644 --- a/backend/src/main/java/com/imeeting/common/SysParamKeys.java +++ b/backend/src/main/java/com/imeeting/common/SysParamKeys.java @@ -4,4 +4,5 @@ public final class SysParamKeys { private SysParamKeys() {} public static final String CAPTCHA_ENABLED = "security.captcha.enabled"; + public static final String MEETING_SUMMARY_SYSTEM_PROMPT = "meeting.summary.system_prompt"; } diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java index 6ce8af2..d0b0b85 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java @@ -356,13 +356,13 @@ public class MeetingController { @PostMapping("/{id}/summary/regenerate") @PreAuthorize("isAuthenticated()") - public ApiResponse reSummary(@PathVariable Long id, @RequestBody MeetingResummaryDTO dto) { + public ApiResponse reSummary(@PathVariable Long id, @Valid @RequestBody MeetingResummaryDTO dto) { LoginUser loginUser = currentLoginUser(); Meeting meeting = meetingAccessService.requireMeeting(id); meetingAccessService.assertCanEditMeeting(meeting, loginUser); dto.setMeetingId(id); assertPromptAvailable(dto.getPromptId(), loginUser); - meetingCommandService.reSummary(dto.getMeetingId(), dto.getSummaryModelId(), dto.getPromptId()); + meetingCommandService.reSummary(dto.getMeetingId(), dto.getSummaryModelId(), dto.getPromptId(), dto.getUserPrompt()); return ApiResponse.ok(true); } diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingPublicPreviewController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingPublicPreviewController.java index ea7da83..59ed8a7 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingPublicPreviewController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingPublicPreviewController.java @@ -28,7 +28,7 @@ public class MeetingPublicPreviewController { @GetMapping("/{id}/preview/access") public ApiResponse getPreviewAccess(@PathVariable Long id) { try { - Meeting meeting = meetingAccessService.requireMeeting(id); + Meeting meeting = meetingAccessService.requireMeetingIgnoreTenant(id); return ApiResponse.ok(new MeetingPreviewAccessVO(meetingAccessService.isPreviewPasswordRequired(meeting))); } catch (RuntimeException ex) { return ApiResponse.error(ex.getMessage()); @@ -39,11 +39,11 @@ public class MeetingPublicPreviewController { public ApiResponse getPreview(@PathVariable Long id, @RequestParam(required = false) String accessPassword) { try { - Meeting meeting = meetingAccessService.requireMeeting(id); + Meeting meeting = meetingAccessService.requireMeetingIgnoreTenant(id); meetingAccessService.assertCanPreviewMeeting(meeting, accessPassword); PublicMeetingPreviewVO data = new PublicMeetingPreviewVO(); - data.setMeeting(meetingQueryService.getDetail(id)); + data.setMeeting(meetingQueryService.getDetailIgnoreTenant(id)); if (data.getMeeting() != null) { data.getMeeting().setAccessPassword(null); } diff --git a/backend/src/main/java/com/imeeting/dto/biz/CreateMeetingCommand.java b/backend/src/main/java/com/imeeting/dto/biz/CreateMeetingCommand.java index ec0f66d..b8b4f5a 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/CreateMeetingCommand.java +++ b/backend/src/main/java/com/imeeting/dto/biz/CreateMeetingCommand.java @@ -3,6 +3,7 @@ package com.imeeting.dto.biz; import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Data; import java.time.LocalDateTime; @@ -34,6 +35,9 @@ public class CreateMeetingCommand { @NotNull(message = "promptId must not be null") private Long promptId; + @Size(max = 2000, message = "userPrompt length must be <= 2000") + private String userPrompt; + private Integer useSpkId; private Boolean enableTextRefine; private List hotWords; diff --git a/backend/src/main/java/com/imeeting/dto/biz/CreateRealtimeMeetingCommand.java b/backend/src/main/java/com/imeeting/dto/biz/CreateRealtimeMeetingCommand.java index d6b764a..844d17f 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/CreateRealtimeMeetingCommand.java +++ b/backend/src/main/java/com/imeeting/dto/biz/CreateRealtimeMeetingCommand.java @@ -3,6 +3,7 @@ package com.imeeting.dto.biz; import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Data; import java.time.LocalDateTime; @@ -31,6 +32,9 @@ public class CreateRealtimeMeetingCommand { @NotNull(message = "promptId must not be null") private Long promptId; + @Size(max = 2000, message = "userPrompt length must be <= 2000") + private String userPrompt; + private String mode; private String language; private Integer useSpkId; diff --git a/backend/src/main/java/com/imeeting/dto/biz/MeetingResummaryDTO.java b/backend/src/main/java/com/imeeting/dto/biz/MeetingResummaryDTO.java index f9b286f..1554182 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/MeetingResummaryDTO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/MeetingResummaryDTO.java @@ -1,10 +1,14 @@ package com.imeeting.dto.biz; import lombok.Data; +import jakarta.validation.constraints.Size; @Data public class MeetingResummaryDTO { private Long meetingId; private Long summaryModelId; private Long promptId; -} \ No newline at end of file + + @Size(max = 2000, message = "userPrompt length must be <= 2000") + private String userPrompt; +} diff --git a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java index 8ae496c..f8a870a 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java @@ -28,6 +28,7 @@ public class MeetingVO { private String accessPassword; private Integer duration; private String summaryContent; + private String lastUserPrompt; private Map analysis; private Integer status; diff --git a/backend/src/main/java/com/imeeting/mapper/biz/MeetingMapper.java b/backend/src/main/java/com/imeeting/mapper/biz/MeetingMapper.java index 95e283a..f232dba 100644 --- a/backend/src/main/java/com/imeeting/mapper/biz/MeetingMapper.java +++ b/backend/src/main/java/com/imeeting/mapper/biz/MeetingMapper.java @@ -1,9 +1,15 @@ package com.imeeting.mapper.biz; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.imeeting.entity.biz.Meeting; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; @Mapper public interface MeetingMapper extends BaseMapper { + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT * FROM biz_meetings WHERE id = #{id} AND is_deleted = 0") + Meeting selectByIdIgnoreTenant(@Param("id") Long id); } diff --git a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java index dd91a73..1eedf5d 100644 --- a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java @@ -19,6 +19,7 @@ import com.imeeting.service.biz.MeetingRuntimeProfileResolver; import com.imeeting.service.biz.MeetingService; import com.imeeting.service.biz.PromptTemplateService; import com.imeeting.service.biz.impl.MeetingDomainSupport; +import com.imeeting.service.biz.impl.MeetingSummaryPromptAssembler; import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService; import com.unisbase.security.LoginUser; import lombok.RequiredArgsConstructor; @@ -54,6 +55,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ private final MeetingDomainSupport meetingDomainSupport; private final MeetingRuntimeProfileResolver runtimeProfileResolver; private final PromptTemplateService promptTemplateService; + private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler; private final AiTaskService aiTaskService; private final MeetingTranscriptMapper transcriptMapper; private final LlmModelMapper llmModelMapper; @@ -272,13 +274,11 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ private void resetOrCreateSummaryTask(Long meetingId, RealtimeMeetingRuntimeProfile profile) { AiTask task = findLatestTask(meetingId, "SUMMARY"); - Map taskConfig = new HashMap<>(); - taskConfig.put("summaryModelId", profile.getResolvedSummaryModelId()); - taskConfig.put("promptId", profile.getResolvedPromptId()); - PromptTemplate template = promptTemplateService.getById(profile.getResolvedPromptId()); - if (template != null) { - taskConfig.put("promptContent", template.getPromptContent()); - } + Map taskConfig = meetingSummaryPromptAssembler.buildTaskConfig( + profile.getResolvedSummaryModelId(), + profile.getResolvedPromptId(), + null + ); resetOrCreateTask(task, meetingId, "SUMMARY", taskConfig); } @@ -327,4 +327,4 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ private String resolveCreatorName(LoginUser loginUser) { return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername(); } -} \ No newline at end of file +} diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingAccessService.java b/backend/src/main/java/com/imeeting/service/biz/MeetingAccessService.java index 2d0077f..5114a00 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingAccessService.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingAccessService.java @@ -6,6 +6,8 @@ import com.unisbase.security.LoginUser; public interface MeetingAccessService { Meeting requireMeeting(Long meetingId); + Meeting requireMeetingIgnoreTenant(Long meetingId); + boolean isPreviewPasswordRequired(Meeting meeting); void assertCanPreviewMeeting(Meeting meeting, String accessPassword); diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java b/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java index 81969d8..3978119 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java @@ -32,7 +32,7 @@ public interface MeetingCommandService { void updateSummaryContent(Long meetingId, String summaryContent); - void reSummary(Long meetingId, Long summaryModelId, Long promptId); + void reSummary(Long meetingId, Long summaryModelId, Long promptId, String userPrompt); void retryTranscription(Long meetingId); } diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingQueryService.java b/backend/src/main/java/com/imeeting/service/biz/MeetingQueryService.java index d18914e..ddfffc3 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingQueryService.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingQueryService.java @@ -13,6 +13,8 @@ public interface MeetingQueryService { MeetingVO getDetail(Long id); + MeetingVO getDetailIgnoreTenant(Long id); + List getTranscripts(Long meetingId); Map getDashboardStats(Long tenantId, Long userId, boolean isAdmin); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index 5945fa3..b9329ef 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -58,6 +58,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme private final HotWordService hotWordService; private final StringRedisTemplate redisTemplate; private final MeetingSummaryFileService meetingSummaryFileService; + private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler; private final TaskSecurityContextRunner taskSecurityContextRunner; @Value("${unisbase.app.server-base-url}") @@ -206,7 +207,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme String queryUrl = appendPath(asrModel.getBaseUrl(), "api/v1/asr/transcriptions/" + taskId); - // 轮询逻辑 (带防卡死防护) + // 轮询逻辑(带防卡死防护) JsonNode resultNode = null; int lastPercent = -1; int unchangedCount = 0; @@ -241,7 +242,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme if (resultNode == null) throw new RuntimeException("ASR轮询超时"); - // 解析并入库 (防御性清理旧数据) + // 解析并入库(防御性清理旧数据) return saveTranscripts(meeting, resultNode); } @@ -432,15 +433,15 @@ public class AiTaskServiceImpl extends ServiceImpl impleme throw new RuntimeException("LLM模型未启用"); } - String promptContent = taskRecord.getTaskConfig().get("promptContent") != null - ? taskRecord.getTaskConfig().get("promptContent").toString() : ""; + String userPrompt = taskRecord.getTaskConfig().get("userPrompt") != null + ? taskRecord.getTaskConfig().get("userPrompt").toString() : null; Map req = new HashMap<>(); req.put("model", llmModel.getModelCode()); req.put("temperature", llmModel.getTemperature()); req.put("messages", List.of( - Map.of("role", "system", "content", buildSummarySystemPrompt(promptContent)), - Map.of("role", "user", "content", buildSummaryUserPrompt(meeting, asrText)) + Map.of("role", "system", "content", meetingSummaryPromptAssembler.buildSystemMessage(taskRecord.getTaskConfig())), + Map.of("role", "user", "content", meetingSummaryPromptAssembler.buildUserMessage(meeting, asrText, userPrompt)) )); taskRecord.setRequestData(req); @@ -587,88 +588,6 @@ public class AiTaskServiceImpl extends ServiceImpl impleme return normalized.substring(firstLineEnd + 1, lastFence).trim(); } - private String buildSummarySystemPrompt(String promptContent) { - String basePrompt = (promptContent == null || promptContent.isBlank()) - ? "你是一个擅长总结会议、提炼章节、聚合发言人观点和整理待办事项的中文助手。" - : promptContent.trim(); - return String.join("\n\n", - "你是一个擅长总结会议、提炼章节、聚合发言人观点和整理待办事项的中文助手。", - "对于 summaryContent,你必须严格遵循以下会议总结模板/风格要求,不要被后续结构化字段说明覆盖:\n" + basePrompt, - "如果模板中已经定义了标题层级、章节顺序、栏目名称、语气、措辞风格、段落组织方式,你必须保持一致。", - "summaryContent 不允许退化成关键词罗列、JSON 翻译或结构化字段拼接,必须是一篇可直接阅读、可直接导出的正式会议纪要正文。", - "analysis 是附加产物,只服务于页面结构化展示;analysis 不能改变 summaryContent 的模板风格和写法。", - "你需要一次性返回一个 JSON 对象,其中同时包含原会议纪要正文和结构化 analysis 结果。", - "最终只返回 JSON,不要输出 markdown 代码围栏、解释、前后缀或额外说明。" - ); - } - - private String buildSummaryUserPrompt(Meeting meeting, String asrText) { - String participants = meeting.getParticipants() == null || meeting.getParticipants().isBlank() - ? "未填写" - : meeting.getParticipants(); - Integer durationMs = resolveMeetingDuration(meeting.getId()); - String durationText = durationMs == null || durationMs <= 0 ? "未知" : formatDuration(durationMs); - String meetingTime = meeting.getMeetingTime() == null ? "未知" : meeting.getMeetingTime().toString(); - - return String.join("\n", - "请基于以下会议转写,一次性生成会议纪要正文和结构化分析结果。", - "会议基础信息:", - "标题:" + (meeting.getTitle() == null || meeting.getTitle().isBlank() ? "未命名会议" : meeting.getTitle()), - "会议时间:" + meetingTime, - "参会人员:" + participants, - "会议时长:" + durationText, - "返回 JSON,字段结构固定如下:", - "{", - " \"summaryContent\": \"原会议纪要正文,使用 markdown,保持自然完整的纪要写法,而不是关键词列表拼接\",", - " \"analysis\": {", - " \"overview\": \"基于整场会议内容生成的中文全文概要,控制在300字内,需尽量覆盖完整讨论内容\",", - " \"keywords\": [\"关键词1\", \"关键词2\"],", - " \"chapters\": [{\"time\":\"00:00\",\"title\":\"章节标题\",\"summary\":\"章节摘要\"}],", - " \"speakerSummaries\": [{\"speaker\":\"发言人 1\",\"summary\":\"该发言人在整场会议中的主要观点总结\"}],", - " \"keyPoints\": [{\"title\":\"重点问题或结论\",\"summary\":\"具体说明\",\"speaker\":\"发言人 1\"}],", - " \"todos\": [\"待办事项1\", \"待办事项2\"]", - " }", - "}", - "要求:", - "1. summaryContent 必须是完整会议纪要正文,优先遵循提示词模板中的标题层级、章节顺序、栏目名称、措辞风格和段落组织方式。", - "2. 如果模板里已经定义了固定标题或固定分节顺序,summaryContent 必须严格复用,不要自行改写成别的结构。", - "3. summaryContent 必须保持自然完整、适合阅读和导出,不能写成关键词清单,也不能直接把 analysis 内容原样展开。", - "4. analysis 是给页面结构化展示使用的单独结果。", - "5. analysis.overview 必须基于所有会话内容,控制在 300 字内。", - "6. analysis.speakerSummaries 必须按发言人聚合,每个发言人只出现一次。", - "7. analysis.chapters 按时间顺序输出,time 使用 mm:ss 或 hh:mm:ss。time需要与讨论内容相对应", - "8. analysis.keyPoints 提炼关键问题、决定、结论或争议点。", - "9. analysis.todos 尽量写成可执行动作;没有就返回空数组。", - "10. 只返回 JSON。", - "", - "会议转写如下:", - asrText - ); - } - - private Integer resolveMeetingDuration(Long meetingId) { - MeetingTranscript latestTranscript = transcriptMapper.selectOne(new LambdaQueryWrapper() - .eq(MeetingTranscript::getMeetingId, meetingId) - .isNotNull(MeetingTranscript::getEndTime) - .orderByDesc(MeetingTranscript::getEndTime) - .last("LIMIT 1")); - if (latestTranscript != null && latestTranscript.getEndTime() != null && latestTranscript.getEndTime() > 0) { - return latestTranscript.getEndTime(); - } - return null; - } - - private String formatDuration(int durationMs) { - int totalSeconds = Math.max(durationMs / 1000, 0); - int hours = totalSeconds / 3600; - int minutes = (totalSeconds % 3600) / 60; - int seconds = totalSeconds % 60; - if (hours > 0) { - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } - return String.format("%02d:%02d", minutes, seconds); - } - private void updateMeetingStatus(Long id, int status) { Meeting m = new Meeting(); m.setId(id); @@ -701,3 +620,4 @@ public class AiTaskServiceImpl extends ServiceImpl impleme this.updateById(task); } } + diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAccessServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAccessServiceImpl.java index 73f61f6..7fa25ae 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAccessServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAccessServiceImpl.java @@ -22,6 +22,15 @@ public class MeetingAccessServiceImpl implements MeetingAccessService { return meeting; } + @Override + public Meeting requireMeetingIgnoreTenant(Long meetingId) { + Meeting meeting = meetingMapper.selectByIdIgnoreTenant(meetingId); + if (meeting == null) { + throw new RuntimeException("Meeting not found"); + } + return meeting; + } + @Override public boolean isPreviewPasswordRequired(Meeting meeting) { return normalizePreviewPassword(meeting == null ? null : meeting.getAccessPassword()) != null; diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java index 3d833ec..c01f002 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java @@ -90,7 +90,12 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { asrTask.setTaskConfig(asrConfig); aiTaskService.save(asrTask); - meetingDomainSupport.createSummaryTask(meeting.getId(), runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId()); + meetingDomainSupport.createSummaryTask( + meeting.getId(), + runtimeProfile.getResolvedSummaryModelId(), + runtimeProfile.getResolvedPromptId(), + command.getUserPrompt() + ); meeting.setAudioUrl(meetingDomainSupport.relocateAudioUrl(meeting.getId(), command.getAudioUrl())); meetingService.updateById(meeting); meetingDomainSupport.publishMeetingCreated(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId()); @@ -109,7 +114,12 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(), null, tenantId, creatorId, creatorName, hostUserId, hostName, 0); meetingService.save(meeting); - meetingDomainSupport.createSummaryTask(meeting.getId(), runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId()); + meetingDomainSupport.createSummaryTask( + meeting.getId(), + runtimeProfile.getResolvedSummaryModelId(), + runtimeProfile.getResolvedPromptId(), + command.getUserPrompt() + ); realtimeMeetingSessionStateService.initSessionIfAbsent(meeting.getId(), tenantId, creatorId); realtimeMeetingSessionStateService.rememberResumeConfig(meeting.getId(), buildRealtimeResumeConfig(command, tenantId, runtimeProfile)); @@ -455,13 +465,13 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { @Override @Transactional(rollbackFor = Exception.class) - public void reSummary(Long meetingId, Long summaryModelId, Long promptId) { + public void reSummary(Long meetingId, Long summaryModelId, Long promptId, String userPrompt) { Meeting meeting = meetingService.getById(meetingId); if (meeting == null) { throw new RuntimeException("Meeting not found"); } - meetingDomainSupport.createSummaryTask(meetingId, summaryModelId, promptId); + meetingDomainSupport.createSummaryTask(meetingId, summaryModelId, promptId, userPrompt); meeting.setStatus(2); meetingService.updateById(meeting); dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java index 4752d52..0474a17 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java @@ -4,12 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.MeetingTranscript; -import com.imeeting.entity.biz.PromptTemplate; import com.imeeting.event.MeetingCreatedEvent; import com.imeeting.mapper.biz.MeetingTranscriptMapper; import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.MeetingSummaryFileService; -import com.imeeting.service.biz.PromptTemplateService; import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService; import com.unisbase.entity.SysUser; import com.unisbase.mapper.SysUserMapper; @@ -39,7 +37,7 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class MeetingDomainSupport { - private final PromptTemplateService promptTemplateService; + private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler; private final AiTaskService aiTaskService; private final MeetingTranscriptMapper transcriptMapper; private final SysUserMapper sysUserMapper; @@ -68,22 +66,12 @@ public class MeetingDomainSupport { return meeting; } - public void createSummaryTask(Long meetingId, Long summaryModelId, Long promptId) { + public void createSummaryTask(Long meetingId, Long summaryModelId, Long promptId, String userPrompt) { AiTask sumTask = new AiTask(); sumTask.setMeetingId(meetingId); sumTask.setTaskType("SUMMARY"); sumTask.setStatus(0); - - Map sumConfig = new HashMap<>(); - sumConfig.put("summaryModelId", summaryModelId); - if (promptId != null) { - sumConfig.put("promptId", promptId); - PromptTemplate template = promptTemplateService.getById(promptId); - if (template != null) { - sumConfig.put("promptContent", template.getPromptContent()); - } - } - sumTask.setTaskConfig(sumConfig); + sumTask.setTaskConfig(meetingSummaryPromptAssembler.buildTaskConfig(summaryModelId, promptId, userPrompt)); aiTaskService.save(sumTask); } @@ -271,9 +259,47 @@ public class MeetingDomainSupport { if (includeSummary) { vo.setSummaryContent(meetingSummaryFileService.loadSummaryContent(meeting)); vo.setAnalysis(meetingSummaryFileService.loadSummaryAnalysis(meeting)); + vo.setLastUserPrompt(resolveLastSummaryUserPrompt(meeting)); } } + private String resolveLastSummaryUserPrompt(Meeting meeting) { + AiTask latestSummaryTask = resolveLatestSummaryTask(meeting); + if (latestSummaryTask == null || latestSummaryTask.getTaskConfig() == null) { + return null; + } + Object userPrompt = latestSummaryTask.getTaskConfig().get("userPrompt"); + return userPrompt == null ? null : meetingSummaryPromptAssembler.normalizeOptionalText(String.valueOf(userPrompt)); + } + + private AiTask resolveLatestSummaryTask(Meeting meeting) { + if (meeting == null || meeting.getId() == null) { + return null; + } + if (meeting.getLatestSummaryTaskId() != null) { + AiTask task = aiTaskService.getById(meeting.getLatestSummaryTaskId()); + if (task != null && "SUMMARY".equals(task.getTaskType()) && meeting.getId().equals(task.getMeetingId())) { + return task; + } + } + + AiTask latestSuccessfulTask = aiTaskService.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meeting.getId()) + .eq(AiTask::getTaskType, "SUMMARY") + .eq(AiTask::getStatus, 2) + .orderByDesc(AiTask::getId) + .last("LIMIT 1")); + if (latestSuccessfulTask != null) { + return latestSuccessfulTask; + } + + return aiTaskService.getOne(new LambdaQueryWrapper() + .eq(AiTask::getMeetingId, meeting.getId()) + .eq(AiTask::getTaskType, "SUMMARY") + .orderByDesc(AiTask::getId) + .last("LIMIT 1")); + } + private record AudioRelocationPlan(Path sourcePath, Path targetPath, Path backupPath, String relocatedUrl) { } } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java index 16cecb2..a1fc06f 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java @@ -6,6 +6,7 @@ import com.imeeting.dto.biz.MeetingTranscriptVO; import com.imeeting.dto.biz.MeetingVO; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.MeetingTranscript; +import com.imeeting.mapper.biz.MeetingMapper; import com.imeeting.mapper.biz.MeetingTranscriptMapper; import com.imeeting.service.biz.MeetingQueryService; import com.imeeting.service.biz.MeetingService; @@ -24,6 +25,7 @@ import java.util.stream.Collectors; public class MeetingQueryServiceImpl implements MeetingQueryService { private final MeetingService meetingService; + private final MeetingMapper meetingMapper; private final MeetingTranscriptMapper transcriptMapper; private final MeetingDomainSupport meetingDomainSupport; @@ -68,6 +70,12 @@ public class MeetingQueryServiceImpl implements MeetingQueryService { return meeting != null ? toVO(meeting, true) : null; } + @Override + public MeetingVO getDetailIgnoreTenant(Long id) { + Meeting meeting = meetingMapper.selectByIdIgnoreTenant(id); + return meeting != null ? toVO(meeting, true) : null; + } + @Override public List getTranscripts(Long meetingId) { return transcriptMapper.selectList(new LambdaQueryWrapper() diff --git a/backend/src/test/java/com/imeeting/db/DbAlterTest.java b/backend/src/test/java/com/imeeting/db/DbAlterTest.java index 86c0303..ab4aa9e 100644 --- a/backend/src/test/java/com/imeeting/db/DbAlterTest.java +++ b/backend/src/test/java/com/imeeting/db/DbAlterTest.java @@ -1,11 +1,13 @@ package com.imeeting.db; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; @SpringBootTest +@Disabled("Requires a live local database and full application context; not part of automated regression.") public class DbAlterTest { @Autowired diff --git a/backend/src/test/java/com/imeeting/dto/biz/MeetingCreateCommandValidationTest.java b/backend/src/test/java/com/imeeting/dto/biz/MeetingCreateCommandValidationTest.java index a157431..d5e491c 100644 --- a/backend/src/test/java/com/imeeting/dto/biz/MeetingCreateCommandValidationTest.java +++ b/backend/src/test/java/com/imeeting/dto/biz/MeetingCreateCommandValidationTest.java @@ -60,4 +60,19 @@ class MeetingCreateCommandValidationTest { assertTrue(validator.validate(command).isEmpty()); } + + @Test + void shouldRejectTooLongResummaryUserPrompt() { + MeetingResummaryDTO dto = new MeetingResummaryDTO(); + dto.setMeetingId(1L); + dto.setSummaryModelId(2L); + dto.setPromptId(3L); + dto.setUserPrompt("x".repeat(2001)); + + Set invalidFields = validator.validate(dto).stream() + .map(violation -> violation.getPropertyPath().toString()) + .collect(java.util.stream.Collectors.toSet()); + + assertEquals(Set.of("userPrompt"), invalidFields); + } } diff --git a/backend/src/test/java/com/imeeting/manual/AndroidRealtimeGrpcManualTest.java b/backend/src/test/java/com/imeeting/manual/AndroidRealtimeGrpcManualTest.java index ba958c5..59f773b 100644 --- a/backend/src/test/java/com/imeeting/manual/AndroidRealtimeGrpcManualTest.java +++ b/backend/src/test/java/com/imeeting/manual/AndroidRealtimeGrpcManualTest.java @@ -13,6 +13,7 @@ import com.imeeting.grpc.realtime.RealtimeServerPacket; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.InputStream; @@ -34,6 +35,7 @@ import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +@Disabled("Manual realtime integration test; requires a running local backend, gRPC service, and PCM fixture.") public class AndroidRealtimeGrpcManualTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); diff --git a/backend/src/test/java/com/imeeting/service/android/legacy/LegacyMeetingAdapterServiceImplTest.java b/backend/src/test/java/com/imeeting/service/android/legacy/LegacyMeetingAdapterServiceImplTest.java index 2aaa5b1..b35a9cd 100644 --- a/backend/src/test/java/com/imeeting/service/android/legacy/LegacyMeetingAdapterServiceImplTest.java +++ b/backend/src/test/java/com/imeeting/service/android/legacy/LegacyMeetingAdapterServiceImplTest.java @@ -12,6 +12,7 @@ import com.imeeting.service.biz.MeetingRuntimeProfileResolver; import com.imeeting.service.biz.MeetingService; import com.imeeting.service.biz.PromptTemplateService; import com.imeeting.service.biz.impl.MeetingDomainSupport; +import com.imeeting.service.biz.impl.MeetingSummaryPromptAssembler; import com.unisbase.security.LoginUser; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -63,6 +64,7 @@ class LegacyMeetingAdapterServiceImplTest { meetingDomainSupport, mock(MeetingRuntimeProfileResolver.class), mock(PromptTemplateService.class), + mock(MeetingSummaryPromptAssembler.class), mock(AiTaskService.class), mock(MeetingTranscriptMapper.class), mock(LlmModelMapper.class) diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingAuthorizationServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingAuthorizationServiceImplTest.java index d1d9318..2b4266e 100644 --- a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingAuthorizationServiceImplTest.java +++ b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingAuthorizationServiceImplTest.java @@ -10,6 +10,7 @@ import org.mockito.ArgumentCaptor; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -51,7 +52,7 @@ class MeetingAuthorizationServiceImplTest { service.assertCanManageRealtimeMeeting(meeting, authContext); ArgumentCaptor loginUserCaptor = ArgumentCaptor.forClass(LoginUser.class); - verify(meetingAccessService).assertCanManageRealtimeMeeting(meeting, loginUserCaptor.capture()); + verify(meetingAccessService).assertCanManageRealtimeMeeting(eq(meeting), loginUserCaptor.capture()); assertEquals(7L, loginUserCaptor.getValue().getUserId()); assertEquals(1L, loginUserCaptor.getValue().getTenantId()); assertEquals("alice", loginUserCaptor.getValue().getUsername()); diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingCommandServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingCommandServiceImplTest.java index 15a9eed..94d1780 100644 --- a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingCommandServiceImplTest.java +++ b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingCommandServiceImplTest.java @@ -319,9 +319,9 @@ class MeetingCommandServiceImplTest { TransactionSynchronizationManager.initSynchronization(); try { - service.reSummary(301L, 22L, 33L); + service.reSummary(301L, 22L, 33L, null); - verify(meetingDomainSupport).createSummaryTask(301L, 22L, 33L); + verify(meetingDomainSupport).createSummaryTask(301L, 22L, 33L, null); assertEquals(2, meeting.getStatus()); verify(meetingService).updateById(meeting); verify(aiTaskService, never()).dispatchSummaryTask(301L, null, null); @@ -484,6 +484,7 @@ class MeetingCommandServiceImplTest { command.setAsrModelId(11L); command.setSummaryModelId(22L); command.setPromptId(33L); + command.setUserPrompt("聚焦关键风险"); service.createMeeting(command, 1L, 7L, "creator"); @@ -494,7 +495,7 @@ class MeetingCommandServiceImplTest { Object asrModelId = task.getTaskConfig().get("asrModelId"); return Long.valueOf(101L).equals(asrModelId); })); - verify(meetingDomainSupport).createSummaryTask(808L, 202L, 303L); + verify(meetingDomainSupport).createSummaryTask(808L, 202L, 303L, "聚焦关键风险"); } @Test @@ -551,10 +552,11 @@ class MeetingCommandServiceImplTest { command.setEnableItn(false); command.setEnableTextRefine(true); command.setSaveAudio(true); + command.setUserPrompt("关注待办事项"); service.createRealtimeMeeting(command, 1L, 7L, "creator"); - verify(meetingDomainSupport).createSummaryTask(909L, 222L, 333L); + verify(meetingDomainSupport).createSummaryTask(909L, 222L, 333L, "关注待办事项"); verify(sessionStateService).rememberResumeConfig(eq(909L), argThat(config -> Long.valueOf(111L).equals(config.getAsrModelId()) && "online".equals(config.getMode()) diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingDomainSupportTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingDomainSupportTest.java index b15a7eb..cbcc2fc 100644 --- a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingDomainSupportTest.java +++ b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingDomainSupportTest.java @@ -1,13 +1,15 @@ package com.imeeting.service.biz.impl; +import com.imeeting.entity.biz.AiTask; +import com.imeeting.entity.biz.Meeting; import com.imeeting.mapper.biz.MeetingTranscriptMapper; import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.MeetingSummaryFileService; -import com.imeeting.service.biz.PromptTemplateService; import com.unisbase.mapper.SysUserMapper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.support.TransactionSynchronization; @@ -17,11 +19,15 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class MeetingDomainSupportTest { @@ -74,10 +80,107 @@ class MeetingDomainSupportTest { assertFalse(hasBackupFile(tempDir.resolve("uploads/meetings/102"))); } + @Test + void shouldPreferLatestSummaryTaskIdWhenResolvingLastUserPrompt() { + AiTaskService aiTaskService = mock(AiTaskService.class); + MeetingSummaryPromptAssembler assembler = mock(MeetingSummaryPromptAssembler.class); + MeetingDomainSupport support = newSupport(aiTaskService, assembler); + + Meeting meeting = new Meeting(); + meeting.setId(201L); + meeting.setLatestSummaryTaskId(501L); + + AiTask latestSummaryTask = new AiTask(); + latestSummaryTask.setTaskType("SUMMARY"); + latestSummaryTask.setMeetingId(201L); + latestSummaryTask.setTaskConfig(Map.of("userPrompt", " 已发布提示词 ")); + + AiTask fallbackTask = new AiTask(); + fallbackTask.setTaskType("SUMMARY"); + fallbackTask.setMeetingId(201L); + fallbackTask.setTaskConfig(Map.of("userPrompt", " 最新草稿提示词 ")); + + when(aiTaskService.getById(501L)).thenReturn(latestSummaryTask); + when(assembler.normalizeOptionalText(" 已发布提示词 ")).thenReturn("已发布提示词"); + when(aiTaskService.getOne(any())).thenReturn(fallbackTask); + + String resolved = ReflectionTestUtils.invokeMethod(support, "resolveLastSummaryUserPrompt", meeting); + + assertEquals("已发布提示词", resolved); + Mockito.verify(aiTaskService, Mockito.never()).getOne(any()); + } + + @Test + void shouldFallbackToLatestSummaryTaskWhenLatestSummaryTaskIdIsUnavailable() { + AiTaskService aiTaskService = mock(AiTaskService.class); + MeetingSummaryPromptAssembler assembler = mock(MeetingSummaryPromptAssembler.class); + MeetingDomainSupport support = newSupport(aiTaskService, assembler); + + Meeting meeting = new Meeting(); + meeting.setId(202L); + meeting.setLatestSummaryTaskId(502L); + + AiTask latestSuccessfulTask = new AiTask(); + latestSuccessfulTask.setTaskType("SUMMARY"); + latestSuccessfulTask.setMeetingId(202L); + latestSuccessfulTask.setStatus(2); + latestSuccessfulTask.setTaskConfig(Map.of("userPrompt", " 成功提示词 ")); + + when(aiTaskService.getById(502L)).thenReturn(null); + when(aiTaskService.getOne(any())).thenReturn(latestSuccessfulTask); + when(assembler.normalizeOptionalText(" 成功提示词 ")).thenReturn("成功提示词"); + + String resolved = ReflectionTestUtils.invokeMethod(support, "resolveLastSummaryUserPrompt", meeting); + + assertEquals("成功提示词", resolved); + } + + @Test + void shouldFallbackToLatestSummaryTaskWhenNoSuccessfulTaskExists() { + AiTaskService aiTaskService = mock(AiTaskService.class); + MeetingSummaryPromptAssembler assembler = mock(MeetingSummaryPromptAssembler.class); + MeetingDomainSupport support = newSupport(aiTaskService, assembler); + + Meeting meeting = new Meeting(); + meeting.setId(203L); + + AiTask latestTask = new AiTask(); + latestTask.setTaskType("SUMMARY"); + latestTask.setMeetingId(203L); + latestTask.setTaskConfig(Map.of("userPrompt", " 最新任务提示词 ")); + + when(aiTaskService.getOne(any())).thenReturn(null).thenReturn(latestTask); + when(assembler.normalizeOptionalText(" 最新任务提示词 ")).thenReturn("最新任务提示词"); + + String resolved = ReflectionTestUtils.invokeMethod(support, "resolveLastSummaryUserPrompt", meeting); + + assertEquals("最新任务提示词", resolved); + } + + @Test + void shouldReturnNullWhenNoSummaryTaskExists() { + AiTaskService aiTaskService = mock(AiTaskService.class); + MeetingSummaryPromptAssembler assembler = mock(MeetingSummaryPromptAssembler.class); + MeetingDomainSupport support = newSupport(aiTaskService, assembler); + + Meeting meeting = new Meeting(); + meeting.setId(204L); + + when(aiTaskService.getOne(any())).thenReturn(null, null); + + String resolved = ReflectionTestUtils.invokeMethod(support, "resolveLastSummaryUserPrompt", meeting); + + assertNull(resolved); + } + private MeetingDomainSupport newSupport() { + return newSupport(mock(AiTaskService.class), mock(MeetingSummaryPromptAssembler.class)); + } + + private MeetingDomainSupport newSupport(AiTaskService aiTaskService, MeetingSummaryPromptAssembler assembler) { MeetingDomainSupport support = new MeetingDomainSupport( - mock(PromptTemplateService.class), - mock(AiTaskService.class), + assembler, + aiTaskService, mock(MeetingTranscriptMapper.class), mock(SysUserMapper.class), mock(ApplicationEventPublisher.class), diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingRuntimeProfileResolverImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingRuntimeProfileResolverImplTest.java index cacc463..2d07750 100644 --- a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingRuntimeProfileResolverImplTest.java +++ b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingRuntimeProfileResolverImplTest.java @@ -9,6 +9,7 @@ import com.imeeting.service.biz.AiModelService; import com.imeeting.service.biz.PromptTemplateService; import org.junit.jupiter.api.Test; +import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -46,7 +47,7 @@ class MeetingRuntimeProfileResolverImplTest { null, Boolean.TRUE, Boolean.TRUE, - List.of(" alpha ", "", "alpha", "beta", null) + Arrays.asList(" alpha ", "", "alpha", "beta", null) ); assertEquals(11L, profile.getResolvedAsrModelId()); diff --git a/frontend/src/api/business/client.ts b/frontend/src/api/business/client.ts index e071a89..42f16ed 100644 --- a/frontend/src/api/business/client.ts +++ b/frontend/src/api/business/client.ts @@ -72,6 +72,7 @@ export async function uploadClientPackage(platformCode: string, file: File) { formData.append("file", file); const resp = await http.post("/api/clients/upload", formData, { headers: { "Content-Type": "multipart/form-data" }, + timeout: 600000 }); return resp.data.data as ClientUploadResult; -} \ No newline at end of file +} diff --git a/frontend/src/api/business/externalApp.ts b/frontend/src/api/business/externalApp.ts index e56c98c..041e19a 100644 --- a/frontend/src/api/business/externalApp.ts +++ b/frontend/src/api/business/externalApp.ts @@ -68,6 +68,7 @@ export async function uploadExternalAppApk(file: File) { formData.append("apkFile", file); const resp = await http.post("/api/external-apps/upload-apk", formData, { headers: { "Content-Type": "multipart/form-data" }, + timeout: 600000 }); return resp.data.data as ExternalAppApkUploadResult; } @@ -79,4 +80,4 @@ export async function uploadExternalAppIcon(file: File) { headers: { "Content-Type": "multipart/form-data" }, }); return resp.data.data as ExternalAppIconUploadResult; -} \ No newline at end of file +} diff --git a/frontend/src/api/business/meeting.ts b/frontend/src/api/business/meeting.ts index 47408b8..fe1f6a6 100644 --- a/frontend/src/api/business/meeting.ts +++ b/frontend/src/api/business/meeting.ts @@ -19,6 +19,7 @@ export interface MeetingVO { audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED"; audioSaveMessage?: string; accessPassword?: string; + lastUserPrompt?: string; summaryContent: string; analysis?: { overview?: string; @@ -46,6 +47,7 @@ export interface CreateMeetingCommand { asrModelId: number; summaryModelId?: number; promptId: number; + userPrompt?: string; useSpkId?: number; enableTextRefine?: boolean; hotWords?: string[]; @@ -63,6 +65,7 @@ export interface CreateRealtimeMeetingCommand { asrModelId: number; summaryModelId?: number; promptId: number; + userPrompt?: string; mode?: string; language?: string; useSpkId?: number; @@ -284,6 +287,7 @@ export interface MeetingResummaryDTO { meetingId: number; summaryModelId: number; promptId: number; + userPrompt?: string; } export const reSummary = (params: MeetingResummaryDTO) => { diff --git a/frontend/src/components/business/MeetingCreateDrawer.tsx b/frontend/src/components/business/MeetingCreateDrawer.tsx index 3b00dae..365d635 100644 --- a/frontend/src/components/business/MeetingCreateDrawer.tsx +++ b/frontend/src/components/business/MeetingCreateDrawer.tsx @@ -436,6 +436,18 @@ export const MeetingCreateDrawer: React.FC = ({ open, )} + + + ), } diff --git a/frontend/src/pages/access/permissions/index.tsx b/frontend/src/pages/access/permissions/index.tsx index e4329b7..3cd2e65 100644 --- a/frontend/src/pages/access/permissions/index.tsx +++ b/frontend/src/pages/access/permissions/index.tsx @@ -595,4 +595,4 @@ export default function Permissions() { ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/access/roles/index.less b/frontend/src/pages/access/roles/index.less index ff3d320..5a271dc 100644 --- a/frontend/src/pages/access/roles/index.less +++ b/frontend/src/pages/access/roles/index.less @@ -207,10 +207,11 @@ .role-list-pagination { flex-shrink: 0; + padding-top: 16px; margin-top: auto; - margin-left: -16px; - margin-right: -16px; - margin-bottom: -16px; + border-top: 1px solid #f1f5f9; + display: flex; + justify-content: flex-end; } /* Detail Card Adjustments */ diff --git a/frontend/src/pages/access/users/index.tsx b/frontend/src/pages/access/users/index.tsx index 4bb2c0c..55c389b 100644 --- a/frontend/src/pages/access/users/index.tsx +++ b/frontend/src/pages/access/users/index.tsx @@ -8,6 +8,8 @@ import AppPagination from "@/components/shared/AppPagination"; import { useDict } from "@/hooks/useDict"; import { usePermission } from "@/hooks/usePermission"; import PageHeader from "@/components/shared/PageHeader"; +import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName"; +import { getStandardPagination } from "@/utils/pagination"; import type { SysOrg, SysRole, SysTenant, SysUser } from "@/types"; import "./index.less"; @@ -236,6 +238,9 @@ export default function Users() { const url = await uploadPlatformAsset(file); form.setFieldValue("avatarUrl", url); message.success(t("common.success")); + } catch (error) { + message.error(error instanceof Error ? error.message : t("common.error")); + return Upload.LIST_IGNORE; } finally { setAvatarUploading(false); } @@ -253,7 +258,8 @@ export default function Users() { phone: values.phone, avatarUrl: values.avatarUrl, status: values.status, - isPlatformAdmin: values.isPlatformAdmin + isPlatformAdmin: values.isPlatformAdmin, + roleIds: values.roleIds || [] }; if (!isPlatformMode) { @@ -266,18 +272,10 @@ export default function Users() { userPayload.password = values.password; } - let userId = editing?.userId; if (editing) { await updateUser(editing.userId, userPayload); } else { await createUser(userPayload); - const updatedList = await listUsers(); - const newUser = updatedList.find((user) => user.username === userPayload.username); - userId = newUser?.userId; - } - - if (userId) { - await saveUserRoles(userId, values.roleIds || []); } message.success(t("common.success")); @@ -307,24 +305,24 @@ export default function Users() { }, ...(isPlatformMode ? [{ - title: t("users.tenant"), - key: "tenant", - render: (_: any, record: SysUser) => { - if (record.memberships && record.memberships.length > 0) { - return ( -
- {record.memberships.slice(0, 2).map((membership: any) => ( - - {tenantMap[membership.tenantId] || `Tenant ${membership.tenantId}`} - - ))} - {record.memberships.length > 2 && +{record.memberships.length - 2} more} -
- ); + title: t("users.tenant"), + key: "tenant", + render: (_: any, record: SysUser) => { + if (record.memberships && record.memberships.length > 0) { + return ( +
+ {record.memberships.slice(0, 2).map((membership: any) => ( + + {tenantMap[membership.tenantId] || `Tenant ${membership.tenantId}`} + + ))} + {record.memberships.length > 2 && +{record.memberships.length - 2} more} +
+ ); + } + return {t("usersExt.noTenant")}; } - return {t("usersExt.noTenant")}; - } - }] + }] : []), { title: t("users.orgNode"), @@ -388,10 +386,10 @@ export default function Users() {
- {isPlatformMode && } className="users-search-input" style={{ width: 300 }} value={searchText} onChange={(event) => { setSearchText(event.target.value); setCurrent(1); }} allowClear aria-label={t("common.search")} /> - - + {isPlatformMode && } className="users-search-input" style={{ width: 300 }} value={searchText} onChange={(event) => { setSearchText(event.target.value); setCurrent(1); }} allowClear aria-label={t("common.search")} /> + + {can("sys:user:create") && } @@ -405,11 +403,11 @@ export default function Users() { { setCurrent(page); setPageSize(size); }} /> -
} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnHidden forceRender footer={
}> +