fix(feishu): add early event-level dedup to prevent duplicate replies (#43762)

* fix(feishu): add early event-level dedup to prevent duplicate replies

Add synchronous in-memory dedup at EventDispatcher handler level using
message_id as key with 5-minute TTL and 2000-entry cap.

This catches duplicate events immediately when they arrive from the Lark
SDK — before the inbound debouncer or processing queue — preventing the
race condition where two concurrent dispatches enter the pipeline before
either records the messageId in the downstream dedup layer.

Fixes the root cause reported in #42687.

* fix(feishu): correct inverted dedup condition

check() returns false on first call (new key) and true on subsequent
calls (duplicate). The previous `!check()` guard was inverted —
dropping every first delivery and passing all duplicates.

Remove the negation so the guard correctly drops duplicates.

* fix(feishu): simplify eventDedup key — drop redundant accountId prefix

eventDedup is already scoped per account (one instance per
registerEventHandlers call), so the accountId prefix in the cache key
is redundant. Use `evt:${messageId}` instead.

* fix(feishu): share inbound processing claim dedupe

---------

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
yunweibang
2026-03-14 11:37:40 +08:00
committed by GitHub
parent 2659fc6c97
commit f4a2bbe0c9
6 changed files with 210 additions and 45 deletions

View File

@@ -3,6 +3,7 @@ import { pruneMapToMaxSize } from "./map-size.js";
export type DedupeCache = {
check: (key: string | undefined | null, now?: number) => boolean;
peek: (key: string | undefined | null, now?: number) => boolean;
delete: (key: string | undefined | null) => void;
clear: () => void;
size: () => number;
};
@@ -71,6 +72,12 @@ export function createDedupeCache(options: DedupeCacheOptions): DedupeCache {
}
return hasUnexpired(key, now, false);
},
delete: (key) => {
if (!key) {
return;
}
cache.delete(key);
},
clear: () => {
cache.clear();
},