
























@@ -124,6 +124,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
124124125125private ws: WebSocket | null = null;
126126private connected = false;
127+private sessionConfigured = false;
127128private intentionallyClosed = false;
128129private reconnectAttempts = 0;
129130private pendingAudio: Buffer[] = [];
@@ -133,6 +134,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
133134private lastAssistantItemId: string | null = null;
134135private toolCallBuffers = new Map<string, { name: string; callId: string; args: string }>();
135136private readonly flowId = randomUUID();
137+private sessionReadyFired = false;
136138137139constructor(private readonly config: OpenAIRealtimeVoiceBridgeConfig) {}
138140@@ -143,7 +145,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
143145}
144146145147sendAudio(audio: Buffer): void {
146-if (!this.connected || this.ws?.readyState !== WebSocket.OPEN) {
148+if (!this.connected || !this.sessionConfigured || this.ws?.readyState !== WebSocket.OPEN) {
147149if (this.pendingAudio.length < 320) {
148150this.pendingAudio.push(audio);
149151}
@@ -172,7 +174,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
172174}
173175174176triggerGreeting(instructions?: string): void {
175-if (!this.connected || !this.ws) {
177+if (!this.isConnected() || !this.ws) {
176178return;
177179}
178180this.sendEvent({
@@ -209,18 +211,37 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
209211close(): void {
210212this.intentionallyClosed = true;
211213this.connected = false;
214+this.sessionConfigured = false;
212215if (this.ws) {
213216this.ws.close(1000, "Bridge closed");
214217this.ws = null;
215218}
216219}
217220218221isConnected(): boolean {
219-return this.connected;
222+return this.connected && this.sessionConfigured;
220223}
221224222225private async doConnect(): Promise<void> {
223226await new Promise<void>((resolve, reject) => {
227+let connectTimeout: ReturnType<typeof setTimeout>;
228+let settled = false;
229+const settleResolve = () => {
230+if (settled) {
231+return;
232+}
233+settled = true;
234+clearTimeout(connectTimeout);
235+resolve();
236+};
237+const settleReject = (error: Error) => {
238+if (settled) {
239+return;
240+}
241+settled = true;
242+clearTimeout(connectTimeout);
243+reject(error);
244+};
224245const { url, headers } = this.resolveConnectionParams();
225246const debugProxy = resolveDebugProxySettings();
226247const proxyAgent = createDebugProxyWebSocketAgent(debugProxy);
@@ -229,13 +250,16 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
229250 ...(proxyAgent ? { agent: proxyAgent } : {}),
230251});
231252232-const connectTimeout = setTimeout(() => {
233-reject(new Error("OpenAI realtime connection timeout"));
253+connectTimeout = setTimeout(() => {
254+if (!this.connected && !this.intentionallyClosed) {
255+this.ws?.terminate();
256+settleReject(new Error("OpenAI realtime connection timeout"));
257+}
234258}, OpenAIRealtimeVoiceBridge.CONNECT_TIMEOUT_MS);
235259236260this.ws.on("open", () => {
237-clearTimeout(connectTimeout);
238261this.connected = true;
262+this.sessionConfigured = false;
239263this.reconnectAttempts = 0;
240264captureWsEvent({
241265 url,
@@ -248,11 +272,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
248272},
249273});
250274this.sendSessionUpdate();
251-for (const chunk of this.pendingAudio.splice(0)) {
252-this.sendAudio(chunk);
253-}
254-this.config.onReady?.();
255-resolve();
275+settleResolve();
256276});
257277258278this.ws.on("message", (data: Buffer) => {
@@ -287,8 +307,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
287307},
288308});
289309if (!this.connected) {
290-clearTimeout(connectTimeout);
291-reject(error);
310+settleReject(error instanceof Error ? error : new Error(String(error)));
292311}
293312this.config.onError?.(error instanceof Error ? error : new Error(String(error)));
294313});
@@ -302,7 +321,9 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
302321 reasonBuffer,
303322});
304323this.connected = false;
324+this.sessionConfigured = false;
305325if (this.intentionallyClosed) {
326+settleResolve();
306327this.config.onClose?.("completed");
307328return;
308329}
@@ -406,6 +427,20 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
406427407428private handleEvent(event: RealtimeEvent): void {
408429switch (event.type) {
430+case "session.created":
431+return;
432+433+case "session.updated":
434+this.sessionConfigured = true;
435+for (const chunk of this.pendingAudio.splice(0)) {
436+this.sendAudio(chunk);
437+}
438+if (!this.sessionReadyFired) {
439+this.sessionReadyFired = true;
440+this.config.onReady?.();
441+}
442+return;
443+409444case "response.audio.delta": {
410445if (!event.delta) {
411446return;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。