From a8ae682bda8f9d5a55e8d185d5e8d979d1ee1b4e Mon Sep 17 00:00:00 2001 From: yunweibang Date: Thu, 12 Mar 2026 14:38:37 +0800 Subject: [PATCH] fix(feishu): add early event-level dedup to prevent duplicate replies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- extensions/feishu/src/monitor.account.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/extensions/feishu/src/monitor.account.ts b/extensions/feishu/src/monitor.account.ts index f7d40d8e280..d900cb2f2b6 100644 --- a/extensions/feishu/src/monitor.account.ts +++ b/extensions/feishu/src/monitor.account.ts @@ -1,6 +1,7 @@ import * as crypto from "crypto"; import * as Lark from "@larksuiteoapi/node-sdk"; import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk/feishu"; +import { createDedupeCache } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { raceWithTimeoutAndAbort } from "./async.js"; import { @@ -389,10 +390,24 @@ function registerEventHandlers( }, }); + // Early event-level dedup to drop duplicate webhook retries and WebSocket replays + // before they enter the debouncer or processing pipeline. The downstream dedup in + // handleFeishuMessage guards against restarts (persistent), but cannot prevent two + // concurrent dispatches of the same event from both being enqueued. + const eventDedup = createDedupeCache({ ttlMs: 5 * 60 * 1000, maxSize: 2_000 }); + eventDispatcher.register({ "im.message.receive_v1": async (data) => { + const event = data as unknown as FeishuMessageEvent; + const messageId = event.message?.message_id?.trim(); + if (messageId) { + const eventKey = `${accountId}:evt:${messageId}`; + if (!eventDedup.check(eventKey)) { + log(`feishu[${accountId}]: dropping duplicate event for message ${messageId}`); + return; + } + } const processMessage = async () => { - const event = data as unknown as FeishuMessageEvent; await inboundDebouncer.enqueue(event); }; if (fireAndForget) {