




















@@ -16,6 +16,7 @@ const OPENAI_REALTIME_MODEL =
1616process.env.OPENCLAW_REALTIME_OPENAI_MODEL?.trim() || "gpt-realtime-2";
1717const OPENAI_REALTIME_VOICE = process.env.OPENCLAW_REALTIME_OPENAI_VOICE?.trim() || "alloy";
1818const DEFAULT_OPENAI_HTTP_TIMEOUT_MS = 30_000;
19+const OPENAI_HTTP_RESPONSE_MAX_BYTES = 256 * 1024;
1920const GOOGLE_REALTIME_MODEL =
2021process.env.OPENCLAW_REALTIME_GOOGLE_MODEL?.trim() ||
2122"gemini-2.5-flash-native-audio-preview-12-2025";
@@ -49,9 +50,104 @@ function shortError(error: unknown): string {
4950return previewForDevToolLog(error instanceof Error ? error.message : String(error), 800);
5051}
515252-async function readBoundedText(response: Response): Promise<string> {
53-const text = await response.text();
54-return previewForDevToolLog(text, 600);
53+function responseBodyTooLargeError(label: string, maxBytes: number): Error {
54+return new Error(`${label} response body exceeded ${maxBytes} bytes`);
55+}
56+57+async function readBoundedText(
58+response: Response,
59+label: string,
60+maxBytes = OPENAI_HTTP_RESPONSE_MAX_BYTES,
61+signal?: AbortSignal,
62+): Promise<string> {
63+const contentLength = Number(response.headers.get("content-length") ?? "");
64+if (Number.isSafeInteger(contentLength) && contentLength > maxBytes) {
65+await response.body?.cancel().catch(() => undefined);
66+throw responseBodyTooLargeError(label, maxBytes);
67+}
68+69+if (!response.body) {
70+return "";
71+}
72+73+const reader = response.body.getReader();
74+const decoder = new TextDecoder();
75+const chunks: string[] = [];
76+let totalBytes = 0;
77+let canceled = false;
78+79+try {
80+for (;;) {
81+const { done, value } = await readResponseChunk(reader, label, signal, () => {
82+canceled = true;
83+});
84+if (done) {
85+const tail = decoder.decode();
86+if (tail) {
87+chunks.push(tail);
88+}
89+break;
90+}
91+92+totalBytes += value.byteLength;
93+if (totalBytes > maxBytes) {
94+canceled = true;
95+await reader.cancel().catch(() => undefined);
96+throw responseBodyTooLargeError(label, maxBytes);
97+}
98+chunks.push(decoder.decode(value, { stream: true }));
99+}
100+} finally {
101+if (!canceled) {
102+reader.releaseLock();
103+}
104+}
105+106+return chunks.join("");
107+}
108+109+async function readResponseChunk(
110+reader: ReadableStreamDefaultReader<Uint8Array>,
111+label: string,
112+signal: AbortSignal | undefined,
113+markCanceled: () => void,
114+): Promise<ReadableStreamReadResult<Uint8Array>> {
115+if (!signal) {
116+return await reader.read();
117+}
118+if (signal.aborted) {
119+markCanceled();
120+await reader.cancel().catch(() => undefined);
121+throw signal.reason instanceof Error ? signal.reason : new Error(`${label} request aborted`);
122+}
123+124+let removeAbortListener: (() => void) | undefined;
125+const abortPromise = new Promise<ReadableStreamReadResult<Uint8Array>>((_resolve, reject) => {
126+const onAbort = () => {
127+markCanceled();
128+void reader.cancel().catch(() => undefined);
129+reject(
130+signal.reason instanceof Error ? signal.reason : new Error(`${label} request aborted`),
131+);
132+};
133+signal.addEventListener("abort", onAbort, { once: true });
134+removeAbortListener = () => signal.removeEventListener("abort", onAbort);
135+});
136+137+try {
138+return await Promise.race([reader.read(), abortPromise]);
139+} finally {
140+removeAbortListener?.();
141+}
142+}
143+144+async function readBoundedJsonResponse(
145+response: Response,
146+label: string,
147+signal?: AbortSignal,
148+): Promise<Record<string, unknown>> {
149+const text = await readBoundedText(response, label, OPENAI_HTTP_RESPONSE_MAX_BYTES, signal);
150+return JSON.parse(text) as Record<string, unknown>;
55151}
5615257153function resolveOpenAIHttpTimeoutMs(
@@ -124,12 +220,18 @@ async function createOpenAIClientSecret(
124220});
125221if (!response.ok) {
126222throw new Error(
127-`OpenAI Realtime client secret failed (${response.status}): ${await readBoundedText(
128- response,
223+`OpenAI Realtime client secret failed (${response.status}): ${previewForDevToolLog(
224+ await readBoundedText(
225+ response,
226+ "OpenAI Realtime client secret error",
227+ OPENAI_HTTP_RESPONSE_MAX_BYTES,
228+ signal,
229+ ),
230+ 600,
129231 )}`,
130232);
131233}
132-return (await response.json()) as Record<string, unknown>;
234+return await readBoundedJsonResponse(response, "OpenAI Realtime client secret", signal);
133235},
134236});
135237const nested =
@@ -195,7 +297,56 @@ async function smokeOpenAIWebRtc(browser: Browser, apiKey: string): Promise<Smok
195297const page = await context.newPage();
196298await page.evaluate("globalThis.__name = (fn) => fn");
197299const result = await page.evaluate(
198-async ({ clientSecret: secret, timeoutMs }) => {
300+async ({ clientSecret: secret, sdpAnswerMaxBytes, timeoutMs }) => {
301+const responseBodyTooLargeError = (label: string, maxBytes: number): Error =>
302+new Error(`${label} response body exceeded ${maxBytes} bytes`);
303+const readBoundedText = async (
304+response: Response,
305+label: string,
306+maxBytes: number,
307+): Promise<string> => {
308+const contentLength = Number(response.headers.get("content-length") ?? "");
309+if (Number.isSafeInteger(contentLength) && contentLength > maxBytes) {
310+await response.body?.cancel().catch(() => undefined);
311+throw responseBodyTooLargeError(label, maxBytes);
312+}
313+if (!response.body) {
314+return "";
315+}
316+317+const reader = response.body.getReader();
318+const decoder = new TextDecoder();
319+const chunks: string[] = [];
320+let totalBytes = 0;
321+let canceled = false;
322+323+try {
324+for (;;) {
325+const { done, value } = await reader.read();
326+if (done) {
327+const tail = decoder.decode();
328+if (tail) {
329+chunks.push(tail);
330+}
331+break;
332+}
333+334+totalBytes += value.byteLength;
335+if (totalBytes > maxBytes) {
336+canceled = true;
337+await reader.cancel().catch(() => undefined);
338+throw responseBodyTooLargeError(label, maxBytes);
339+}
340+chunks.push(decoder.decode(value, { stream: true }));
341+}
342+} finally {
343+if (!canceled) {
344+reader.releaseLock();
345+}
346+}
347+348+return chunks.join("");
349+};
199350const withBrowserTimeout = async <T>(
200351label: string,
201352run: (signal: AbortSignal) => Promise<T>,
@@ -268,7 +419,11 @@ async function smokeOpenAIWebRtc(browser: Browser, apiKey: string): Promise<Smok
268419if (!response.ok) {
269420throw new Error(`OpenAI Realtime SDP offer failed (${response.status})`);
270421}
271-return await response.text();
422+return await readBoundedText(
423+response,
424+"OpenAI Realtime SDP answer",
425+sdpAnswerMaxBytes,
426+);
272427},
273428);
274429await peer.setRemoteDescription({ type: "answer", sdp: answer });
@@ -283,7 +438,11 @@ async function smokeOpenAIWebRtc(browser: Browser, apiKey: string): Promise<Smok
283438media?.getTracks().forEach((track) => track.stop());
284439}
285440},
286-{ clientSecret, timeoutMs: openAIHttpTimeoutMs },
441+{
442+ clientSecret,
443+sdpAnswerMaxBytes: OPENAI_HTTP_RESPONSE_MAX_BYTES,
444+timeoutMs: openAIHttpTimeoutMs,
445+},
287446);
288447return {
289448name: "openai-webrtc-browser",
@@ -677,6 +836,8 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
677836}
678837679838export const testing = {
839+OPENAI_HTTP_RESPONSE_MAX_BYTES,
680840 createOpenAIClientSecret,
841+ readBoundedText,
681842 resolveOpenAIHttpTimeoutMs,
682843};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。