






















@@ -266,19 +266,19 @@ function resolveOpenAITimeoutMs(raw = process.env.OPENCLAW_LABEL_OPEN_ISSUES_OPE
266266async function withOpenAITimeout<T>(
267267label: string,
268268timeoutMs: number,
269-run: (signal: AbortSignal) => Promise<T>,
269+run: (signal: AbortSignal, timeoutPromise: Promise<never>) => Promise<T>,
270270): Promise<T> {
271271const controller = new AbortController();
272272let timeout: ReturnType<typeof setTimeout> | undefined;
273-const timeoutPromise = new Promise<T>((_resolve, reject) => {
273+const timeoutPromise = new Promise<never>((_resolve, reject) => {
274274timeout = setTimeout(() => {
275275const error = new Error(`${label} exceeded timeout of ${timeoutMs}ms`);
276276reject(error);
277277controller.abort(error);
278278}, timeoutMs);
279279});
280280try {
281-return await Promise.race([run(controller.signal), timeoutPromise]);
281+return await Promise.race([run(controller.signal, timeoutPromise), timeoutPromise]);
282282} finally {
283283if (timeout) {
284284clearTimeout(timeout);
@@ -289,6 +289,7 @@ async function withOpenAITimeout<T>(
289289async function readBoundedResponseText(
290290response: Response,
291291maxChars = OPENAI_ERROR_BODY_MAX_CHARS,
292+timeoutPromise?: Promise<never>,
292293): Promise<string> {
293294if (!response.body) {
294295return "";
@@ -298,10 +299,13 @@ async function readBoundedResponseText(
298299const decoder = new TextDecoder();
299300let text = "";
300301let truncated = false;
302+let canceled = false;
301303302304try {
303305while (text.length <= maxChars) {
304-const { done, value } = await reader.read();
306+const { done, value } = await readOpenAIErrorChunk(reader, timeoutPromise, () => {
307+canceled = true;
308+});
305309if (done) {
306310text += decoder.decode();
307311break;
@@ -317,23 +321,63 @@ async function readBoundedResponseText(
317321} finally {
318322if (truncated) {
319323await reader.cancel().catch(() => undefined);
320-} else {
324+} else if (!canceled) {
321325reader.releaseLock();
322326}
323327}
324328325329return truncated ? `${text}\n[truncated]` : text;
326330}
327331332+function cancelOpenAIErrorReaderSoon(reader: ReadableStreamDefaultReader<Uint8Array>): void {
333+void Promise.resolve()
334+.then(() => reader.cancel())
335+.catch(() => undefined);
336+}
337+338+async function readOpenAIErrorChunk(
339+reader: ReadableStreamDefaultReader<Uint8Array>,
340+timeoutPromise: Promise<never> | undefined,
341+markCanceled: () => void,
342+): Promise<ReadableStreamReadResult<Uint8Array>> {
343+const readPromise = reader.read();
344+if (!timeoutPromise) {
345+return await readPromise;
346+}
347+348+let waitingForRead = true;
349+const timeoutReadPromise = timeoutPromise.catch((error: unknown) => {
350+if (waitingForRead) {
351+markCanceled();
352+cancelOpenAIErrorReaderSoon(reader);
353+}
354+throw error instanceof Error ? error : new Error("OpenAI error response body read timed out");
355+});
356+357+try {
358+return await Promise.race([readPromise, timeoutReadPromise]);
359+} finally {
360+waitingForRead = false;
361+}
362+}
363+364+type OpenAIJsonReadOptions = {
365+signal?: AbortSignal;
366+timeoutPromise?: Promise<never>;
367+};
368+328369async function readBoundedOpenAIJson(
329370response: Response,
330371maxBytes = OPENAI_RESPONSE_BODY_MAX_BYTES,
372+options: OpenAIJsonReadOptions = {},
331373): Promise<OpenAIResponse> {
332374const text = await readBoundedBodyText(response, "OpenAI classification", maxBytes, {
333375createTooLargeError: (message) =>
334376Object.assign(new Error(message), {
335377code: "ETOOBIG",
336378}),
379+signal: options.signal,
380+timeoutPromise: options.timeoutPromise,
337381});
338382return JSON.parse(text) as OpenAIResponse;
339383}
@@ -692,7 +736,7 @@ async function classifyItem(
692736const payload = await withOpenAITimeout(
693737"OpenAI issue label classification request",
694738timeoutMs,
695-async (signal) => {
739+async (signal, timeoutPromise) => {
696740const response = await fetchImpl("https://api.openai.com/v1/responses", {
697741method: "POST",
698742headers: {
@@ -741,11 +785,11 @@ async function classifyItem(
741785});
742786743787if (!response.ok) {
744-const text = await readBoundedResponseText(response);
788+const text = await readBoundedResponseText(response, undefined, timeoutPromise);
745789throw new Error(`OpenAI request failed (${response.status}): ${text}`);
746790}
747791748-return await readBoundedOpenAIJson(response);
792+return await readBoundedOpenAIJson(response, undefined, { signal, timeoutPromise });
749793},
750794);
751795const rawText = extractResponseText(payload);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。