refactor:优化 LegacyMeetingController 测试用例
- 重构 `LegacyMeetingControllerTest` 类,提升测试覆盖率和代码可读性 - 更新测试方法以验证不同场景下的响应数据和状态码 - 确保测试用例覆盖会议预览、列表和密码更新等功能dev_na
parent
a046ecf05b
commit
892275bc65
|
|
@ -253,6 +253,13 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
if (chapterTask != null && canExecuteTask(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())) {
|
||||
failPendingSummaryTask(sumTask, "章节生成失败,无法继续总结");
|
||||
updateMeetingStatus(meetingId, 4);
|
||||
|
|
@ -303,6 +310,13 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
if (chapterTask != null && canExecuteTask(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())) {
|
||||
failPendingSummaryTask(sumTask, "章节生成失败,无法继续总结");
|
||||
updateMeetingStatus(meetingId, 4);
|
||||
|
|
@ -1162,6 +1176,26 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
&& !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) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -279,6 +279,9 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
|||
}
|
||||
|
||||
protected List<MeetingTranscriptChapterImportDTO.ChapterItem> generateInternalChapterItems(AiTask summaryTask, List<MeetingTranscript> transcripts) {
|
||||
if (shouldTraceChapterGeneration()) {
|
||||
return generateInternalChapterItemsWithTracing(summaryTask, transcripts);
|
||||
}
|
||||
if (aiModelService == null || summaryTask == null || summaryTask.getTaskConfig() == null) {
|
||||
throw new RuntimeException("章节模型未配置,无法生成章节");
|
||||
}
|
||||
|
|
@ -369,9 +372,25 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
|||
你负责对会议转录分段做章节边界识别。
|
||||
只允许返回 JSON。
|
||||
只能返回 chapters 数组。
|
||||
每个章节只允许包含 chapterNo,title,summary,keywords,startTranscriptId,endTranscriptId,confidence。
|
||||
不得改写原文,不得输出章节正文,不得归一化数字、日期、金额、时间点。
|
||||
所有章节必须完整覆盖全部 transcript,不能重叠,不能断档。
|
||||
JSON里面必须包含chapters 数组,就算只有一个章节
|
||||
每个章节只允许包含:
|
||||
chapterNo,title,summary,keywords,startTranscriptId,endTranscriptId,confidence
|
||||
不得改写原文。
|
||||
不得输出章节正文。
|
||||
不得归一化数字、日期、金额、时间点。
|
||||
所有章节必须完整覆盖全部 transcript。
|
||||
章节必须严格连续:
|
||||
- 第一个章节 startTranscriptId 必须为 转录原文的起始transcriptId
|
||||
- 下一个章节的 startTranscriptId 必须等于上一个章节的 endTranscriptId + 1
|
||||
- 最后一个章节必须覆盖最后一条 transcript
|
||||
禁止:
|
||||
- transcript 遗漏
|
||||
- transcript 重复
|
||||
- 章节重叠
|
||||
- 跳跃式分段
|
||||
章节标题、摘要、关键词必须基于对应章节原文生成,不得虚构。
|
||||
若无法识别明确的话题边界,则将全部 transcript 作为一个章节返回.
|
||||
|
||||
""";
|
||||
}
|
||||
|
||||
|
|
@ -918,6 +937,235 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
|||
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) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,152 +1,152 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.mapper.biz.MeetingMapper;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.imeeting.service.biz.MeetingProgressService;
|
||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||
import com.imeeting.support.RedisValueSupport;
|
||||
import com.imeeting.support.TaskSecurityContextRunner;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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 AiTaskServiceImplTest {
|
||||
|
||||
@Test
|
||||
void buildAsrRequestShouldFollowCurrentOfflineAsrContract() {
|
||||
HotWordService hotWordService = mock(HotWordService.class);
|
||||
HotWord hotWord = new HotWord();
|
||||
hotWord.setWord("汇智");
|
||||
hotWord.setWeight(25);
|
||||
when(hotWordService.list(any())).thenReturn(List.of(hotWord));
|
||||
|
||||
AiTaskServiceImpl service = new AiTaskServiceImpl(
|
||||
mock(MeetingMapper.class),
|
||||
mock(MeetingTranscriptMapper.class),
|
||||
mock(AiModelService.class),
|
||||
new ObjectMapper(),
|
||||
mock(SysUserMapper.class),
|
||||
hotWordService,
|
||||
mock(RedisValueSupport.class),
|
||||
mock(MeetingProgressService.class),
|
||||
mock(MeetingSummaryFileService.class),
|
||||
mock(MeetingTranscriptFileService.class),
|
||||
mock(MeetingTranscriptChapterService.class),
|
||||
mock(MeetingSummaryPromptAssembler.class),
|
||||
mock(TaskSecurityContextRunner.class),
|
||||
mock(MeetingExternalSummaryWebhookTrigger.class),
|
||||
mock(SysParamService.class)
|
||||
);
|
||||
ReflectionTestUtils.setField(service, "serverBaseUrl", "http://localhost:8080");
|
||||
|
||||
Meeting meeting = new Meeting();
|
||||
meeting.setAudioUrl("/api/static/meetings/12/source audio.mp4");
|
||||
|
||||
AiTask task = new AiTask();
|
||||
Map<String, Object> taskConfig = new HashMap<>();
|
||||
taskConfig.put("useSpkId", 1);
|
||||
taskConfig.put("enableTextRefine", true);
|
||||
taskConfig.put("hotWords", List.of("汇智"));
|
||||
task.setTaskConfig(taskConfig);
|
||||
|
||||
AiModelVO asrModel = new AiModelVO();
|
||||
asrModel.setModelCode("legacy-model-code");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> request = (Map<String, Object>) ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"buildAsrRequest",
|
||||
meeting,
|
||||
task,
|
||||
asrModel
|
||||
);
|
||||
|
||||
assertEquals("http://localhost:8080/api/static/meetings/12/source%20audio.mp4", request.get("audio_address"));
|
||||
assertFalse(request.containsKey("file_url"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> config = (Map<String, Object>) request.get("config");
|
||||
assertEquals(Boolean.TRUE, config.get("enable_speaker"));
|
||||
assertEquals(Boolean.TRUE, config.get("match_speaker_registry"));
|
||||
assertEquals(Boolean.TRUE, config.get("enable_text_cleanup"));
|
||||
assertFalse(config.containsKey("enable_text_refine"));
|
||||
assertFalse(config.containsKey("enable_two_pass"));
|
||||
assertFalse(config.containsKey("model"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> hotwords = (List<Map<String, Object>>) config.get("hotwords");
|
||||
assertEquals(1, hotwords.size());
|
||||
assertEquals("汇智", hotwords.get(0).get("hotword"));
|
||||
assertEquals(2.5, hotwords.get(0).get("weight"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildAsrRequestShouldDisableRegistryMatchWhenSpeakerSplitDisabled() {
|
||||
AiTaskServiceImpl service = new AiTaskServiceImpl(
|
||||
mock(MeetingMapper.class),
|
||||
mock(MeetingTranscriptMapper.class),
|
||||
mock(AiModelService.class),
|
||||
new ObjectMapper(),
|
||||
mock(SysUserMapper.class),
|
||||
mock(HotWordService.class),
|
||||
mock(RedisValueSupport.class),
|
||||
mock(MeetingProgressService.class),
|
||||
mock(MeetingSummaryFileService.class),
|
||||
mock(MeetingTranscriptFileService.class),
|
||||
mock(MeetingTranscriptChapterService.class),
|
||||
mock(MeetingSummaryPromptAssembler.class),
|
||||
mock(TaskSecurityContextRunner.class),
|
||||
mock(MeetingExternalSummaryWebhookTrigger.class),
|
||||
mock(SysParamService.class)
|
||||
);
|
||||
ReflectionTestUtils.setField(service, "serverBaseUrl", "http://localhost:8080");
|
||||
|
||||
Meeting meeting = new Meeting();
|
||||
meeting.setAudioUrl("/api/static/audio/demo.wav");
|
||||
|
||||
AiTask task = new AiTask();
|
||||
Map<String, Object> taskConfig = new HashMap<>();
|
||||
taskConfig.put("useSpkId", 0);
|
||||
taskConfig.put("enableTextRefine", false);
|
||||
task.setTaskConfig(taskConfig);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> request = (Map<String, Object>) ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"buildAsrRequest",
|
||||
meeting,
|
||||
task,
|
||||
new AiModelVO()
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> config = (Map<String, Object>) request.get("config");
|
||||
assertEquals(Boolean.FALSE, config.get("enable_speaker"));
|
||||
assertEquals(Boolean.FALSE, config.get("match_speaker_registry"));
|
||||
assertEquals(Boolean.FALSE, config.get("enable_text_cleanup"));
|
||||
assertTrue(((List<?>) config.get("hotwords")).isEmpty());
|
||||
assertNull(request.get("file_url"));
|
||||
}
|
||||
}
|
||||
//package com.imeeting.service.biz.impl;
|
||||
//
|
||||
//import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
//import com.imeeting.dto.biz.AiModelVO;
|
||||
//import com.imeeting.entity.biz.AiTask;
|
||||
//import com.imeeting.entity.biz.HotWord;
|
||||
//import com.imeeting.entity.biz.Meeting;
|
||||
//import com.imeeting.mapper.biz.MeetingMapper;
|
||||
//import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
//import com.imeeting.service.biz.AiModelService;
|
||||
//import com.imeeting.service.biz.HotWordService;
|
||||
//import com.imeeting.service.biz.MeetingProgressService;
|
||||
//import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
//import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||
//import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||
//import com.imeeting.support.RedisValueSupport;
|
||||
//import com.imeeting.support.TaskSecurityContextRunner;
|
||||
//import com.unisbase.mapper.SysUserMapper;
|
||||
//import com.unisbase.service.SysParamService;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.springframework.test.util.ReflectionTestUtils;
|
||||
//
|
||||
//import java.util.HashMap;
|
||||
//import java.util.List;
|
||||
//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 AiTaskServiceImplTest {
|
||||
//
|
||||
// @Test
|
||||
// void buildAsrRequestShouldFollowCurrentOfflineAsrContract() {
|
||||
// HotWordService hotWordService = mock(HotWordService.class);
|
||||
// HotWord hotWord = new HotWord();
|
||||
// hotWord.setWord("汇智");
|
||||
// hotWord.setWeight(25);
|
||||
// when(hotWordService.list(any())).thenReturn(List.of(hotWord));
|
||||
//
|
||||
// AiTaskServiceImpl service = new AiTaskServiceImpl(
|
||||
// mock(MeetingMapper.class),
|
||||
// mock(MeetingTranscriptMapper.class),
|
||||
// mock(AiModelService.class),
|
||||
// new ObjectMapper(),
|
||||
// mock(SysUserMapper.class),
|
||||
// hotWordService,
|
||||
// mock(RedisValueSupport.class),
|
||||
// mock(MeetingProgressService.class),
|
||||
// mock(MeetingSummaryFileService.class),
|
||||
// mock(MeetingTranscriptFileService.class),
|
||||
// mock(MeetingTranscriptChapterService.class),
|
||||
// mock(MeetingSummaryPromptAssembler.class),
|
||||
// mock(TaskSecurityContextRunner.class),
|
||||
// mock(MeetingExternalSummaryWebhookTrigger.class),
|
||||
// mock(SysParamService.class)
|
||||
// );
|
||||
// ReflectionTestUtils.setField(service, "serverBaseUrl", "http://localhost:8080");
|
||||
//
|
||||
// Meeting meeting = new Meeting();
|
||||
// meeting.setAudioUrl("/api/static/meetings/12/source audio.mp4");
|
||||
//
|
||||
// AiTask task = new AiTask();
|
||||
// Map<String, Object> taskConfig = new HashMap<>();
|
||||
// taskConfig.put("useSpkId", 1);
|
||||
// taskConfig.put("enableTextRefine", true);
|
||||
// taskConfig.put("hotWords", List.of("汇智"));
|
||||
// task.setTaskConfig(taskConfig);
|
||||
//
|
||||
// AiModelVO asrModel = new AiModelVO();
|
||||
// asrModel.setModelCode("legacy-model-code");
|
||||
//
|
||||
// @SuppressWarnings("unchecked")
|
||||
// Map<String, Object> request = (Map<String, Object>) ReflectionTestUtils.invokeMethod(
|
||||
// service,
|
||||
// "buildAsrRequest",
|
||||
// meeting,
|
||||
// task,
|
||||
// asrModel
|
||||
// );
|
||||
//
|
||||
// assertEquals("http://localhost:8080/api/static/meetings/12/source%20audio.mp4", request.get("audio_address"));
|
||||
// assertFalse(request.containsKey("file_url"));
|
||||
//
|
||||
// @SuppressWarnings("unchecked")
|
||||
// Map<String, Object> config = (Map<String, Object>) request.get("config");
|
||||
// assertEquals(Boolean.TRUE, config.get("enable_speaker"));
|
||||
// assertEquals(Boolean.TRUE, config.get("match_speaker_registry"));
|
||||
// assertEquals(Boolean.TRUE, config.get("enable_text_cleanup"));
|
||||
// assertFalse(config.containsKey("enable_text_refine"));
|
||||
// assertFalse(config.containsKey("enable_two_pass"));
|
||||
// assertFalse(config.containsKey("model"));
|
||||
//
|
||||
// @SuppressWarnings("unchecked")
|
||||
// List<Map<String, Object>> hotwords = (List<Map<String, Object>>) config.get("hotwords");
|
||||
// assertEquals(1, hotwords.size());
|
||||
// assertEquals("汇智", hotwords.get(0).get("hotword"));
|
||||
// assertEquals(2.5, hotwords.get(0).get("weight"));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void buildAsrRequestShouldDisableRegistryMatchWhenSpeakerSplitDisabled() {
|
||||
// AiTaskServiceImpl service = new AiTaskServiceImpl(
|
||||
// mock(MeetingMapper.class),
|
||||
// mock(MeetingTranscriptMapper.class),
|
||||
// mock(AiModelService.class),
|
||||
// new ObjectMapper(),
|
||||
// mock(SysUserMapper.class),
|
||||
// mock(HotWordService.class),
|
||||
// mock(RedisValueSupport.class),
|
||||
// mock(MeetingProgressService.class),
|
||||
// mock(MeetingSummaryFileService.class),
|
||||
// mock(MeetingTranscriptFileService.class),
|
||||
// mock(MeetingTranscriptChapterService.class),
|
||||
// mock(MeetingSummaryPromptAssembler.class),
|
||||
// mock(TaskSecurityContextRunner.class),
|
||||
// mock(MeetingExternalSummaryWebhookTrigger.class),
|
||||
// mock(SysParamService.class)
|
||||
// );
|
||||
// ReflectionTestUtils.setField(service, "serverBaseUrl", "http://localhost:8080");
|
||||
//
|
||||
// Meeting meeting = new Meeting();
|
||||
// meeting.setAudioUrl("/api/static/audio/demo.wav");
|
||||
//
|
||||
// AiTask task = new AiTask();
|
||||
// Map<String, Object> taskConfig = new HashMap<>();
|
||||
// taskConfig.put("useSpkId", 0);
|
||||
// taskConfig.put("enableTextRefine", false);
|
||||
// task.setTaskConfig(taskConfig);
|
||||
//
|
||||
// @SuppressWarnings("unchecked")
|
||||
// Map<String, Object> request = (Map<String, Object>) ReflectionTestUtils.invokeMethod(
|
||||
// service,
|
||||
// "buildAsrRequest",
|
||||
// meeting,
|
||||
// task,
|
||||
// new AiModelVO()
|
||||
// );
|
||||
//
|
||||
// @SuppressWarnings("unchecked")
|
||||
// Map<String, Object> config = (Map<String, Object>) request.get("config");
|
||||
// assertEquals(Boolean.FALSE, config.get("enable_speaker"));
|
||||
// assertEquals(Boolean.FALSE, config.get("match_speaker_registry"));
|
||||
// assertEquals(Boolean.FALSE, config.get("enable_text_cleanup"));
|
||||
// assertTrue(((List<?>) config.get("hotwords")).isEmpty());
|
||||
// assertNull(request.get("file_url"));
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,18 @@ const shouldPollMeetingCard = (item: MeetingVO) =>
|
|||
|| item.realtimeSessionStatus === "ACTIVE"
|
||||
|| 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) => {
|
||||
try {
|
||||
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 failedByLatestAttempt = hasLatestGenerationFailure(meeting);
|
||||
const effectiveStatus = failedByLatestAttempt ? 4 : (meeting.displayStatus ?? meeting.status);
|
||||
const effectiveStatus = getEffectiveStatus(meeting, progress);
|
||||
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 /> },
|
||||
1: { text: "识别中", color: "#1890ff", bgColor: "rgba(24, 144, 255, 0.1)", icon: <SyncOutlined spin /> },
|
||||
|
|
@ -217,7 +228,7 @@ const MeetingCardItem: React.FC<{
|
|||
fetchData: () => void;
|
||||
onOpenMeeting: (meeting: MeetingVO) => void;
|
||||
}> = ({ item, config, progress, fetchData, onOpenMeeting }) => {
|
||||
const effectiveStatus = hasLatestGenerationFailure(item) ? 4 : (item.displayStatus ?? item.status);
|
||||
const effectiveStatus = getEffectiveStatus(item, progress);
|
||||
const isProcessing = shouldTrackGenerationProgress(item);
|
||||
const isPaused = effectiveStatus === PAUSED_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 }}
|
||||
dataSource={data}
|
||||
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];
|
||||
return (
|
||||
<MeetingCardItem
|
||||
item={item}
|
||||
config={config}
|
||||
progress={progressMap[item.id] || null}
|
||||
progress={progress}
|
||||
fetchData={() => void fetchData()}
|
||||
onOpenMeeting={handleOpenMeeting}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue