mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 13:30:48 +00:00
* Feishu: cache failing probes * Changelog: add Feishu probe failure backoff note --------- Co-authored-by: bmendonca3 <208517100+bmendonca3@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
157 lines
4.4 KiB
TypeScript
157 lines
4.4 KiB
TypeScript
import { raceWithTimeoutAndAbort } from "./async.js";
|
|
import { createFeishuClient, type FeishuClientCredentials } from "./client.js";
|
|
import type { FeishuProbeResult } from "./types.js";
|
|
|
|
/** Cache probe results to reduce repeated health-check calls.
|
|
* Gateway health checks call probeFeishu() every minute; without caching this
|
|
* burns ~43,200 calls/month, easily exceeding Feishu's free-tier quota.
|
|
* Successful bot info is effectively static, while failures are cached briefly
|
|
* to avoid hammering the API during transient outages. */
|
|
const probeCache = new Map<string, { result: FeishuProbeResult; expiresAt: number }>();
|
|
const PROBE_SUCCESS_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
const PROBE_ERROR_TTL_MS = 60 * 1000; // 1 minute
|
|
const MAX_PROBE_CACHE_SIZE = 64;
|
|
export const FEISHU_PROBE_REQUEST_TIMEOUT_MS = 10_000;
|
|
export type ProbeFeishuOptions = {
|
|
timeoutMs?: number;
|
|
abortSignal?: AbortSignal;
|
|
};
|
|
|
|
type FeishuBotInfoResponse = {
|
|
code: number;
|
|
msg?: string;
|
|
bot?: { bot_name?: string; open_id?: string };
|
|
data?: { bot?: { bot_name?: string; open_id?: string } };
|
|
};
|
|
|
|
function setCachedProbeResult(
|
|
cacheKey: string,
|
|
result: FeishuProbeResult,
|
|
ttlMs: number,
|
|
): FeishuProbeResult {
|
|
probeCache.set(cacheKey, { result, expiresAt: Date.now() + ttlMs });
|
|
if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
|
|
const oldest = probeCache.keys().next().value;
|
|
if (oldest !== undefined) {
|
|
probeCache.delete(oldest);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export async function probeFeishu(
|
|
creds?: FeishuClientCredentials,
|
|
options: ProbeFeishuOptions = {},
|
|
): Promise<FeishuProbeResult> {
|
|
if (!creds?.appId || !creds?.appSecret) {
|
|
return {
|
|
ok: false,
|
|
error: "missing credentials (appId, appSecret)",
|
|
};
|
|
}
|
|
if (options.abortSignal?.aborted) {
|
|
return {
|
|
ok: false,
|
|
appId: creds.appId,
|
|
error: "probe aborted",
|
|
};
|
|
}
|
|
|
|
const timeoutMs = options.timeoutMs ?? FEISHU_PROBE_REQUEST_TIMEOUT_MS;
|
|
|
|
// Return cached result if still valid.
|
|
// Use accountId when available; otherwise include appSecret prefix so two
|
|
// accounts sharing the same appId (e.g. after secret rotation) don't
|
|
// pollute each other's cache entry.
|
|
const cacheKey = creds.accountId ?? `${creds.appId}:${creds.appSecret.slice(0, 8)}`;
|
|
const cached = probeCache.get(cacheKey);
|
|
if (cached && cached.expiresAt > Date.now()) {
|
|
return cached.result;
|
|
}
|
|
|
|
try {
|
|
const client = createFeishuClient(creds);
|
|
// Use bot/v3/info API to get bot information
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK generic request method
|
|
const responseResult = await raceWithTimeoutAndAbort<FeishuBotInfoResponse>(
|
|
(client as any).request({
|
|
method: "GET",
|
|
url: "/open-apis/bot/v3/info",
|
|
data: {},
|
|
timeout: timeoutMs,
|
|
}) as Promise<FeishuBotInfoResponse>,
|
|
{
|
|
timeoutMs,
|
|
abortSignal: options.abortSignal,
|
|
},
|
|
);
|
|
|
|
if (responseResult.status === "aborted") {
|
|
return {
|
|
ok: false,
|
|
appId: creds.appId,
|
|
error: "probe aborted",
|
|
};
|
|
}
|
|
if (responseResult.status === "timeout") {
|
|
return setCachedProbeResult(
|
|
cacheKey,
|
|
{
|
|
ok: false,
|
|
appId: creds.appId,
|
|
error: `probe timed out after ${timeoutMs}ms`,
|
|
},
|
|
PROBE_ERROR_TTL_MS,
|
|
);
|
|
}
|
|
|
|
const response = responseResult.value;
|
|
if (options.abortSignal?.aborted) {
|
|
return {
|
|
ok: false,
|
|
appId: creds.appId,
|
|
error: "probe aborted",
|
|
};
|
|
}
|
|
|
|
if (response.code !== 0) {
|
|
return setCachedProbeResult(
|
|
cacheKey,
|
|
{
|
|
ok: false,
|
|
appId: creds.appId,
|
|
error: `API error: ${response.msg || `code ${response.code}`}`,
|
|
},
|
|
PROBE_ERROR_TTL_MS,
|
|
);
|
|
}
|
|
|
|
const bot = response.bot || response.data?.bot;
|
|
return setCachedProbeResult(
|
|
cacheKey,
|
|
{
|
|
ok: true,
|
|
appId: creds.appId,
|
|
botName: bot?.bot_name,
|
|
botOpenId: bot?.open_id,
|
|
},
|
|
PROBE_SUCCESS_TTL_MS,
|
|
);
|
|
} catch (err) {
|
|
return setCachedProbeResult(
|
|
cacheKey,
|
|
{
|
|
ok: false,
|
|
appId: creds.appId,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
},
|
|
PROBE_ERROR_TTL_MS,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Clear the probe cache (for testing). */
|
|
export function clearProbeCache(): void {
|
|
probeCache.clear();
|
|
}
|