feat: 添加上游断开时的会议暂停处理逻辑并优化实时转录显示

- 在 `RealtimeAsrSession` 组件中添加 `handleUpstreamPauseError` 方法,处理上游断开时的会议暂停
- 更新 `LocalRealtimeAsrChannel` 类,移除不必要的回调调用
- 优化 `RealtimeMeetingTranscriptCacheServiceImpl` 中的说话人解析逻辑
- 移除 `RealtimeAsrSession` 组件中部分未使用的实时转录显示字段
dev_na
chenhao 2026-06-26 14:22:12 +08:00
parent bf40b13383
commit c0cc4b1c27
3 changed files with 42 additions and 14 deletions

View File

@ -513,7 +513,6 @@ public class LocalRealtimeAsrChannel implements RealtimeAsrChannel {
log.info("上游 ASR websocket 已关闭meetingId={}, sessionId={}, code={}, reason={}", log.info("上游 ASR websocket 已关闭meetingId={}, sessionId={}, code={}, reason={}",
context.getMeetingId(), currentConnectionId(context), statusCode, reason); context.getMeetingId(), currentConnectionId(context), statusCode, reason);
context.getChannelState().remove(STATE_UPSTREAM_SOCKET); context.getChannelState().remove(STATE_UPSTREAM_SOCKET);
context.getCallback().removeMeetingSession(context.getMeetingId());
context.getCallback().sendFrontendError(context.getMeetingId(), context.getCallback().sendFrontendError(context.getMeetingId(),
"REALTIME_UPSTREAM_CLOSED", "REALTIME_UPSTREAM_CLOSED",
reason == null || reason.isBlank() ? "上游 ASR WebSocket 已断开" : "上游 ASR WebSocket 已断开: " + reason); reason == null || reason.isBlank() ? "上游 ASR WebSocket 已断开" : "上游 ASR WebSocket 已断开: " + reason);
@ -533,6 +532,7 @@ public class LocalRealtimeAsrChannel implements RealtimeAsrChannel {
? "上游 ASR WebSocket 连接异常" ? "上游 ASR WebSocket 连接异常"
: "上游 ASR WebSocket 连接异常: " + error.getMessage()); : "上游 ASR WebSocket 连接异常: " + error.getMessage());
context.getCallback().closeFrontend(context.getMeetingId(), CloseStatus.SERVER_ERROR); context.getCallback().closeFrontend(context.getMeetingId(), CloseStatus.SERVER_ERROR);
context.getCallback().removeMeetingSession(context.getMeetingId());
} }
} }
} }

View File

@ -206,7 +206,7 @@ public class RealtimeMeetingTranscriptCacheServiceImpl implements RealtimeMeetin
return item.getSpeakerName().trim(); return item.getSpeakerName().trim();
} }
String speakerId = resolveSpeakerId(item); String speakerId = resolveSpeakerId(item);
return speakerId == null || speakerId.isBlank() ? null : speakerId; return speakerId == null || speakerId.isBlank() ? null : "未知说话人" + speakerId;
} }
private String resolveSpeakerId(JsonNode sentence) { private String resolveSpeakerId(JsonNode sentence) {

View File

@ -442,6 +442,30 @@ export function RealtimeAsrSession() {
message.error(errorMessage); message.error(errorMessage);
}; };
const handleUpstreamPauseError = async (errorMessage: string) => {
if (recording && startedAtRef.current) {
elapsedOffsetRef.current += Math.floor((Date.now() - startedAtRef.current) / 1000);
}
setConnecting(false);
setRecording(false);
setStatusText("上游识别已断开,正在暂停会议...");
sessionStartedRef.current = false;
wsRef.current?.close();
wsRef.current = null;
await shutdownAudioPipeline();
startedAtRef.current = null;
try {
const pauseRes = await pauseRealtimeMeeting(meetingId);
setSessionStatus(pauseRes.data.data);
setElapsedSeconds(elapsedOffsetRef.current);
setStatusText("已暂停,可继续会议");
message.error(errorMessage);
} catch {
setStatusText("识别已断开");
message.error(errorMessage);
}
};
const startAudioPipeline = async () => { const startAudioPipeline = async () => {
if (!window.isSecureContext || !navigator.mediaDevices?.getUserMedia) { if (!window.isSecureContext || !navigator.mediaDevices?.getUserMedia) {
throw new Error("当前浏览器环境不支持麦克风访问。请使用 localhost 或 HTTPS 域名访问系统。"); throw new Error("当前浏览器环境不支持麦克风访问。请使用 localhost 或 HTTPS 域名访问系统。");
@ -585,7 +609,11 @@ export function RealtimeAsrSession() {
if ((payload.code || payload.type === "error") && payload.message) { if ((payload.code || payload.type === "error") && payload.message) {
setStatusText(payload.message); setStatusText(payload.message);
void handleFatalRealtimeError(payload.message); if (payload.code === "REALTIME_UPSTREAM_CLOSED" || payload.code === "REALTIME_UPSTREAM_ERROR") {
void handleUpstreamPauseError(payload.message);
} else {
void handleFatalRealtimeError(payload.message);
}
return; return;
} }
@ -804,17 +832,17 @@ export function RealtimeAsrSession() {
<Text type="secondary" style={{ fontSize: 13 }}> <Text type="secondary" style={{ fontSize: 13 }}>
<strong style={{ color: "#334155" }}>{totalTranscriptChars}</strong> <strong style={{ color: "#334155" }}>{totalTranscriptChars}</strong>
</Text> </Text>
<Text type="secondary" style={{ fontSize: 13 }}> {/*<Text type="secondary" style={{ fontSize: 13 }}>*/}
<strong style={{ color: "#334155" }}>{sessionDraft.asrModelName}</strong> {/* 模型 <strong style={{ color: "#334155" }}>{sessionDraft.asrModelName}</strong>*/}
</Text> {/*</Text>*/}
<Text type="secondary" style={{ fontSize: 13 }}> {/*<Text type="secondary" style={{ fontSize: 13 }}>*/}
<strong style={{ color: "#334155" }}>{sessionDraft.mode}</strong> {/* 模式 <strong style={{ color: "#334155" }}>{sessionDraft.mode}</strong>*/}
</Text> {/*</Text>*/}
{sessionDraft.hotwords && sessionDraft.hotwords.length > 0 && ( {/*{sessionDraft.hotwords && sessionDraft.hotwords.length > 0 && (*/}
<Text type="secondary" style={{ fontSize: 13 }}> {/* <Text type="secondary" style={{ fontSize: 13 }}>*/}
<strong style={{ color: "#334155" }}>{sessionDraft.hotwords.length}</strong> {/* 热词 <strong style={{ color: "#334155" }}>{sessionDraft.hotwords.length}</strong>*/}
</Text> {/* </Text>*/}
)} {/*)}*/}
</Space> </Space>
)} )}
</div> </div>