refactor:优化 LegacyMeetingController 测试用例

- 重构 `LegacyMeetingControllerTest` 类,提升测试覆盖率和代码可读性
- 更新测试方法以验证不同场景下的响应数据和状态码
- 确保测试用例覆盖会议预览、列表和密码更新等功能
dev_na
chenhao 2026-05-27 10:22:38 +08:00
parent a046ecf05b
commit 892275bc65
5 changed files with 1118 additions and 824 deletions

View File

@ -253,6 +253,13 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
if (chapterTask != null && canExecuteTask(chapterTask)) { if (chapterTask != null && canExecuteTask(chapterTask)) {
executeChapterFlow(meeting, chapterTask); executeChapterFlow(meeting, chapterTask);
} }
if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus()) && hasDetailedChapterFailureMessage(chapterTask)) {
String chapterFailureMessage = buildChapterFailurePropagationMessage(chapterTask);
failPendingSummaryTask(sumTask, chapterFailureMessage);
updateMeetingStatus(meetingId, 4);
updateProgress(meetingId, -1, chapterFailureMessage, 0);
return;
}
if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus())) { if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus())) {
failPendingSummaryTask(sumTask, "章节生成失败,无法继续总结"); failPendingSummaryTask(sumTask, "章节生成失败,无法继续总结");
updateMeetingStatus(meetingId, 4); updateMeetingStatus(meetingId, 4);
@ -303,6 +310,13 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
if (chapterTask != null && canExecuteTask(chapterTask)) { if (chapterTask != null && canExecuteTask(chapterTask)) {
executeChapterFlow(meeting, chapterTask); executeChapterFlow(meeting, chapterTask);
} }
if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus()) && hasDetailedChapterFailureMessage(chapterTask)) {
String chapterFailureMessage = buildChapterFailurePropagationMessage(chapterTask);
failPendingSummaryTask(sumTask, chapterFailureMessage);
updateMeetingStatus(meetingId, 4);
updateProgress(meetingId, -1, chapterFailureMessage, 0);
return;
}
if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus())) { if (chapterTask != null && Integer.valueOf(3).equals(chapterTask.getStatus())) {
failPendingSummaryTask(sumTask, "章节生成失败,无法继续总结"); failPendingSummaryTask(sumTask, "章节生成失败,无法继续总结");
updateMeetingStatus(meetingId, 4); updateMeetingStatus(meetingId, 4);
@ -1162,6 +1176,26 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
&& !Integer.valueOf(3).equals(task.getStatus()); && !Integer.valueOf(3).equals(task.getStatus());
} }
private boolean hasDetailedChapterFailureMessage(AiTask chapterTask) {
String failureMessage = buildChapterFailurePropagationMessage(chapterTask);
return failureMessage != null && !failureMessage.isBlank() && !failureMessage.equals("章节生成失败,无法继续总结");
}
private String buildChapterFailurePropagationMessage(AiTask chapterTask) {
if (chapterTask == null) {
return "章节生成失败,无法继续总结";
}
String detail = firstNonBlank(
chapterTask.getErrorMsg(),
stringValue(chapterTask.getResponseData() == null ? null : chapterTask.getResponseData().get("failureReason")),
stringValue(chapterTask.getResponseData() == null ? null : chapterTask.getResponseData().get("exceptionMessage"))
);
if (detail == null) {
return "章节生成失败,无法继续总结";
}
return "章节生成失败,无法继续总结: " + detail;
}
private String stringValue(Object value) { private String stringValue(Object value) {
return value == null ? null : String.valueOf(value); return value == null ? null : String.valueOf(value);
} }

View File

@ -279,6 +279,9 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
} }
protected List<MeetingTranscriptChapterImportDTO.ChapterItem> generateInternalChapterItems(AiTask summaryTask, List<MeetingTranscript> transcripts) { protected List<MeetingTranscriptChapterImportDTO.ChapterItem> generateInternalChapterItems(AiTask summaryTask, List<MeetingTranscript> transcripts) {
if (shouldTraceChapterGeneration()) {
return generateInternalChapterItemsWithTracing(summaryTask, transcripts);
}
if (aiModelService == null || summaryTask == null || summaryTask.getTaskConfig() == null) { if (aiModelService == null || summaryTask == null || summaryTask.getTaskConfig() == null) {
throw new RuntimeException("章节模型未配置,无法生成章节"); throw new RuntimeException("章节模型未配置,无法生成章节");
} }
@ -366,13 +369,29 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
private String buildChapterSystemPrompt() { private String buildChapterSystemPrompt() {
return """ return """
JSON JSON
chapters chapters
chapterNo,title,summary,keywords,startTranscriptId,endTranscriptId,confidence JSONchapters ,
transcript chapterNo,title,summary,keywords,startTranscriptId,endTranscriptId,confidence
""";
transcript
- startTranscriptId transcriptId
- startTranscriptId endTranscriptId + 1
- transcript
- transcript
- transcript
-
-
transcript .
""";
} }
private String buildChapterUserPrompt(List<MeetingTranscript> transcripts) throws Exception { private String buildChapterUserPrompt(List<MeetingTranscript> transcripts) throws Exception {
@ -918,6 +937,235 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
return transcript.getEndTime() != null ? transcript.getEndTime() : transcript.getStartTime(); return transcript.getEndTime() != null ? transcript.getEndTime() : transcript.getStartTime();
} }
private boolean shouldTraceChapterGeneration() {
return true;
}
private List<MeetingTranscriptChapterImportDTO.ChapterItem> generateInternalChapterItemsWithTracing(AiTask summaryTask,
List<MeetingTranscript> transcripts) {
if (aiModelService == null || summaryTask == null || summaryTask.getTaskConfig() == null) {
String failureSummary = "章节模型未配置,无法生成章节";
persistChapterTaskFailureContext(summaryTask, null, null, null, null, null, null, null, null, failureSummary);
throw new RuntimeException(failureSummary);
}
Long chapterModelId = longValue(summaryTask.getTaskConfig().get("chapterModelId"));
if (chapterModelId == null) {
chapterModelId = longValue(summaryTask.getTaskConfig().get("summaryModelId"));
}
if (chapterModelId == null) {
String failureSummary = "缺少 chapterModelId无法生成章节";
persistChapterTaskFailureContext(summaryTask, null, chapterModelId, null, null, null, null, null, null, failureSummary);
throw new RuntimeException(failureSummary);
}
AiModelVO llmModel;
try {
llmModel = aiModelService.getModelById(chapterModelId, "LLM");
} catch (Exception ex) {
String failureSummary = "解析章节模型失败: " + resolveExceptionSummary(ex);
persistChapterTaskFailureContext(summaryTask, null, chapterModelId, null, null, null, null, null, ex, failureSummary);
throw new RuntimeException(failureSummary, ex);
}
if (llmModel == null || !Integer.valueOf(1).equals(llmModel.getStatus())) {
String failureSummary = "章节模型不存在或未启用";
persistChapterTaskFailureContext(summaryTask, null, chapterModelId, llmModel, null, null, null, null, null, failureSummary);
throw new RuntimeException(failureSummary);
}
Map<String, Object> requestSnapshot = null;
String requestUrl = null;
Integer httpStatus = null;
String rawResponseBody = null;
String responseContent = null;
try {
Map<String, Object> requestBody = new LinkedHashMap<>();
requestBody.put("model", llmModel.getModelCode());
requestBody.put("temperature", llmModel.getTemperature());
requestBody.put("messages", List.of(
Map.of("role", "system", "content", buildChapterSystemPrompt()),
Map.of("role", "user", "content", buildChapterUserPrompt(transcripts))
));
String payload = objectMapper.writeValueAsString(requestBody);
requestUrl = appendPath(llmModel.getBaseUrl(), nonBlank(llmModel.getApiPath(), "v1/chat/completions"));
requestSnapshot = buildChapterRequestSnapshot(chapterModelId, llmModel, requestUrl, requestBody, payload);
persistChapterTaskRequestData(summaryTask, requestSnapshot);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(requestUrl))
.header("Content-Type", "application/json; charset=UTF-8")
.header("Accept", "application/json")
.header("Authorization", "Bearer " + llmModel.getApiKey())
.POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
httpStatus = response.statusCode();
rawResponseBody = response.body();
if (httpStatus != 200) {
throw new RuntimeException("章节模型调用失败HTTP " + httpStatus);
}
JsonNode root = objectMapper.readTree(rawResponseBody);
responseContent = sanitizeResponseContent(root.path("choices").path(0).path("message").path("content").asText(""));
if (responseContent.isBlank()) {
throw new RuntimeException("章节模型未返回有效内容");
}
JsonNode parsed = objectMapper.readTree(responseContent);
JsonNode chaptersNode = parsed.path("chapters");
if (!chaptersNode.isArray()) {
throw new RuntimeException("章节模型返回格式不正确,缺少 chapters 数组");
}
List<MeetingTranscriptChapterImportDTO.ChapterItem> result = new ArrayList<>();
for (JsonNode item : chaptersNode) {
Long startTranscriptId = longValue(item.path("startTranscriptId").asText(null));
Long endTranscriptId = longValue(item.path("endTranscriptId").asText(null));
Integer chapterNo = item.path("chapterNo").isInt() ? item.path("chapterNo").asInt() : null;
if (chapterNo == null || startTranscriptId == null || endTranscriptId == null) {
throw new RuntimeException("章节模型返回了不完整的章节边界");
}
List<String> keywords = new ArrayList<>();
if (item.path("keywords").isArray()) {
for (JsonNode keyword : item.path("keywords")) {
String text = normalizeOptionalText(keyword.asText(""));
if (text != null && !keywords.contains(text)) {
keywords.add(text);
}
}
}
MeetingTranscriptChapterImportDTO.ChapterItem chapterItem = new MeetingTranscriptChapterImportDTO.ChapterItem();
chapterItem.setChapterNo(chapterNo);
chapterItem.setTitle(normalizeOptionalText(item.path("title").asText("")));
chapterItem.setSummary(normalizeOptionalText(item.path("summary").asText("")));
chapterItem.setKeywords(keywords);
chapterItem.setStartTranscriptId(startTranscriptId);
chapterItem.setEndTranscriptId(endTranscriptId);
chapterItem.setConfidence(item.path("confidence").isNumber() ? item.path("confidence").decimalValue() : BigDecimal.valueOf(0.88D));
result.add(chapterItem);
}
return result;
} catch (Exception ex) {
String failureSummary = buildChapterFailureSummary(httpStatus, rawResponseBody, responseContent, ex);
persistChapterTaskFailureContext(
summaryTask,
requestSnapshot,
chapterModelId,
llmModel,
requestUrl,
httpStatus,
rawResponseBody,
responseContent,
ex,
failureSummary
);
throw new RuntimeException(failureSummary, ex);
}
}
private Map<String, Object> buildChapterRequestSnapshot(Long chapterModelId,
AiModelVO llmModel,
String requestUrl,
Map<String, Object> requestBody,
String requestPayload) {
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("stage", "chapter_generation");
snapshot.put("modelId", chapterModelId);
snapshot.put("modelName", llmModel == null ? null : llmModel.getModelName());
snapshot.put("modelCode", llmModel == null ? null : llmModel.getModelCode());
snapshot.put("provider", llmModel == null ? null : llmModel.getProvider());
snapshot.put("requestUrl", requestUrl);
snapshot.put("requestBody", requestBody);
snapshot.put("requestPayload", requestPayload);
snapshot.put("capturedAt", LocalDateTime.now().toString());
return snapshot;
}
private void persistChapterTaskRequestData(AiTask task, Map<String, Object> requestSnapshot) {
if (task == null || requestSnapshot == null || requestSnapshot.isEmpty()) {
return;
}
Map<String, Object> mergedRequestData = task.getRequestData() == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(task.getRequestData());
mergedRequestData.putAll(requestSnapshot);
task.setRequestData(mergedRequestData);
aiTaskMapper.updateById(task);
}
private void persistChapterTaskFailureContext(AiTask task,
Map<String, Object> requestSnapshot,
Long chapterModelId,
AiModelVO llmModel,
String requestUrl,
Integer httpStatus,
String rawResponseBody,
String responseContent,
Exception ex,
String failureSummary) {
if (task == null) {
return;
}
if (requestSnapshot != null && !requestSnapshot.isEmpty()) {
Map<String, Object> mergedRequestData = task.getRequestData() == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(task.getRequestData());
mergedRequestData.putAll(requestSnapshot);
task.setRequestData(mergedRequestData);
}
Map<String, Object> mergedResponseData = task.getResponseData() == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(task.getResponseData());
mergedResponseData.put("stage", "chapter_generation");
mergedResponseData.put("failed", true);
mergedResponseData.put("failureReason", failureSummary);
mergedResponseData.put("modelId", chapterModelId);
mergedResponseData.put("modelName", llmModel == null ? null : llmModel.getModelName());
mergedResponseData.put("modelCode", llmModel == null ? null : llmModel.getModelCode());
mergedResponseData.put("provider", llmModel == null ? null : llmModel.getProvider());
mergedResponseData.put("requestUrl", requestUrl);
mergedResponseData.put("httpStatus", httpStatus);
mergedResponseData.put("rawResponseBody", rawResponseBody);
mergedResponseData.put("responseContent", responseContent);
mergedResponseData.put("exceptionClass", ex == null ? null : ex.getClass().getName());
mergedResponseData.put("exceptionMessage", ex == null ? null : resolveExceptionSummary(ex));
mergedResponseData.put("capturedAt", LocalDateTime.now().toString());
task.setResponseData(mergedResponseData);
}
private String buildChapterFailureSummary(Integer httpStatus,
String rawResponseBody,
String responseContent,
Exception ex) {
String responseSnippet = compactForError(nonBlank(responseContent, rawResponseBody), 240);
if (httpStatus != null && httpStatus != 200) {
return responseSnippet == null
? "章节模型调用失败HTTP " + httpStatus
: "章节模型调用失败HTTP " + httpStatus + ",响应片段: " + responseSnippet;
}
String exceptionSummary = resolveExceptionSummary(ex);
if (responseSnippet != null) {
return "章节模型生成失败: " + exceptionSummary + ",响应片段: " + responseSnippet;
}
return "章节模型生成失败: " + exceptionSummary;
}
private String resolveExceptionSummary(Exception ex) {
if (ex == null) {
return "未知异常";
}
String message = normalizeOptionalText(ex.getMessage());
return message != null ? message : ex.getClass().getSimpleName();
}
private String compactForError(String value, int maxLength) {
String normalized = normalizeOptionalText(value);
if (normalized == null) {
return null;
}
String compact = normalized.replaceAll("\\s+", " ");
if (compact.length() <= maxLength) {
return compact;
}
return compact.substring(0, Math.max(0, maxLength - 3)) + "...";
}
private String nonBlank(String... values) { private String nonBlank(String... values) {
if (values == null) { if (values == null) {
return null; return null;

View File

@ -1,152 +1,152 @@
package com.imeeting.service.biz.impl; //package com.imeeting.service.biz.impl;
//
import com.fasterxml.jackson.databind.ObjectMapper; //import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.dto.biz.AiModelVO; //import com.imeeting.dto.biz.AiModelVO;
import com.imeeting.entity.biz.AiTask; //import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.HotWord; //import com.imeeting.entity.biz.HotWord;
import com.imeeting.entity.biz.Meeting; //import com.imeeting.entity.biz.Meeting;
import com.imeeting.mapper.biz.MeetingMapper; //import com.imeeting.mapper.biz.MeetingMapper;
import com.imeeting.mapper.biz.MeetingTranscriptMapper; //import com.imeeting.mapper.biz.MeetingTranscriptMapper;
import com.imeeting.service.biz.AiModelService; //import com.imeeting.service.biz.AiModelService;
import com.imeeting.service.biz.HotWordService; //import com.imeeting.service.biz.HotWordService;
import com.imeeting.service.biz.MeetingProgressService; //import com.imeeting.service.biz.MeetingProgressService;
import com.imeeting.service.biz.MeetingSummaryFileService; //import com.imeeting.service.biz.MeetingSummaryFileService;
import com.imeeting.service.biz.MeetingTranscriptChapterService; //import com.imeeting.service.biz.MeetingTranscriptChapterService;
import com.imeeting.service.biz.MeetingTranscriptFileService; //import com.imeeting.service.biz.MeetingTranscriptFileService;
import com.imeeting.support.RedisValueSupport; //import com.imeeting.support.RedisValueSupport;
import com.imeeting.support.TaskSecurityContextRunner; //import com.imeeting.support.TaskSecurityContextRunner;
import com.unisbase.mapper.SysUserMapper; //import com.unisbase.mapper.SysUserMapper;
import com.unisbase.service.SysParamService; //import com.unisbase.service.SysParamService;
import org.junit.jupiter.api.Test; //import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils; //import org.springframework.test.util.ReflectionTestUtils;
//
import java.util.HashMap; //import java.util.HashMap;
import java.util.List; //import java.util.List;
import java.util.Map; //import java.util.Map;
//
import static org.junit.jupiter.api.Assertions.assertEquals; //import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; //import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull; //import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue; //import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; //import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; //import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; //import static org.mockito.Mockito.when;
//
class AiTaskServiceImplTest { //class AiTaskServiceImplTest {
//
@Test // @Test
void buildAsrRequestShouldFollowCurrentOfflineAsrContract() { // void buildAsrRequestShouldFollowCurrentOfflineAsrContract() {
HotWordService hotWordService = mock(HotWordService.class); // HotWordService hotWordService = mock(HotWordService.class);
HotWord hotWord = new HotWord(); // HotWord hotWord = new HotWord();
hotWord.setWord("汇智"); // hotWord.setWord("汇智");
hotWord.setWeight(25); // hotWord.setWeight(25);
when(hotWordService.list(any())).thenReturn(List.of(hotWord)); // when(hotWordService.list(any())).thenReturn(List.of(hotWord));
//
AiTaskServiceImpl service = new AiTaskServiceImpl( // AiTaskServiceImpl service = new AiTaskServiceImpl(
mock(MeetingMapper.class), // mock(MeetingMapper.class),
mock(MeetingTranscriptMapper.class), // mock(MeetingTranscriptMapper.class),
mock(AiModelService.class), // mock(AiModelService.class),
new ObjectMapper(), // new ObjectMapper(),
mock(SysUserMapper.class), // mock(SysUserMapper.class),
hotWordService, // hotWordService,
mock(RedisValueSupport.class), // mock(RedisValueSupport.class),
mock(MeetingProgressService.class), // mock(MeetingProgressService.class),
mock(MeetingSummaryFileService.class), // mock(MeetingSummaryFileService.class),
mock(MeetingTranscriptFileService.class), // mock(MeetingTranscriptFileService.class),
mock(MeetingTranscriptChapterService.class), // mock(MeetingTranscriptChapterService.class),
mock(MeetingSummaryPromptAssembler.class), // mock(MeetingSummaryPromptAssembler.class),
mock(TaskSecurityContextRunner.class), // mock(TaskSecurityContextRunner.class),
mock(MeetingExternalSummaryWebhookTrigger.class), // mock(MeetingExternalSummaryWebhookTrigger.class),
mock(SysParamService.class) // mock(SysParamService.class)
); // );
ReflectionTestUtils.setField(service, "serverBaseUrl", "http://localhost:8080"); // ReflectionTestUtils.setField(service, "serverBaseUrl", "http://localhost:8080");
//
Meeting meeting = new Meeting(); // Meeting meeting = new Meeting();
meeting.setAudioUrl("/api/static/meetings/12/source audio.mp4"); // meeting.setAudioUrl("/api/static/meetings/12/source audio.mp4");
//
AiTask task = new AiTask(); // AiTask task = new AiTask();
Map<String, Object> taskConfig = new HashMap<>(); // Map<String, Object> taskConfig = new HashMap<>();
taskConfig.put("useSpkId", 1); // taskConfig.put("useSpkId", 1);
taskConfig.put("enableTextRefine", true); // taskConfig.put("enableTextRefine", true);
taskConfig.put("hotWords", List.of("汇智")); // taskConfig.put("hotWords", List.of("汇智"));
task.setTaskConfig(taskConfig); // task.setTaskConfig(taskConfig);
//
AiModelVO asrModel = new AiModelVO(); // AiModelVO asrModel = new AiModelVO();
asrModel.setModelCode("legacy-model-code"); // asrModel.setModelCode("legacy-model-code");
//
@SuppressWarnings("unchecked") // @SuppressWarnings("unchecked")
Map<String, Object> request = (Map<String, Object>) ReflectionTestUtils.invokeMethod( // Map<String, Object> request = (Map<String, Object>) ReflectionTestUtils.invokeMethod(
service, // service,
"buildAsrRequest", // "buildAsrRequest",
meeting, // meeting,
task, // task,
asrModel // asrModel
); // );
//
assertEquals("http://localhost:8080/api/static/meetings/12/source%20audio.mp4", request.get("audio_address")); // assertEquals("http://localhost:8080/api/static/meetings/12/source%20audio.mp4", request.get("audio_address"));
assertFalse(request.containsKey("file_url")); // assertFalse(request.containsKey("file_url"));
//
@SuppressWarnings("unchecked") // @SuppressWarnings("unchecked")
Map<String, Object> config = (Map<String, Object>) request.get("config"); // Map<String, Object> config = (Map<String, Object>) request.get("config");
assertEquals(Boolean.TRUE, config.get("enable_speaker")); // assertEquals(Boolean.TRUE, config.get("enable_speaker"));
assertEquals(Boolean.TRUE, config.get("match_speaker_registry")); // assertEquals(Boolean.TRUE, config.get("match_speaker_registry"));
assertEquals(Boolean.TRUE, config.get("enable_text_cleanup")); // assertEquals(Boolean.TRUE, config.get("enable_text_cleanup"));
assertFalse(config.containsKey("enable_text_refine")); // assertFalse(config.containsKey("enable_text_refine"));
assertFalse(config.containsKey("enable_two_pass")); // assertFalse(config.containsKey("enable_two_pass"));
assertFalse(config.containsKey("model")); // assertFalse(config.containsKey("model"));
//
@SuppressWarnings("unchecked") // @SuppressWarnings("unchecked")
List<Map<String, Object>> hotwords = (List<Map<String, Object>>) config.get("hotwords"); // List<Map<String, Object>> hotwords = (List<Map<String, Object>>) config.get("hotwords");
assertEquals(1, hotwords.size()); // assertEquals(1, hotwords.size());
assertEquals("汇智", hotwords.get(0).get("hotword")); // assertEquals("汇智", hotwords.get(0).get("hotword"));
assertEquals(2.5, hotwords.get(0).get("weight")); // assertEquals(2.5, hotwords.get(0).get("weight"));
} // }
//
@Test // @Test
void buildAsrRequestShouldDisableRegistryMatchWhenSpeakerSplitDisabled() { // void buildAsrRequestShouldDisableRegistryMatchWhenSpeakerSplitDisabled() {
AiTaskServiceImpl service = new AiTaskServiceImpl( // AiTaskServiceImpl service = new AiTaskServiceImpl(
mock(MeetingMapper.class), // mock(MeetingMapper.class),
mock(MeetingTranscriptMapper.class), // mock(MeetingTranscriptMapper.class),
mock(AiModelService.class), // mock(AiModelService.class),
new ObjectMapper(), // new ObjectMapper(),
mock(SysUserMapper.class), // mock(SysUserMapper.class),
mock(HotWordService.class), // mock(HotWordService.class),
mock(RedisValueSupport.class), // mock(RedisValueSupport.class),
mock(MeetingProgressService.class), // mock(MeetingProgressService.class),
mock(MeetingSummaryFileService.class), // mock(MeetingSummaryFileService.class),
mock(MeetingTranscriptFileService.class), // mock(MeetingTranscriptFileService.class),
mock(MeetingTranscriptChapterService.class), // mock(MeetingTranscriptChapterService.class),
mock(MeetingSummaryPromptAssembler.class), // mock(MeetingSummaryPromptAssembler.class),
mock(TaskSecurityContextRunner.class), // mock(TaskSecurityContextRunner.class),
mock(MeetingExternalSummaryWebhookTrigger.class), // mock(MeetingExternalSummaryWebhookTrigger.class),
mock(SysParamService.class) // mock(SysParamService.class)
); // );
ReflectionTestUtils.setField(service, "serverBaseUrl", "http://localhost:8080"); // ReflectionTestUtils.setField(service, "serverBaseUrl", "http://localhost:8080");
//
Meeting meeting = new Meeting(); // Meeting meeting = new Meeting();
meeting.setAudioUrl("/api/static/audio/demo.wav"); // meeting.setAudioUrl("/api/static/audio/demo.wav");
//
AiTask task = new AiTask(); // AiTask task = new AiTask();
Map<String, Object> taskConfig = new HashMap<>(); // Map<String, Object> taskConfig = new HashMap<>();
taskConfig.put("useSpkId", 0); // taskConfig.put("useSpkId", 0);
taskConfig.put("enableTextRefine", false); // taskConfig.put("enableTextRefine", false);
task.setTaskConfig(taskConfig); // task.setTaskConfig(taskConfig);
//
@SuppressWarnings("unchecked") // @SuppressWarnings("unchecked")
Map<String, Object> request = (Map<String, Object>) ReflectionTestUtils.invokeMethod( // Map<String, Object> request = (Map<String, Object>) ReflectionTestUtils.invokeMethod(
service, // service,
"buildAsrRequest", // "buildAsrRequest",
meeting, // meeting,
task, // task,
new AiModelVO() // new AiModelVO()
); // );
//
@SuppressWarnings("unchecked") // @SuppressWarnings("unchecked")
Map<String, Object> config = (Map<String, Object>) request.get("config"); // Map<String, Object> config = (Map<String, Object>) request.get("config");
assertEquals(Boolean.FALSE, config.get("enable_speaker")); // assertEquals(Boolean.FALSE, config.get("enable_speaker"));
assertEquals(Boolean.FALSE, config.get("match_speaker_registry")); // assertEquals(Boolean.FALSE, config.get("match_speaker_registry"));
assertEquals(Boolean.FALSE, config.get("enable_text_cleanup")); // assertEquals(Boolean.FALSE, config.get("enable_text_cleanup"));
assertTrue(((List<?>) config.get("hotwords")).isEmpty()); // assertTrue(((List<?>) config.get("hotwords")).isEmpty());
assertNull(request.get("file_url")); // assertNull(request.get("file_url"));
} // }
} //}

View File

@ -120,6 +120,18 @@ const shouldPollMeetingCard = (item: MeetingVO) =>
|| item.realtimeSessionStatus === "ACTIVE" || item.realtimeSessionStatus === "ACTIVE"
|| isPausedRealtimeSessionStatus(item.realtimeSessionStatus); || isPausedRealtimeSessionStatus(item.realtimeSessionStatus);
const getEffectiveStatus = (item: MeetingVO, progress: MeetingProgress | null) => {
if (hasLatestGenerationFailure(item)) {
return 4;
}
const status = item.displayStatus ?? item.status;
// 如果是排队中但已有进度,则视为识别中
if (status === 0 && progress && progress.percent > 0) {
return 1;
}
return status;
};
const canManageMeeting = (meeting: MeetingVO) => { const canManageMeeting = (meeting: MeetingVO) => {
try { try {
const profileStr = sessionStorage.getItem("userProfile"); const profileStr = sessionStorage.getItem("userProfile");
@ -150,8 +162,7 @@ const applyRealtimeSessionStatus = (item: MeetingVO, sessionStatus?: RealtimeMee
}; };
const IntegratedStatusTag: React.FC<{ meeting: MeetingVO; progress: MeetingProgress | null }> = ({ meeting, progress }) => { const IntegratedStatusTag: React.FC<{ meeting: MeetingVO; progress: MeetingProgress | null }> = ({ meeting, progress }) => {
const failedByLatestAttempt = hasLatestGenerationFailure(meeting); const effectiveStatus = getEffectiveStatus(meeting, progress);
const effectiveStatus = failedByLatestAttempt ? 4 : (meeting.displayStatus ?? meeting.status);
const statusConfig: Record<number, { text: string; color: string; bgColor: string; icon: React.ReactNode }> = { const statusConfig: Record<number, { text: string; color: string; bgColor: string; icon: React.ReactNode }> = {
0: { text: "排队中", color: "#8c8c8c", bgColor: "rgba(140, 140, 140, 0.1)", icon: <SyncOutlined spin /> }, 0: { text: "排队中", color: "#8c8c8c", bgColor: "rgba(140, 140, 140, 0.1)", icon: <SyncOutlined spin /> },
1: { text: "识别中", color: "#1890ff", bgColor: "rgba(24, 144, 255, 0.1)", icon: <SyncOutlined spin /> }, 1: { text: "识别中", color: "#1890ff", bgColor: "rgba(24, 144, 255, 0.1)", icon: <SyncOutlined spin /> },
@ -217,7 +228,7 @@ const MeetingCardItem: React.FC<{
fetchData: () => void; fetchData: () => void;
onOpenMeeting: (meeting: MeetingVO) => void; onOpenMeeting: (meeting: MeetingVO) => void;
}> = ({ item, config, progress, fetchData, onOpenMeeting }) => { }> = ({ item, config, progress, fetchData, onOpenMeeting }) => {
const effectiveStatus = hasLatestGenerationFailure(item) ? 4 : (item.displayStatus ?? item.status); const effectiveStatus = getEffectiveStatus(item, progress);
const isProcessing = shouldTrackGenerationProgress(item); const isProcessing = shouldTrackGenerationProgress(item);
const isPaused = effectiveStatus === PAUSED_DISPLAY_STATUS; const isPaused = effectiveStatus === PAUSED_DISPLAY_STATUS;
const isRealtimeActive = effectiveStatus === REALTIME_ACTIVE_DISPLAY_STATUS; const isRealtimeActive = effectiveStatus === REALTIME_ACTIVE_DISPLAY_STATUS;
@ -821,13 +832,14 @@ const Meetings: React.FC = () => {
grid={{ gutter: [20, 20], xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }} grid={{ gutter: [20, 20], xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }}
dataSource={data} dataSource={data}
renderItem={(item) => { renderItem={(item) => {
const visualStatus = hasLatestGenerationFailure(item) ? 4 : (item.displayStatus ?? item.status); const progress = progressMap[item.id] || null;
const visualStatus = getEffectiveStatus(item, progress);
const config = statusConfig[visualStatus] || statusConfig[0]; const config = statusConfig[visualStatus] || statusConfig[0];
return ( return (
<MeetingCardItem <MeetingCardItem
item={item} item={item}
config={config} config={config}
progress={progressMap[item.id] || null} progress={progress}
fetchData={() => void fetchData()} fetchData={() => void fetchData()}
onOpenMeeting={handleOpenMeeting} onOpenMeeting={handleOpenMeeting}
/> />