fix(feishu): avoid media regressions from global HTTP timeout (#36500)

* fix(feishu): avoid media regressions from global http timeout

* fix(feishu): source HTTP timeout from config

* fix(feishu): apply media timeout override to image uploads

* fix(feishu): invalidate cached client when timeout changes

* fix(feishu): clamp timeout values and cover image download
This commit is contained in:
Tak Hoffman
2026-03-05 12:13:40 -06:00
committed by GitHub
parent 09c68f8f0e
commit bc66a8fa81
5 changed files with 214 additions and 15 deletions

View File

@@ -1,9 +1,11 @@
import * as Lark from "@larksuiteoapi/node-sdk";
import { HttpsProxyAgent } from "https-proxy-agent";
import type { FeishuDomain, ResolvedFeishuAccount } from "./types.js";
import type { FeishuConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
/** Default HTTP timeout for Feishu API requests (30 seconds). */
export const FEISHU_HTTP_TIMEOUT_MS = 30_000;
export const FEISHU_HTTP_TIMEOUT_MAX_MS = 300_000;
export const FEISHU_HTTP_TIMEOUT_ENV_VAR = "OPENCLAW_FEISHU_HTTP_TIMEOUT_MS";
function getWsProxyAgent(): HttpsProxyAgent<string> | undefined {
const proxyUrl =
@@ -20,7 +22,7 @@ const clientCache = new Map<
string,
{
client: Lark.Client;
config: { appId: string; appSecret: string; domain?: FeishuDomain };
config: { appId: string; appSecret: string; domain?: FeishuDomain; httpTimeoutMs: number };
}
>();
@@ -39,11 +41,11 @@ function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
* but injects a default request timeout to prevent indefinite hangs
* (e.g. when the Feishu API is slow, causing per-chat queue deadlocks).
*/
function createTimeoutHttpInstance(): Lark.HttpInstance {
function createTimeoutHttpInstance(defaultTimeoutMs: number): Lark.HttpInstance {
const base: Lark.HttpInstance = Lark.defaultHttpInstance as unknown as Lark.HttpInstance;
function injectTimeout<D>(opts?: Lark.HttpRequestOptions<D>): Lark.HttpRequestOptions<D> {
return { timeout: FEISHU_HTTP_TIMEOUT_MS, ...opts } as Lark.HttpRequestOptions<D>;
return { timeout: defaultTimeoutMs, ...opts } as Lark.HttpRequestOptions<D>;
}
return {
@@ -67,14 +69,40 @@ export type FeishuClientCredentials = {
appId?: string;
appSecret?: string;
domain?: FeishuDomain;
httpTimeoutMs?: number;
config?: Pick<FeishuConfig, "httpTimeoutMs">;
};
function resolveConfiguredHttpTimeoutMs(creds: FeishuClientCredentials): number {
const clampTimeout = (value: number): number => {
const rounded = Math.floor(value);
return Math.min(Math.max(rounded, 1), FEISHU_HTTP_TIMEOUT_MAX_MS);
};
const envRaw = process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
if (envRaw) {
const envValue = Number(envRaw);
if (Number.isFinite(envValue) && envValue > 0) {
return clampTimeout(envValue);
}
}
const fromConfig = creds.config?.httpTimeoutMs;
const fromDirectField = creds.httpTimeoutMs;
const timeout = fromDirectField ?? fromConfig;
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) {
return FEISHU_HTTP_TIMEOUT_MS;
}
return clampTimeout(timeout);
}
/**
* Create or get a cached Feishu client for an account.
* Accepts any object with appId, appSecret, and optional domain/accountId.
*/
export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client {
const { accountId = "default", appId, appSecret, domain } = creds;
const defaultHttpTimeoutMs = resolveConfiguredHttpTimeoutMs(creds);
if (!appId || !appSecret) {
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
@@ -86,7 +114,8 @@ export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client
cached &&
cached.config.appId === appId &&
cached.config.appSecret === appSecret &&
cached.config.domain === domain
cached.config.domain === domain &&
cached.config.httpTimeoutMs === defaultHttpTimeoutMs
) {
return cached.client;
}
@@ -97,13 +126,13 @@ export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client
appSecret,
appType: Lark.AppType.SelfBuild,
domain: resolveDomain(domain),
httpInstance: createTimeoutHttpInstance(),
httpInstance: createTimeoutHttpInstance(defaultHttpTimeoutMs),
});
// Cache it
clientCache.set(accountId, {
client,
config: { appId, appSecret, domain },
config: { appId, appSecret, domain, httpTimeoutMs: defaultHttpTimeoutMs },
});
return client;