diff --git a/CHANGELOG.md b/CHANGELOG.md index e031d97ac..269e5dfa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (`provider: "message"`) and normalize `lark`/`feishu` provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526) +- Feishu/Plugin sdk compatibility: add safe webhook default fallbacks when loading Feishu monitor state so mixed-version installs no longer crash if older `openclaw/plugin-sdk` builds omit webhook default constants. (#31606) - Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc. - Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204. - BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204. diff --git a/extensions/feishu/src/monitor.state.defaults.test.ts b/extensions/feishu/src/monitor.state.defaults.test.ts new file mode 100644 index 000000000..1fa4be409 --- /dev/null +++ b/extensions/feishu/src/monitor.state.defaults.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { + resolveFeishuWebhookAnomalyDefaultsForTest, + resolveFeishuWebhookRateLimitDefaultsForTest, +} from "./monitor.state.js"; + +describe("feishu monitor state defaults", () => { + it("falls back to hard defaults when sdk defaults are missing", () => { + expect(resolveFeishuWebhookRateLimitDefaultsForTest(undefined)).toEqual({ + windowMs: 60_000, + maxRequests: 120, + maxTrackedKeys: 4_096, + }); + expect(resolveFeishuWebhookAnomalyDefaultsForTest(undefined)).toEqual({ + maxTrackedKeys: 4_096, + ttlMs: 21_600_000, + logEvery: 25, + }); + }); + + it("keeps valid sdk values and repairs invalid fields", () => { + expect( + resolveFeishuWebhookRateLimitDefaultsForTest({ + windowMs: 45_000, + maxRequests: 0, + maxTrackedKeys: -1, + }), + ).toEqual({ + windowMs: 45_000, + maxRequests: 120, + maxTrackedKeys: 4_096, + }); + + expect( + resolveFeishuWebhookAnomalyDefaultsForTest({ + maxTrackedKeys: 2048, + ttlMs: Number.NaN, + logEvery: 10, + }), + ).toEqual({ + maxTrackedKeys: 2048, + ttlMs: 21_600_000, + logEvery: 10, + }); + }); +}); diff --git a/extensions/feishu/src/monitor.state.ts b/extensions/feishu/src/monitor.state.ts index 95a0beb3b..150a9adc2 100644 --- a/extensions/feishu/src/monitor.state.ts +++ b/extensions/feishu/src/monitor.state.ts @@ -4,8 +4,8 @@ import { createFixedWindowRateLimiter, createWebhookAnomalyTracker, type RuntimeEnv, - WEBHOOK_ANOMALY_COUNTER_DEFAULTS, - WEBHOOK_RATE_LIMIT_DEFAULTS, + WEBHOOK_ANOMALY_COUNTER_DEFAULTS as WEBHOOK_ANOMALY_COUNTER_DEFAULTS_FROM_SDK, + WEBHOOK_RATE_LIMIT_DEFAULTS as WEBHOOK_RATE_LIMIT_DEFAULTS_FROM_SDK, } from "openclaw/plugin-sdk"; export const wsClients = new Map(); @@ -15,16 +15,92 @@ export const botOpenIds = new Map(); export const FEISHU_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024; export const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000; +type WebhookRateLimitDefaults = { + windowMs: number; + maxRequests: number; + maxTrackedKeys: number; +}; + +type WebhookAnomalyDefaults = { + maxTrackedKeys: number; + ttlMs: number; + logEvery: number; +}; + +const FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS: WebhookRateLimitDefaults = { + windowMs: 60_000, + maxRequests: 120, + maxTrackedKeys: 4_096, +}; + +const FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS: WebhookAnomalyDefaults = { + maxTrackedKeys: 4_096, + ttlMs: 6 * 60 * 60_000, + logEvery: 25, +}; + +function coercePositiveInt(value: unknown, fallback: number): number { + if (typeof value !== "number" || !Number.isFinite(value)) { + return fallback; + } + const normalized = Math.floor(value); + return normalized > 0 ? normalized : fallback; +} + +export function resolveFeishuWebhookRateLimitDefaultsForTest( + defaults: unknown, +): WebhookRateLimitDefaults { + const resolved = defaults as Partial | null | undefined; + return { + windowMs: coercePositiveInt( + resolved?.windowMs, + FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.windowMs, + ), + maxRequests: coercePositiveInt( + resolved?.maxRequests, + FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.maxRequests, + ), + maxTrackedKeys: coercePositiveInt( + resolved?.maxTrackedKeys, + FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.maxTrackedKeys, + ), + }; +} + +export function resolveFeishuWebhookAnomalyDefaultsForTest( + defaults: unknown, +): WebhookAnomalyDefaults { + const resolved = defaults as Partial | null | undefined; + return { + maxTrackedKeys: coercePositiveInt( + resolved?.maxTrackedKeys, + FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.maxTrackedKeys, + ), + ttlMs: coercePositiveInt(resolved?.ttlMs, FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.ttlMs), + logEvery: coercePositiveInt( + resolved?.logEvery, + FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.logEvery, + ), + }; +} + +const feishuWebhookRateLimitDefaults = resolveFeishuWebhookRateLimitDefaultsForTest( + WEBHOOK_RATE_LIMIT_DEFAULTS_FROM_SDK, +); +const feishuWebhookAnomalyDefaults = resolveFeishuWebhookAnomalyDefaultsForTest( + WEBHOOK_ANOMALY_COUNTER_DEFAULTS_FROM_SDK, +); + export const feishuWebhookRateLimiter = createFixedWindowRateLimiter({ - windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs, - maxRequests: WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests, - maxTrackedKeys: WEBHOOK_RATE_LIMIT_DEFAULTS.maxTrackedKeys, + windowMs: feishuWebhookRateLimitDefaults.windowMs, + maxRequests: feishuWebhookRateLimitDefaults.maxRequests, + maxTrackedKeys: feishuWebhookRateLimitDefaults.maxTrackedKeys, }); const feishuWebhookAnomalyTracker = createWebhookAnomalyTracker({ - maxTrackedKeys: WEBHOOK_ANOMALY_COUNTER_DEFAULTS.maxTrackedKeys, - ttlMs: WEBHOOK_ANOMALY_COUNTER_DEFAULTS.ttlMs, - logEvery: WEBHOOK_ANOMALY_COUNTER_DEFAULTS.logEvery, + maxTrackedKeys: feishuWebhookAnomalyDefaults.maxTrackedKeys, + ttlMs: feishuWebhookAnomalyDefaults.ttlMs, + logEvery: feishuWebhookAnomalyDefaults.logEvery, }); export function clearFeishuWebhookRateLimitStateForTest(): void {