From 657891606193f7fd8ab6f7327d594071703f4257 Mon Sep 17 00:00:00 2001 From: lvjunjie-byte Date: Sun, 26 Apr 2026 15:10:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=BA=E9=A3=9E=E4=B9=A6=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=99=A8=E7=9A=84=20HTTP=20=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B6=85=E6=97=B6=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:AdapterHttpClient 中的 HTTP 请求没有设置超时时间, 当服务器响应缓慢时,请求会无限期挂起,阻塞聊天队列, 导致飞书机器人看起来卡住了(电脑在运行,但飞书不回复)。 修复: - 为所有 HTTP 方法添加 30 秒超时(AbortController) - createSession、listRecentProjects、getGitInfo、getTasksForSession - 为图片下载的 fetch 调用添加超时 Co-Authored-By: Claude Opus 4.7 --- adapters/common/http-client.ts | 93 ++++++++++++++++++++++++---------- adapters/feishu/index.ts | 14 +++-- 2 files changed, 76 insertions(+), 31 deletions(-) diff --git a/adapters/common/http-client.ts b/adapters/common/http-client.ts index 10446003..f10606f8 100644 --- a/adapters/common/http-client.ts +++ b/adapters/common/http-client.ts @@ -24,6 +24,8 @@ export type SessionTask = { export class AdapterHttpClient { readonly httpBaseUrl: string + /** Default timeout for HTTP requests (30 seconds) */ + private static readonly DEFAULT_TIMEOUT_MS = 30_000 constructor(wsUrl: string) { this.httpBaseUrl = wsUrl @@ -32,27 +34,50 @@ export class AdapterHttpClient { .replace(/\/$/, '') } + /** Create an AbortController with timeout */ + private createTimeoutController(timeoutMs = AdapterHttpClient.DEFAULT_TIMEOUT_MS): { + controller: AbortController + timer: ReturnType + } { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), timeoutMs) + return { controller, timer } + } + async createSession(workDir: string): Promise { - const res = await fetch(`${this.httpBaseUrl}/api/sessions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workDir }), - }) - if (!res.ok) { - const err = await res.json().catch(() => ({ message: res.statusText })) - throw new Error(`Failed to create session: ${(err as any).message}`) + const { controller, timer } = this.createTimeoutController() + try { + const res = await fetch(`${this.httpBaseUrl}/api/sessions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workDir }), + signal: controller.signal, + }) + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })) + throw new Error(`Failed to create session: ${(err as any).message}`) + } + const data = (await res.json()) as { sessionId: string } + return data.sessionId + } finally { + clearTimeout(timer) } - const data = (await res.json()) as { sessionId: string } - return data.sessionId } async listRecentProjects(): Promise { - const res = await fetch(`${this.httpBaseUrl}/api/sessions/recent-projects`) - if (!res.ok) { - throw new Error(`Failed to list projects: ${res.statusText}`) + const { controller, timer } = this.createTimeoutController() + try { + const res = await fetch(`${this.httpBaseUrl}/api/sessions/recent-projects`, { + signal: controller.signal, + }) + if (!res.ok) { + throw new Error(`Failed to list projects: ${res.statusText}`) + } + const data = (await res.json()) as { projects: RecentProject[] } + return data.projects + } finally { + clearTimeout(timer) } - const data = (await res.json()) as { projects: RecentProject[] } - return data.projects } /** @@ -86,22 +111,36 @@ export class AdapterHttpClient { } async getGitInfo(sessionId: string): Promise { - const res = await fetch(`${this.httpBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/git-info`) - if (!res.ok) { - const err = await res.json().catch(() => ({ message: res.statusText })) - throw new Error(`Failed to load git info: ${(err as any).message}`) + const { controller, timer } = this.createTimeoutController() + try { + const res = await fetch(`${this.httpBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/git-info`, { + signal: controller.signal, + }) + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })) + throw new Error(`Failed to load git info: ${(err as any).message}`) + } + return (await res.json()) as GitInfo + } finally { + clearTimeout(timer) } - return (await res.json()) as GitInfo } async getTasksForSession(sessionId: string): Promise { - const res = await fetch(`${this.httpBaseUrl}/api/tasks/lists/${encodeURIComponent(sessionId)}`) - if (!res.ok) { - if (res.status === 404) return [] - const err = await res.json().catch(() => ({ message: res.statusText })) - throw new Error(`Failed to load tasks: ${(err as any).message}`) + const { controller, timer } = this.createTimeoutController() + try { + const res = await fetch(`${this.httpBaseUrl}/api/tasks/lists/${encodeURIComponent(sessionId)}`, { + signal: controller.signal, + }) + if (!res.ok) { + if (res.status === 404) return [] + const err = await res.json().catch(() => ({ message: res.statusText })) + throw new Error(`Failed to load tasks: ${(err as any).message}`) + } + const data = (await res.json()) as { tasks?: SessionTask[] } + return Array.isArray(data.tasks) ? data.tasks : [] + } finally { + clearTimeout(timer) } - const data = (await res.json()) as { tasks?: SessionTask[] } - return Array.isArray(data.tasks) ? data.tasks : [] } } diff --git a/adapters/feishu/index.ts b/adapters/feishu/index.ts index b08875ab..46b52aa4 100644 --- a/adapters/feishu/index.ts +++ b/adapters/feishu/index.ts @@ -144,10 +144,16 @@ async function dispatchOutboundImage(chatId: string, pending: PendingUpload): Pr break } case 'url': { - const resp = await fetch(pending.source.url) - if (!resp.ok) throw new Error(`fetch ${pending.source.url} -> ${resp.status}`) - buffer = Buffer.from(await resp.arrayBuffer()) - mime = pending.source.mime ?? resp.headers.get('content-type') ?? 'image/png' + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), 30_000) + try { + const resp = await fetch(pending.source.url, { signal: controller.signal }) + if (!resp.ok) throw new Error(`fetch ${pending.source.url} -> ${resp.status}`) + buffer = Buffer.from(await resp.arrayBuffer()) + mime = pending.source.mime ?? resp.headers.get('content-type') ?? 'image/png' + } finally { + clearTimeout(timer) + } break } }