From bdca44693c3301c0fbd32b7011975db4bf7cdb80 Mon Sep 17 00:00:00 2001 From: bmendonca3 <208517100+bmendonca3@users.noreply.github.com> Date: Sat, 28 Feb 2026 10:22:02 -0700 Subject: [PATCH] Feishu: serialize startup bot-info probes --- CHANGELOG.md | 1 + extensions/feishu/src/monitor.ts | 17 ++++-- .../src/monitor.webhook-security.test.ts | 57 +++++++++++++++++++ 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92536147b30..308fd7e5b9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -267,6 +267,7 @@ Docs: https://docs.openclaw.ai - Agents/Ollama discovery: skip Ollama discovery when explicit models are configured. (#28827) Thanks @Kansodata and @vincentkoc. - Issues/triage labeling: consolidate bug intake to a single bug issue form with required bug-type classification (regression/crash/behavior), auto-apply matching subtype labels from issue form content, and retire the separate regression template to reduce misfiled issue types and improve queue filtering. Thanks @vincentkoc. - Android/Onboarding + voice reliability: request per-toggle onboarding permissions, update pairing guidance to `openclaw devices list/approve`, restore assistant speech playback in mic capture flow, cancel superseded in-flight speech (mute + per-reply token rotation), and keep `talk.config` loads retryable after transient failures. (#29796) Thanks @obviyus. +- Feishu/Startup probes: serialize multi-account bot-info probes during monitor startup so large Feishu account sets do not burst `/open-apis/bot/v3/info` and trigger avoidable rate limits. (#26685) - FS/Sandbox workspace boundaries: add a dedicated `outside-workspace` safe-open error code for root-escape checks, and propagate specific outside-workspace messages across edit/browser/media consumers instead of generic not-found/invalid-path fallbacks. (#29715) Thanks @YuzuruS. - Config/Doctor group allowlist diagnostics: align `groupPolicy: "allowlist"` warnings with per-channel runtime semantics by excluding Google Chat sender-list checks and by warning when no-fallback channels (for example iMessage) omit `groupAllowFrom`, with regression coverage. (#28477) Thanks @tonydehnke. - Slack/Disabled channel startup: skip Slack monitor socket startup entirely when `channels.slack.enabled=false` (including configs that still contain valid tokens), preventing disabled accounts from opening websocket connections. (#30586) Thanks @liuxiaopai-ai. diff --git a/extensions/feishu/src/monitor.ts b/extensions/feishu/src/monitor.ts index e44db2c2b61..ce782cb615c 100644 --- a/extensions/feishu/src/monitor.ts +++ b/extensions/feishu/src/monitor.ts @@ -334,6 +334,7 @@ type MonitorAccountParams = { account: ResolvedFeishuAccount; runtime?: RuntimeEnv; abortSignal?: AbortSignal; + botOpenId?: string; }; /** @@ -345,7 +346,7 @@ async function monitorSingleAccount(params: MonitorAccountParams): Promise const log = runtime?.log ?? console.log; // Fetch bot open_id - const botOpenId = await fetchBotOpenId(account); + const botOpenId = params.botOpenId ?? (await fetchBotOpenId(account)); botOpenIds.set(accountId, botOpenId ?? ""); log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`); @@ -546,17 +547,21 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi `feishu: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`, ); - // Start all accounts in parallel - await Promise.all( - accounts.map((account) => + const monitorPromises: Promise[] = []; + for (const account of accounts) { + // Probe sequentially so large multi-account startups do not burst Feishu's bot-info endpoint. + const botOpenId = await fetchBotOpenId(account); + monitorPromises.push( monitorSingleAccount({ cfg, account, runtime: opts.runtime, abortSignal: opts.abortSignal, + botOpenId, }), - ), - ); + ); + } + await Promise.all(monitorPromises); } /** diff --git a/extensions/feishu/src/monitor.webhook-security.test.ts b/extensions/feishu/src/monitor.webhook-security.test.ts index 9da288032de..53bb7c739d4 100644 --- a/extensions/feishu/src/monitor.webhook-security.test.ts +++ b/extensions/feishu/src/monitor.webhook-security.test.ts @@ -84,6 +84,27 @@ function buildConfig(params: { } as ClawdbotConfig; } +function buildMultiAccountWebsocketConfig(accountIds: string[]): ClawdbotConfig { + return { + channels: { + feishu: { + enabled: true, + accounts: Object.fromEntries( + accountIds.map((accountId) => [ + accountId, + { + enabled: true, + appId: `cli_${accountId}`, + appSecret: `secret_${accountId}`, + connectionMode: "websocket", + }, + ]), + ), + }, + }, + } as ClawdbotConfig; +} + async function withRunningWebhookMonitor( params: { accountId: string; @@ -206,4 +227,40 @@ describe("Feishu webhook security hardening", () => { isWebhookRateLimitedForTest("/feishu-rate-limit-stale:fresh", now + 60_001); expect(getFeishuWebhookRateLimitStateSizeForTest()).toBe(1); }); + + it("starts account probes sequentially to avoid startup bursts", async () => { + let inFlight = 0; + let maxInFlight = 0; + const started: string[] = []; + let releaseProbes!: () => void; + const probesReleased = new Promise((resolve) => { + releaseProbes = () => resolve(); + }); + probeFeishuMock.mockImplementation(async (account: { accountId: string }) => { + started.push(account.accountId); + inFlight += 1; + maxInFlight = Math.max(maxInFlight, inFlight); + await probesReleased; + inFlight -= 1; + return { ok: true, botOpenId: `bot_${account.accountId}` }; + }); + + const abortController = new AbortController(); + const monitorPromise = monitorFeishuProvider({ + config: buildMultiAccountWebsocketConfig(["alpha", "beta", "gamma"]), + abortSignal: abortController.signal, + }); + + try { + await Promise.resolve(); + await Promise.resolve(); + + expect(started).toEqual(["alpha"]); + expect(maxInFlight).toBe(1); + } finally { + releaseProbes(); + abortController.abort(); + await monitorPromise; + } + }); });