Merge pull request #188 from lvjunjie-byte/fix/feishu-http-timeout

fix: 为飞书适配器的 HTTP 请求添加超时机制
This commit is contained in:
程序员阿江-Relakkes 2026-04-27 08:24:37 +08:00 committed by GitHub
commit 139dcbc4c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 31 deletions

View File

@ -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<typeof setTimeout>
} {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
return { controller, timer }
}
async createSession(workDir: string): Promise<string> {
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<RecentProject[]> {
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<GitInfo> {
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<SessionTask[]> {
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 : []
}
}

View File

@ -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
}
}