feat:优化会议转录和章节功能
- 更新 `MeetingTranscriptFileServiceImpl`,使用 Markdown 格式导出会议转录 - 在 `MeetingQueryServiceImpl` 中添加会议存在性检查,并调用 `listDisplayChapterAnalysis` - 在 `MeetingCommandServiceImpl` 中更新会议进度信息 - 在前端 `http.ts` 和 `meeting.ts` 中添加 `suppressErrorToast` 选项 - 在 `MeetingVO` 中添加最近一次总结和章节尝试的任务状态及错误信息 - 更新 `MeetingDetail.tsx` 和 `Meetings.tsx`,处理生成失败的提示和展示逻辑 - 在 `MeetingTranscriptChapterService` 和 `MeetingTranscriptChapterServiceImpl` 中添加加载当前章节 Markdown 的方法 - 优化 `PageContainer` 和 `Meetings` 页面布局,改善滚动和内容展示dev_na
parent
1877c64cc2
commit
ccb408ade5
|
|
@ -57,6 +57,18 @@ public class MeetingVO {
|
||||||
private String lastUserPrompt;
|
private String lastUserPrompt;
|
||||||
@Schema(description = "分析结果")
|
@Schema(description = "分析结果")
|
||||||
private Map<String, Object> analysis;
|
private Map<String, Object> analysis;
|
||||||
|
@Schema(description = "最近一次总结尝试任务 ID")
|
||||||
|
private Long latestSummaryAttemptTaskId;
|
||||||
|
@Schema(description = "最近一次总结尝试任务状态")
|
||||||
|
private Integer latestSummaryAttemptStatus;
|
||||||
|
@Schema(description = "最近一次总结尝试错误信息")
|
||||||
|
private String latestSummaryAttemptErrorMsg;
|
||||||
|
@Schema(description = "最近一次章节尝试任务 ID")
|
||||||
|
private Long latestChapterAttemptTaskId;
|
||||||
|
@Schema(description = "最近一次章节尝试任务状态")
|
||||||
|
private Integer latestChapterAttemptStatus;
|
||||||
|
@Schema(description = "最近一次章节尝试错误信息")
|
||||||
|
private String latestChapterAttemptErrorMsg;
|
||||||
@Schema(description = "会议状态")
|
@Schema(description = "会议状态")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ public interface MeetingTranscriptChapterService {
|
||||||
|
|
||||||
List<Map<String, Object>> listCurrentChapterAnalysis(Long meetingId);
|
List<Map<String, Object>> listCurrentChapterAnalysis(Long meetingId);
|
||||||
|
|
||||||
|
List<Map<String, Object>> listDisplayChapterAnalysis(Meeting meeting);
|
||||||
|
|
||||||
void invalidateCurrentVersion(Long meetingId);
|
void invalidateCurrentVersion(Long meetingId);
|
||||||
|
|
||||||
MeetingTranscriptChapterVersion importExternalChapters(Meeting meeting, AiTask sourceTask, MeetingTranscriptChapterImportDTO command);
|
MeetingTranscriptChapterVersion importExternalChapters(Meeting meeting, AiTask sourceTask, MeetingTranscriptChapterImportDTO command);
|
||||||
|
|
@ -22,4 +24,6 @@ public interface MeetingTranscriptChapterService {
|
||||||
MeetingTranscriptSourceVO buildTranscriptSource(Long meetingId);
|
MeetingTranscriptSourceVO buildTranscriptSource(Long meetingId);
|
||||||
|
|
||||||
MeetingTranscriptChapterVersion getCurrentVersion(Long meetingId);
|
MeetingTranscriptChapterVersion getCurrentVersion(Long meetingId);
|
||||||
|
|
||||||
|
String loadCurrentChapterMarkdown(Meeting meeting);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,6 @@ public interface MeetingTranscriptFileService {
|
||||||
void initializeTranscriptFileIfAbsent(Long meetingId);
|
void initializeTranscriptFileIfAbsent(Long meetingId);
|
||||||
|
|
||||||
MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail);
|
MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail);
|
||||||
|
|
||||||
|
String loadTranscriptMarkdown(Meeting meeting, MeetingVO meetingDetail);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -735,6 +735,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
}
|
}
|
||||||
meeting.setStatus(2);
|
meeting.setStatus(2);
|
||||||
meetingService.updateById(meeting);
|
meetingService.updateById(meeting);
|
||||||
|
if ("EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) {
|
||||||
|
updateMeetingProgress(meetingId, 95, "等待外部章节与总结编排...", 0);
|
||||||
|
} else {
|
||||||
|
updateMeetingProgress(meetingId, 85, "重新总结已提交,正在生成章节...", 0);
|
||||||
|
}
|
||||||
dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,7 @@ public class MeetingDomainSupport {
|
||||||
vo.setSummaryContent(meetingSummaryFileService.loadSummaryContent(meeting));
|
vo.setSummaryContent(meetingSummaryFileService.loadSummaryContent(meeting));
|
||||||
vo.setAnalysis(meetingSummaryFileService.loadSummaryAnalysis(meeting));
|
vo.setAnalysis(meetingSummaryFileService.loadSummaryAnalysis(meeting));
|
||||||
vo.setLastUserPrompt(resolveLastSummaryUserPrompt(meeting));
|
vo.setLastUserPrompt(resolveLastSummaryUserPrompt(meeting));
|
||||||
|
fillLatestTaskAttemptInfo(meeting, vo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,6 +380,41 @@ public class MeetingDomainSupport {
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fillLatestTaskAttemptInfo(Meeting meeting, com.imeeting.dto.biz.MeetingVO vo) {
|
||||||
|
AiTask latestSummaryAttempt = resolveLatestTaskAttempt(meeting, "SUMMARY");
|
||||||
|
if (latestSummaryAttempt != null) {
|
||||||
|
vo.setLatestSummaryAttemptTaskId(latestSummaryAttempt.getId());
|
||||||
|
vo.setLatestSummaryAttemptStatus(latestSummaryAttempt.getStatus());
|
||||||
|
vo.setLatestSummaryAttemptErrorMsg(normalizeTaskError(latestSummaryAttempt.getErrorMsg()));
|
||||||
|
}
|
||||||
|
|
||||||
|
AiTask latestChapterAttempt = resolveLatestTaskAttempt(meeting, "CHAPTER");
|
||||||
|
if (latestChapterAttempt != null) {
|
||||||
|
vo.setLatestChapterAttemptTaskId(latestChapterAttempt.getId());
|
||||||
|
vo.setLatestChapterAttemptStatus(latestChapterAttempt.getStatus());
|
||||||
|
vo.setLatestChapterAttemptErrorMsg(normalizeTaskError(latestChapterAttempt.getErrorMsg()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiTask resolveLatestTaskAttempt(Meeting meeting, String taskType) {
|
||||||
|
if (meeting == null || meeting.getId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getMeetingId, meeting.getId())
|
||||||
|
.eq(AiTask::getTaskType, taskType)
|
||||||
|
.orderByDesc(AiTask::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeTaskError(String errorMsg) {
|
||||||
|
if (errorMsg == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = errorMsg.trim();
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
private record AudioRelocationPlan(Path sourcePath, Path targetPath, Path backupPath, String relocatedUrl) {
|
private record AudioRelocationPlan(Path sourcePath, Path targetPath, Path backupPath, String relocatedUrl) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,11 @@ public class MeetingQueryServiceImpl implements MeetingQueryService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Map<String, Object>> getChapters(Long meetingId) {
|
public List<Map<String, Object>> getChapters(Long meetingId) {
|
||||||
return meetingTranscriptChapterService.listCurrentChapterAnalysis(meetingId);
|
Meeting meeting = meetingService.getById(meetingId);
|
||||||
|
if (meeting == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return meetingTranscriptChapterService.listDisplayChapterAnalysis(meeting);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.entity.biz.MeetingTranscript;
|
import com.imeeting.entity.biz.MeetingTranscript;
|
||||||
import com.imeeting.entity.biz.MeetingTranscriptChapter;
|
import com.imeeting.entity.biz.MeetingTranscriptChapter;
|
||||||
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
|
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
|
||||||
|
import com.imeeting.mapper.biz.AiTaskMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingTranscriptChapterMapper;
|
import com.imeeting.mapper.biz.MeetingTranscriptChapterMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingTranscriptChapterVersionMapper;
|
import com.imeeting.mapper.biz.MeetingTranscriptChapterVersionMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||||
|
|
@ -71,6 +72,7 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
||||||
private final MeetingTranscriptMapper transcriptMapper;
|
private final MeetingTranscriptMapper transcriptMapper;
|
||||||
private final MeetingTranscriptChapterVersionMapper versionMapper;
|
private final MeetingTranscriptChapterVersionMapper versionMapper;
|
||||||
private final MeetingTranscriptChapterMapper chapterMapper;
|
private final MeetingTranscriptChapterMapper chapterMapper;
|
||||||
|
private final AiTaskMapper aiTaskMapper;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private AiModelService aiModelService;
|
private AiModelService aiModelService;
|
||||||
|
|
@ -129,10 +131,26 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
return listVersionChapterAnalysis(meetingId, current.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> listDisplayChapterAnalysis(Meeting meeting) {
|
||||||
|
if (meeting == null || meeting.getId() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
MeetingTranscriptChapterVersion displayVersion = resolveDisplayVersion(meeting);
|
||||||
|
if (displayVersion == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return listVersionChapterAnalysis(meeting.getId(), displayVersion.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> listVersionChapterAnalysis(Long meetingId, Long versionId) {
|
||||||
List<MeetingTranscript> transcripts = loadRawTranscripts(meetingId);
|
List<MeetingTranscript> transcripts = loadRawTranscripts(meetingId);
|
||||||
Map<Long, MeetingTranscript> transcriptById = transcripts.stream()
|
Map<Long, MeetingTranscript> transcriptById = transcripts.stream()
|
||||||
.collect(Collectors.toMap(MeetingTranscript::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
|
.collect(Collectors.toMap(MeetingTranscript::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
|
||||||
return loadVersionChapters(current.getId()).stream()
|
return loadVersionChapters(versionId).stream()
|
||||||
.map(chapter -> toChapterAnalysis(chapter, transcriptById))
|
.map(chapter -> toChapterAnalysis(chapter, transcriptById))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
@ -200,6 +218,29 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
||||||
return findCurrentVersion(meetingId);
|
return findCurrentVersion(meetingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String loadCurrentChapterMarkdown(Meeting meeting) {
|
||||||
|
if (meeting == null || meeting.getId() == null) {
|
||||||
|
throw new RuntimeException("Meeting not found");
|
||||||
|
}
|
||||||
|
MeetingTranscriptChapterVersion current = findCurrentVersion(meeting.getId());
|
||||||
|
if (current == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
List<MeetingTranscript> transcripts = loadRawTranscripts(meeting.getId());
|
||||||
|
Map<Long, MeetingTranscript> transcriptById = transcripts.stream()
|
||||||
|
.collect(Collectors.toMap(MeetingTranscript::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
|
||||||
|
List<MeetingTranscriptChapter> chapters = loadVersionChapters(current.getId());
|
||||||
|
String relativePath = writeCurrentChapterMarkdown(meeting, current, chapters, transcriptById);
|
||||||
|
try {
|
||||||
|
String basePath = uploadPath.endsWith("/") || uploadPath.endsWith("\\") ? uploadPath : uploadPath + "/";
|
||||||
|
Path targetPath = Paths.get(basePath, relativePath.replace("\\", "/"));
|
||||||
|
return Files.exists(targetPath) ? Files.readString(targetPath, StandardCharsets.UTF_8) : "";
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException("Failed to load meeting chapter markdown", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private MeetingTranscriptChapterVersion generateInternalVersion(Meeting meeting,
|
private MeetingTranscriptChapterVersion generateInternalVersion(Meeting meeting,
|
||||||
AiTask summaryTask,
|
AiTask summaryTask,
|
||||||
List<MeetingTranscript> transcripts,
|
List<MeetingTranscript> transcripts,
|
||||||
|
|
@ -397,6 +438,49 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MeetingTranscriptChapterVersion resolveDisplayVersion(Meeting meeting) {
|
||||||
|
Long chapterVersionId = extractChapterVersionId(resolveDisplaySummaryTask(meeting));
|
||||||
|
if (chapterVersionId != null) {
|
||||||
|
MeetingTranscriptChapterVersion version = versionMapper.selectById(chapterVersionId);
|
||||||
|
if (version != null && Objects.equals(version.getMeetingId(), meeting.getId())) {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findCurrentVersion(meeting.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiTask resolveDisplaySummaryTask(Meeting meeting) {
|
||||||
|
if (meeting.getLatestSummaryTaskId() != null) {
|
||||||
|
AiTask task = aiTaskMapper.selectById(meeting.getLatestSummaryTaskId());
|
||||||
|
if (task != null
|
||||||
|
&& Objects.equals(task.getMeetingId(), meeting.getId())
|
||||||
|
&& "SUMMARY".equals(task.getTaskType())
|
||||||
|
&& Integer.valueOf(2).equals(task.getStatus())) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aiTaskMapper.selectOne(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getMeetingId, meeting.getId())
|
||||||
|
.eq(AiTask::getTaskType, "SUMMARY")
|
||||||
|
.eq(AiTask::getStatus, 2)
|
||||||
|
.orderByDesc(AiTask::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long extractChapterVersionId(AiTask summaryTask) {
|
||||||
|
if (summaryTask == null || summaryTask.getResponseData() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object summarySource = summaryTask.getResponseData().get("summarySource");
|
||||||
|
if (summarySource instanceof Map<?, ?> sourceMap) {
|
||||||
|
Long versionId = longValue(sourceMap.get("chapterVersionId"));
|
||||||
|
if (versionId != null) {
|
||||||
|
return versionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return longValue(summaryTask.getResponseData().get("chapterVersionId"));
|
||||||
|
}
|
||||||
|
|
||||||
private MeetingSummarySource buildSummarySource(Meeting meeting,
|
private MeetingSummarySource buildSummarySource(Meeting meeting,
|
||||||
MeetingTranscriptChapterVersion version,
|
MeetingTranscriptChapterVersion version,
|
||||||
List<MeetingTranscript> transcripts,
|
List<MeetingTranscript> transcripts,
|
||||||
|
|
|
||||||
|
|
@ -49,15 +49,16 @@ public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileSe
|
||||||
if (meeting == null) {
|
if (meeting == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writeTranscriptFile(meeting, null);
|
loadTranscriptMarkdown(meeting, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail) {
|
public MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail) {
|
||||||
if (meeting == null || meeting.getId() == null) {
|
if (meeting == null || meeting.getId() == null) {
|
||||||
throw new RuntimeException("会议不存在");
|
throw new RuntimeException("Meeting not found");
|
||||||
}
|
}
|
||||||
byte[] content = writeTranscriptFile(meeting, meetingDetail);
|
String markdown = loadTranscriptMarkdown(meeting, meetingDetail);
|
||||||
|
byte[] content = markdown.getBytes(StandardCharsets.UTF_8);
|
||||||
String safeTitle = sanitizeFileName(
|
String safeTitle = sanitizeFileName(
|
||||||
meetingDetail != null ? meetingDetail.getTitle() : meeting.getTitle(),
|
meetingDetail != null ? meetingDetail.getTitle() : meeting.getTitle(),
|
||||||
"meeting-transcript-" + meeting.getId()
|
"meeting-transcript-" + meeting.getId()
|
||||||
|
|
@ -65,7 +66,8 @@ public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileSe
|
||||||
return new MeetingTranscriptExportResult(content, CONTENT_TYPE, safeTitle + "-Transcript.md");
|
return new MeetingTranscriptExportResult(content, CONTENT_TYPE, safeTitle + "-Transcript.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] writeTranscriptFile(Meeting meeting, MeetingVO meetingDetail) {
|
@Override
|
||||||
|
public String loadTranscriptMarkdown(Meeting meeting, MeetingVO meetingDetail) {
|
||||||
try {
|
try {
|
||||||
Path transcriptPath = buildTranscriptPath(meeting.getId());
|
Path transcriptPath = buildTranscriptPath(meeting.getId());
|
||||||
Path parent = transcriptPath.getParent();
|
Path parent = transcriptPath.getParent();
|
||||||
|
|
@ -74,9 +76,9 @@ public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileSe
|
||||||
}
|
}
|
||||||
String markdown = buildTranscriptMarkdown(meeting, meetingDetail);
|
String markdown = buildTranscriptMarkdown(meeting, meetingDetail);
|
||||||
Files.writeString(transcriptPath, markdown, StandardCharsets.UTF_8);
|
Files.writeString(transcriptPath, markdown, StandardCharsets.UTF_8);
|
||||||
return markdown.getBytes(StandardCharsets.UTF_8);
|
return markdown;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new RuntimeException("写入会议转录文件失败", ex);
|
throw new RuntimeException("Failed to write meeting transcript markdown", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ export interface MeetingVO {
|
||||||
keyPoints?: Array<{ title?: string; summary?: string; speaker?: string; time?: string }>;
|
keyPoints?: Array<{ title?: string; summary?: string; speaker?: string; time?: string }>;
|
||||||
todos?: string[];
|
todos?: string[];
|
||||||
};
|
};
|
||||||
|
latestSummaryAttemptTaskId?: number;
|
||||||
|
latestSummaryAttemptStatus?: number;
|
||||||
|
latestSummaryAttemptErrorMsg?: string;
|
||||||
|
latestChapterAttemptTaskId?: number;
|
||||||
|
latestChapterAttemptStatus?: number;
|
||||||
|
latestChapterAttemptErrorMsg?: string;
|
||||||
status: number;
|
status: number;
|
||||||
displayStatus?: number;
|
displayStatus?: number;
|
||||||
realtimeSessionStatus?: RealtimeMeetingSessionStatus["status"];
|
realtimeSessionStatus?: RealtimeMeetingSessionStatus["status"];
|
||||||
|
|
@ -269,11 +275,12 @@ export interface PublicMeetingPreviewVO {
|
||||||
chapters?: MeetingChapterVO[];
|
chapters?: MeetingChapterVO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMeetingDetail = (id: number) => {
|
export const getMeetingDetail = (id: number, options?: { suppressErrorToast?: boolean }) => {
|
||||||
return http.get<{ code: string; data: MeetingVO; msg: string }>(
|
return http.get<{ code: string; data: MeetingVO; msg: string }>(
|
||||||
`/api/biz/meeting/${id}`,
|
`/api/biz/meeting/${id}`,
|
||||||
{
|
{
|
||||||
timeout: MEETING_DETAIL_TIMEOUT,
|
timeout: MEETING_DETAIL_TIMEOUT,
|
||||||
|
suppressErrorToast: options?.suppressErrorToast,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -403,9 +410,12 @@ export interface MeetingProgress {
|
||||||
eta?: number;
|
eta?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMeetingProgress = (id: number) => {
|
export const getMeetingProgress = (id: number, options?: { suppressErrorToast?: boolean }) => {
|
||||||
return http.get<{ code: string; data: MeetingProgress; msg: string }>(
|
return http.get<{ code: string; data: MeetingProgress; msg: string }>(
|
||||||
`/api/biz/meeting/${id}/progress`
|
`/api/biz/meeting/${id}/progress`,
|
||||||
|
{
|
||||||
|
suppressErrorToast: options?.suppressErrorToast,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
|
|
||||||
|
declare module "axios" {
|
||||||
|
interface AxiosRequestConfig {
|
||||||
|
suppressErrorToast?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InternalAxiosRequestConfig {
|
||||||
|
suppressErrorToast?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
baseURL: "/",
|
baseURL: "/",
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
|
|
@ -122,7 +132,9 @@ http.interceptors.response.use(
|
||||||
const body = resp.data;
|
const body = resp.data;
|
||||||
if (body && !isApiSuccessCode(body.code)) {
|
if (body && !isApiSuccessCode(body.code)) {
|
||||||
const errorMsg = body.msg || "请求失败";
|
const errorMsg = body.msg || "请求失败";
|
||||||
|
if (!resp.config?.suppressErrorToast) {
|
||||||
message.error(errorMsg);
|
message.error(errorMsg);
|
||||||
|
}
|
||||||
const err = new Error(errorMsg);
|
const err = new Error(errorMsg);
|
||||||
(err as any).code = body.code;
|
(err as any).code = body.code;
|
||||||
(err as any).msg = body.msg;
|
(err as any).msg = body.msg;
|
||||||
|
|
@ -154,7 +166,9 @@ http.interceptors.response.use(
|
||||||
|
|
||||||
const body = error.response?.data;
|
const body = error.response?.data;
|
||||||
const errorMsg = body?.msg || error.message || "网络异常";
|
const errorMsg = body?.msg || error.message || "网络异常";
|
||||||
|
if (!originalRequest.suppressErrorToast) {
|
||||||
message.error(errorMsg);
|
message.error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
if (body && body.msg) {
|
if (body && body.msg) {
|
||||||
const err = new Error(body.msg);
|
const err = new Error(body.msg);
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,6 @@ const PageContainer: React.FC<PageContainerProps> = ({
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'auto',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
flexDirection: 'column'
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -359,20 +359,31 @@ const MeetingProgressDisplay: React.FC<{
|
||||||
const [progress, setProgress] = useState<MeetingProgress | null>(null);
|
const [progress, setProgress] = useState<MeetingProgress | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let completed = false;
|
||||||
|
|
||||||
const fetchProgress = async () => {
|
const fetchProgress = async () => {
|
||||||
|
if (completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const [progressRes, detailRes] = await Promise.all([
|
const [progressRes, detailRes] = await Promise.all([
|
||||||
getMeetingProgress(meetingId),
|
getMeetingProgress(meetingId, { suppressErrorToast: true }),
|
||||||
getMeetingDetail(meetingId),
|
getMeetingDetail(meetingId, { suppressErrorToast: true }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (detailRes.data?.data) {
|
if (detailRes.data?.data) {
|
||||||
onProgressUpdate?.(detailRes.data.data);
|
onProgressUpdate?.(detailRes.data.data);
|
||||||
|
if (detailRes.data.data.status !== 1 && detailRes.data.data.status !== 2) {
|
||||||
|
completed = true;
|
||||||
|
onComplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressRes.data?.data) {
|
if (progressRes.data?.data) {
|
||||||
setProgress(progressRes.data.data);
|
setProgress(progressRes.data.data);
|
||||||
if (progressRes.data.data.percent === 100) {
|
if (progressRes.data.data.percent === 100 || progressRes.data.data.percent < 0) {
|
||||||
|
completed = true;
|
||||||
onComplete();
|
onComplete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -383,7 +394,10 @@ const MeetingProgressDisplay: React.FC<{
|
||||||
|
|
||||||
fetchProgress();
|
fetchProgress();
|
||||||
const timer = setInterval(fetchProgress, 3000);
|
const timer = setInterval(fetchProgress, 3000);
|
||||||
return () => clearInterval(timer);
|
return () => {
|
||||||
|
completed = true;
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
}, [meetingId, onComplete, onProgressUpdate]);
|
}, [meetingId, onComplete, onProgressUpdate]);
|
||||||
|
|
||||||
const percent = progress?.percent || 0;
|
const percent = progress?.percent || 0;
|
||||||
|
|
@ -902,6 +916,38 @@ const MeetingDetail: React.FC = () => {
|
||||||
|
|
||||||
const canRetrySummary = isOwner && transcripts.length > 0 && meeting?.status !== 1 && meeting?.status !== 2;
|
const canRetrySummary = isOwner && transcripts.length > 0 && meeting?.status !== 1 && meeting?.status !== 2;
|
||||||
const canRetryTranscription = isOwner && meeting?.status === 4 && transcripts.length === 0 && !!meeting?.audioUrl;
|
const canRetryTranscription = isOwner && meeting?.status === 4 && transcripts.length === 0 && !!meeting?.audioUrl;
|
||||||
|
const generationFailureNotice = useMemo(() => {
|
||||||
|
if (!meeting || meeting.status !== 4) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFallbackContent = Boolean(meeting.summaryContent) || meetingChapters.length > 0;
|
||||||
|
if (meeting.latestChapterAttemptStatus === 3) {
|
||||||
|
const detail = meeting.latestChapterAttemptErrorMsg || '章节生成失败';
|
||||||
|
return {
|
||||||
|
key: `chapter-${meeting.latestChapterAttemptTaskId ?? 'latest'}`,
|
||||||
|
title: '本次重新总结失败',
|
||||||
|
description: hasFallbackContent
|
||||||
|
? `章节生成失败,当前展示的是上一次成功的摘要和 AI 目录。失败原因:${detail}`
|
||||||
|
: `章节生成失败,且当前没有可展示的历史摘要或 AI 目录。失败原因:${detail}`,
|
||||||
|
hasFallbackContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meeting.latestSummaryAttemptStatus === 3) {
|
||||||
|
const detail = meeting.latestSummaryAttemptErrorMsg || '总结生成失败';
|
||||||
|
return {
|
||||||
|
key: `summary-${meeting.latestSummaryAttemptTaskId ?? 'latest'}`,
|
||||||
|
title: '本次重新总结失败',
|
||||||
|
description: hasFallbackContent
|
||||||
|
? `总结生成失败,当前展示的是上一次成功的摘要和 AI 目录。失败原因:${detail}`
|
||||||
|
: `总结生成失败,且当前没有可展示的历史摘要或 AI 目录。失败原因:${detail}`,
|
||||||
|
hasFallbackContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [meeting, meetingChapters.length]);
|
||||||
const emptyTranscriptFailureNotice = useMemo(() => {
|
const emptyTranscriptFailureNotice = useMemo(() => {
|
||||||
if (!meeting || meeting.status !== 4 || transcripts.length > 0) {
|
if (!meeting || meeting.status !== 4 || transcripts.length > 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -916,6 +962,24 @@ const MeetingDetail: React.FC = () => {
|
||||||
};
|
};
|
||||||
}, [canRetryTranscription, meeting, transcripts.length]);
|
}, [canRetryTranscription, meeting, transcripts.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!generationFailureNotice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const acknowledgedKey = `meeting-failure-ack:${generationFailureNotice.key}`;
|
||||||
|
if (sessionStorage.getItem(acknowledgedKey) === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Modal.warning({
|
||||||
|
title: generationFailureNotice.title,
|
||||||
|
content: generationFailureNotice.description,
|
||||||
|
okText: '我知道了',
|
||||||
|
onOk: () => {
|
||||||
|
sessionStorage.setItem(acknowledgedKey, '1');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [generationFailureNotice]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!playbackAudioUrl) {
|
if (!playbackAudioUrl) {
|
||||||
setShowFloatingTranscriptPlayer(false);
|
setShowFloatingTranscriptPlayer(false);
|
||||||
|
|
@ -1831,11 +1895,26 @@ const MeetingDetail: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{generationFailureNotice && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
message={generationFailureNotice.title}
|
||||||
|
description={generationFailureNotice.description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{meeting.status === 2 ? (
|
{meeting.status === 2 ? (
|
||||||
<div className="summary-progress-shell">
|
<div className="summary-progress-shell">
|
||||||
<MeetingProgressDisplay
|
<MeetingProgressDisplay
|
||||||
meetingId={meeting.id}
|
meetingId={meeting.id}
|
||||||
onComplete={() => fetchData(meeting.id)}
|
onComplete={() => fetchData(meeting.id)}
|
||||||
|
onProgressUpdate={(updated) => {
|
||||||
|
if (updated.status !== meeting.status) {
|
||||||
|
void fetchData(updated.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2029,6 +2108,15 @@ const MeetingDetail: React.FC = () => {
|
||||||
<div className="transcript-scroll-shell">
|
<div className="transcript-scroll-shell">
|
||||||
{workspaceTab === 'catalog' ? (
|
{workspaceTab === 'catalog' ? (
|
||||||
<div className="catalog-list">
|
<div className="catalog-list">
|
||||||
|
{generationFailureNotice && !generationFailureNotice.hasFallbackContent && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
message="AI 目录生成失败"
|
||||||
|
description={generationFailureNotice.description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{catalogChapterLinks.length ? (
|
{catalogChapterLinks.length ? (
|
||||||
catalogChapterLinks.map((chapter, index) => (
|
catalogChapterLinks.map((chapter, index) => (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -138,10 +138,10 @@ const useMeetingProgress = (meeting: MeetingVO, onComplete?: () => void) => {
|
||||||
if (meeting.status !== 1 && meeting.status !== 2) return;
|
if (meeting.status !== 1 && meeting.status !== 2) return;
|
||||||
const fetchProgress = async () => {
|
const fetchProgress = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getMeetingProgress(meeting.id);
|
const res = await getMeetingProgress(meeting.id, { suppressErrorToast: true });
|
||||||
if (res.data && res.data.data) {
|
if (res.data && res.data.data) {
|
||||||
setProgress(res.data.data);
|
setProgress(res.data.data);
|
||||||
if (res.data.data.percent === 100 && onComplete) {
|
if ((res.data.data.percent === 100 || res.data.data.percent < 0) && onComplete) {
|
||||||
onComplete();
|
onComplete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -604,7 +604,12 @@ const Meetings: React.FC = () => {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: '16px 24px', flex: 1 } }}>
|
<Card
|
||||||
|
className="app-page__content-card"
|
||||||
|
style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
|
||||||
|
styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}
|
||||||
|
>
|
||||||
|
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px 24px' }}>
|
||||||
{displayMode === 'card' ? (
|
{displayMode === 'card' ? (
|
||||||
<Skeleton loading={loading} active paragraph={{ rows: 10 }}>
|
<Skeleton loading={loading} active paragraph={{ rows: 10 }}>
|
||||||
<List
|
<List
|
||||||
|
|
@ -624,6 +629,7 @@ const Meetings: React.FC = () => {
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
onRow={(record) => ({
|
onRow={(record) => ({
|
||||||
onClick: () => handleOpenMeeting(record),
|
onClick: () => handleOpenMeeting(record),
|
||||||
style: { cursor: 'pointer' }
|
style: { cursor: 'pointer' }
|
||||||
|
|
@ -631,8 +637,11 @@ const Meetings: React.FC = () => {
|
||||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
|
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: '0 24px 16px', flexShrink: 0 }}>
|
||||||
<AppPagination current={current} pageSize={size} total={total} onChange={(p, s) => { setCurrent(p); setSize(s); }} />
|
<AppPagination current={current} pageSize={size} total={total} onChange={(p, s) => { setCurrent(p); setSize(s); }} />
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<MeetingCreateDrawer
|
<MeetingCreateDrawer
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue