





















@@ -43,6 +43,16 @@ type OpenAIHttpOptions = {
4343timeoutMs?: number;
4444};
454546+type OpenAIRealtimeBrowserResponseReader = (
47+response: Response,
48+label: string,
49+maxBytes: number,
50+) => Promise<string>;
51+52+type OpenAIWebRtcSmokeGlobal = typeof globalThis & {
53+openclawReadBoundedRealtimeResponseText?: OpenAIRealtimeBrowserResponseReader;
54+};
55+4656function getEnv(name: string): string | undefined {
4757const value = process.env[name]?.trim();
4858return value ? value : undefined;
@@ -114,6 +124,63 @@ function compareStrings(left: string | undefined, right: string | undefined): nu
114124return (left ?? "").localeCompare(right ?? "");
115125}
116126127+async function readOpenAIRealtimeBrowserResponseText(
128+response: Response,
129+label: string,
130+maxBytes: number,
131+): Promise<string> {
132+const responseBodyTooLargeError = (errorLabel: string, errorMaxBytes: number): Error =>
133+new Error(`${errorLabel} response body exceeded ${errorMaxBytes} bytes`);
134+const rawContentLength = response.headers.get("content-length");
135+if (rawContentLength && /^\d+$/u.test(rawContentLength)) {
136+const contentLength = Number(rawContentLength);
137+if (!Number.isSafeInteger(contentLength) || contentLength > maxBytes) {
138+await response.body?.cancel().catch(() => undefined);
139+throw responseBodyTooLargeError(label, maxBytes);
140+}
141+}
142+if (!response.body) {
143+return "";
144+}
145+146+const reader = response.body.getReader();
147+const decoder = new TextDecoder();
148+const chunks: string[] = [];
149+let totalBytes = 0;
150+let canceled = false;
151+152+try {
153+for (;;) {
154+const { done, value } = await reader.read();
155+if (done) {
156+const tail = decoder.decode();
157+if (tail) {
158+chunks.push(tail);
159+}
160+break;
161+}
162+163+totalBytes += value.byteLength;
164+if (totalBytes > maxBytes) {
165+canceled = true;
166+await reader.cancel().catch(() => undefined);
167+throw responseBodyTooLargeError(label, maxBytes);
168+}
169+chunks.push(decoder.decode(value, { stream: true }));
170+}
171+} finally {
172+if (!canceled) {
173+reader.releaseLock();
174+}
175+}
176+177+return chunks.join("");
178+}
179+180+function openAIRealtimeBrowserResponseReaderInitScript(): string {
181+return `globalThis.openclawReadBoundedRealtimeResponseText = ${readOpenAIRealtimeBrowserResponseText.toString()};`;
182+}
183+117184async function createOpenAIClientSecret(
118185apiKey: string,
119186options: OpenAIHttpOptions = {},
@@ -219,57 +286,14 @@ async function smokeOpenAIWebRtc(browser: Browser, apiKey: string): Promise<Smok
219286try {
220287const page = await context.newPage();
221288await page.evaluate("globalThis.__name = (fn) => fn");
289+await page.evaluate(openAIRealtimeBrowserResponseReaderInitScript());
222290const result = await page.evaluate(
223291async ({ clientSecret: secret, sdpAnswerMaxBytes, timeoutMs }) => {
224-const responseBodyTooLargeError = (label: string, maxBytes: number): Error =>
225-new Error(`${label} response body exceeded ${maxBytes} bytes`);
226-const readBoundedTextLocal = async (
227-response: Response,
228-label: string,
229-maxBytes: number,
230-): Promise<string> => {
231-const contentLength = Number(response.headers.get("content-length") ?? "");
232-if (Number.isSafeInteger(contentLength) && contentLength > maxBytes) {
233-await response.body?.cancel().catch(() => undefined);
234-throw responseBodyTooLargeError(label, maxBytes);
235-}
236-if (!response.body) {
237-return "";
238-}
239-240-const reader = response.body.getReader();
241-const decoder = new TextDecoder();
242-const chunks: string[] = [];
243-let totalBytes = 0;
244-let canceled = false;
245-246-try {
247-for (;;) {
248-const { done, value } = await reader.read();
249-if (done) {
250-const tail = decoder.decode();
251-if (tail) {
252-chunks.push(tail);
253-}
254-break;
255-}
256-257-totalBytes += value.byteLength;
258-if (totalBytes > maxBytes) {
259-canceled = true;
260-await reader.cancel().catch(() => undefined);
261-throw responseBodyTooLargeError(label, maxBytes);
262-}
263-chunks.push(decoder.decode(value, { stream: true }));
264-}
265-} finally {
266-if (!canceled) {
267-reader.releaseLock();
268-}
269-}
270-271-return chunks.join("");
272-};
292+const readBoundedTextLocal = (globalThis as OpenAIWebRtcSmokeGlobal)
293+.openclawReadBoundedRealtimeResponseText;
294+if (!readBoundedTextLocal) {
295+throw new Error("OpenAI Realtime bounded response reader was not installed");
296+}
273297const withBrowserTimeout = async <T>(
274298label: string,
275299run: (signal: AbortSignal) => Promise<T>,
@@ -327,12 +351,16 @@ async function smokeOpenAIWebRtc(browser: Browser, apiKey: string): Promise<Smok
327351});
328352const offer = await peer.createOffer();
329353await peer.setLocalDescription(offer);
354+const offerSdp = offer.sdp;
355+if (!offerSdp) {
356+throw new Error("OpenAI Realtime SDP offer did not include SDP");
357+}
330358const answer = await withBrowserTimeout(
331359"OpenAI Realtime SDP offer request",
332360async (signal) => {
333361const response = await fetch("https://api.openai.com/v1/realtime/calls", {
334362method: "POST",
335-body: offer.sdp,
363+body: offerSdp,
336364headers: {
337365Authorization: `Bearer ${secret}`,
338366"Content-Type": "application/sdp",
@@ -761,6 +789,7 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
761789export const testing = {
762790OPENAI_HTTP_RESPONSE_MAX_BYTES,
763791 createOpenAIClientSecret,
792+ readOpenAIRealtimeBrowserResponseText,
764793 readBoundedText,
765794 resolveOpenAIHttpTimeoutMs,
766795};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。