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:
Xin Wang
2026-07-10 16:25:06 +08:00
parent 5705726cfb
commit 059ad8162e
13 changed files with 1096 additions and 43 deletions

View File

@@ -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 {