Files
openclaw/extensions/feishu/src/monitor.startup.ts
Elian b9f48707dc fix(feishu): prevent silent group message drops when bot-info probe times out
When OpenClaw restarts under load, the Feishu bot-info probe
(`/open-apis/bot/v3/info`) can exceed the 10-second timeout due to
event-loop contention during channel initialization. This leaves
`botOpenId` empty, causing `checkBotMentioned()` to return `false`
for every group message — silently dropping them all while DMs
continue to work fine.

Two fixes:

1. **Increase startup probe timeout from 10s to 30s** and make it
   configurable via `OPENCLAW_FEISHU_STARTUP_PROBE_TIMEOUT_MS` env var.
   The previous 10s budget was too tight when multiple channels
   (Slack, Discord, Feishu) initialize concurrently.

2. **Graceful degradation in `checkBotMentioned()`**: when `botOpenId`
   is unknown, return `true` (assume mentioned) instead of `false`.
   This prevents group messages from being silently discarded when the
   probe fails for any reason. The trade-off is that the bot may
   respond to non-@-mentioned messages temporarily until the next
   successful probe, which is far preferable to total silence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 16:55:37 -07:00

82 lines
2.4 KiB
TypeScript

import type { RuntimeEnv } from "../runtime-api.js";
import { probeFeishu } from "./probe.js";
import type { ResolvedFeishuAccount } from "./types.js";
const FEISHU_STARTUP_BOT_INFO_TIMEOUT_DEFAULT_MS = 30_000;
const FEISHU_STARTUP_BOT_INFO_TIMEOUT_ENV = "OPENCLAW_FEISHU_STARTUP_PROBE_TIMEOUT_MS";
function resolveStartupProbeTimeoutMs(): number {
const raw = process.env[FEISHU_STARTUP_BOT_INFO_TIMEOUT_ENV];
if (raw) {
const parsed = Number(raw);
if (Number.isFinite(parsed) && parsed > 0) {
return Math.floor(parsed);
}
console.warn(
`[feishu] ${FEISHU_STARTUP_BOT_INFO_TIMEOUT_ENV}="${raw}" is invalid; using default ${FEISHU_STARTUP_BOT_INFO_TIMEOUT_DEFAULT_MS}ms`,
);
}
return FEISHU_STARTUP_BOT_INFO_TIMEOUT_DEFAULT_MS;
}
export const FEISHU_STARTUP_BOT_INFO_TIMEOUT_MS = resolveStartupProbeTimeoutMs();
type FetchBotOpenIdOptions = {
runtime?: RuntimeEnv;
abortSignal?: AbortSignal;
timeoutMs?: number;
};
export type FeishuMonitorBotIdentity = {
botOpenId?: string;
botName?: string;
};
function isTimeoutErrorMessage(message: string | undefined): boolean {
return message?.toLowerCase().includes("timeout") || message?.toLowerCase().includes("timed out")
? true
: false;
}
function isAbortErrorMessage(message: string | undefined): boolean {
return message?.toLowerCase().includes("aborted") ?? false;
}
export async function fetchBotIdentityForMonitor(
account: ResolvedFeishuAccount,
options: FetchBotOpenIdOptions = {},
): Promise<FeishuMonitorBotIdentity> {
if (options.abortSignal?.aborted) {
return {};
}
const timeoutMs = options.timeoutMs ?? FEISHU_STARTUP_BOT_INFO_TIMEOUT_MS;
const result = await probeFeishu(account, {
timeoutMs,
abortSignal: options.abortSignal,
});
if (result.ok) {
return { botOpenId: result.botOpenId, botName: result.botName };
}
if (options.abortSignal?.aborted || isAbortErrorMessage(result.error)) {
return {};
}
if (isTimeoutErrorMessage(result.error)) {
const error = options.runtime?.error ?? console.error;
error(
`feishu[${account.accountId}]: bot info probe timed out after ${timeoutMs}ms; continuing startup`,
);
}
return {};
}
export async function fetchBotOpenIdForMonitor(
account: ResolvedFeishuAccount,
options: FetchBotOpenIdOptions = {},
): Promise<string | undefined> {
const identity = await fetchBotIdentityForMonitor(account, options);
return identity.botOpenId;
}