Add conversation history management and API endpoints
- Introduce new database models for conversation sessions, messages, and artifacts to support conversation history tracking. - Implement API routes for listing conversations and retrieving detailed conversation data, enhancing user interaction with historical records. - Add a conversation recorder service to persist conversation messages in real-time without disrupting ongoing calls. - Update the frontend to display conversation history, including filtering and sorting options, improving user experience. - Enhance the pipeline to integrate conversation history recording seamlessly during interactions.
This commit is contained in:
@@ -230,6 +230,7 @@ export function useVoicePreview(
|
||||
const dataChannelRef = useRef<RTCDataChannel | null>(null);
|
||||
const localStreamRef = useRef<MediaStream | null>(null);
|
||||
const startingRef = useRef(false);
|
||||
const negotiationInProgressRef = useRef(false);
|
||||
const messageSeqRef = useRef(0);
|
||||
const networkStatsRef = useRef<NetworkStatsSample | null>(null);
|
||||
const videoBitrateRef = useRef(INITIAL_VIDEO_BITRATE);
|
||||
@@ -341,6 +342,7 @@ export function useVoicePreview(
|
||||
localStreamRef.current = null;
|
||||
if (audioRef.current) audioRef.current.srcObject = null;
|
||||
startingRef.current = false;
|
||||
negotiationInProgressRef.current = false;
|
||||
onNodeActiveRef.current?.(null);
|
||||
}, []);
|
||||
|
||||
@@ -436,10 +438,18 @@ export function useVoicePreview(
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === "answer") {
|
||||
await pcRef.current?.setRemoteDescription({
|
||||
type: "answer",
|
||||
sdp: msg.payload.sdp,
|
||||
});
|
||||
const currentPc = pcRef.current;
|
||||
if (!currentPc) return;
|
||||
try {
|
||||
await currentPc.setRemoteDescription({
|
||||
type: "answer",
|
||||
sdp: msg.payload.sdp,
|
||||
});
|
||||
} finally {
|
||||
if (pcRef.current === currentPc) {
|
||||
negotiationInProgressRef.current = false;
|
||||
}
|
||||
}
|
||||
} else if (msg.type === "ice-candidate" && msg.payload?.candidate) {
|
||||
// 后端当前不主动 trickle,留兼容
|
||||
try {
|
||||
@@ -478,6 +488,42 @@ export function useVoicePreview(
|
||||
const pc = new RTCPeerConnection({ iceServers });
|
||||
pcRef.current = pc;
|
||||
|
||||
const sendOffer = async () => {
|
||||
if (
|
||||
pcRef.current !== pc ||
|
||||
ws.readyState !== WebSocket.OPEN ||
|
||||
pc.signalingState !== "stable" ||
|
||||
negotiationInProgressRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
negotiationInProgressRef.current = true;
|
||||
try {
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
const localDescription = pc.localDescription;
|
||||
if (!localDescription?.sdp) {
|
||||
throw new Error("浏览器无法创建 WebRTC offer。");
|
||||
}
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "offer",
|
||||
payload: {
|
||||
pc_id: pcId,
|
||||
sdp: localDescription.sdp,
|
||||
type: localDescription.type,
|
||||
assistant_id: assistantId,
|
||||
vision_enabled: Boolean(options.visionEnabled),
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (offerError) {
|
||||
negotiationInProgressRef.current = false;
|
||||
throw offerError;
|
||||
}
|
||||
};
|
||||
|
||||
pc.onicecandidate = (e) => {
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send(
|
||||
@@ -503,10 +549,15 @@ export function useVoicePreview(
|
||||
channel.onopen = () => {
|
||||
channel.send(JSON.stringify({ type: "client-ready" }));
|
||||
};
|
||||
channel.onmessage = (event) => {
|
||||
channel.onmessage = async (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (
|
||||
msg?.type === "signalling" &&
|
||||
msg?.message?.type === "renegotiate"
|
||||
) {
|
||||
await sendOffer();
|
||||
} else if (
|
||||
msg?.type === "assistant-text-start" &&
|
||||
typeof msg.turn_id === "string"
|
||||
) {
|
||||
@@ -631,25 +682,8 @@ export function useVoicePreview(
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 生成 offer 并发给后端(assistant_id 在 payload 顶层)
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
const localDescription = pc.localDescription;
|
||||
if (!localDescription?.sdp) {
|
||||
throw new Error("浏览器无法创建 WebRTC offer。");
|
||||
}
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "offer",
|
||||
payload: {
|
||||
pc_id: pcId,
|
||||
sdp: localDescription.sdp,
|
||||
type: localDescription.type,
|
||||
assistant_id: assistantId,
|
||||
vision_enabled: Boolean(options.visionEnabled),
|
||||
},
|
||||
}),
|
||||
);
|
||||
// 4) 生成 offer 并发给后端;同一方法也响应 SmallWebRTC 的重新协商请求。
|
||||
await sendOffer();
|
||||
} catch (connectionError) {
|
||||
fail(errorMessage(connectionError, "无法连接语音服务。"));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user