Files
Moltbot/extensions/feishu/src/monitor.state.ts
Mark L 097ad88f9d fix(feishu): tolerate missing webhook defaults in older plugin-sdk (openclaw#31639) thanks @liuxiaopai-ai
Verified:
- pnpm test extensions/feishu/src/monitor.state.defaults.test.ts
- pnpm exec vitest run extensions/feishu/src/monitor.state.defaults.test.ts
- pnpm exec oxfmt --check extensions/feishu/src/monitor.state.ts extensions/feishu/src/monitor.state.defaults.test.ts CHANGELOG.md
- CI note: non-required check "check" failed on unrelated  TS errors outside this PR scope.

Co-authored-by: liuxiaopai-ai <73659136+liuxiaopai-ai@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 07:42:16 -06:00

153 lines
4.5 KiB
TypeScript

import * as http from "http";
import * as Lark from "@larksuiteoapi/node-sdk";
import {
createFixedWindowRateLimiter,
createWebhookAnomalyTracker,
type RuntimeEnv,
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<string, Lark.WSClient>();
export const httpServers = new Map<string, http.Server>();
export const botOpenIds = new Map<string, string>();
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<WebhookRateLimitDefaults> | 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<WebhookAnomalyDefaults> | 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: feishuWebhookRateLimitDefaults.windowMs,
maxRequests: feishuWebhookRateLimitDefaults.maxRequests,
maxTrackedKeys: feishuWebhookRateLimitDefaults.maxTrackedKeys,
});
const feishuWebhookAnomalyTracker = createWebhookAnomalyTracker({
maxTrackedKeys: feishuWebhookAnomalyDefaults.maxTrackedKeys,
ttlMs: feishuWebhookAnomalyDefaults.ttlMs,
logEvery: feishuWebhookAnomalyDefaults.logEvery,
});
export function clearFeishuWebhookRateLimitStateForTest(): void {
feishuWebhookRateLimiter.clear();
feishuWebhookAnomalyTracker.clear();
}
export function getFeishuWebhookRateLimitStateSizeForTest(): number {
return feishuWebhookRateLimiter.size();
}
export function isWebhookRateLimitedForTest(key: string, nowMs: number): boolean {
return feishuWebhookRateLimiter.isRateLimited(key, nowMs);
}
export function recordWebhookStatus(
runtime: RuntimeEnv | undefined,
accountId: string,
path: string,
statusCode: number,
): void {
feishuWebhookAnomalyTracker.record({
key: `${accountId}:${path}:${statusCode}`,
statusCode,
log: runtime?.log ?? console.log,
message: (count) =>
`feishu[${accountId}]: webhook anomaly path=${path} status=${statusCode} count=${count}`,
});
}
export function stopFeishuMonitorState(accountId?: string): void {
if (accountId) {
wsClients.delete(accountId);
const server = httpServers.get(accountId);
if (server) {
server.close();
httpServers.delete(accountId);
}
botOpenIds.delete(accountId);
return;
}
wsClients.clear();
for (const server of httpServers.values()) {
server.close();
}
httpServers.clear();
botOpenIds.clear();
}