From 44941f0ed15b221490860768f9548f0bba63ccf1 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:22:58 -0700 Subject: [PATCH] fix: activate WeCom callback message deduplication (#10305) (#10588) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WecomCallbackAdapter declared a _seen_messages dict and MESSAGE_DEDUP_TTL_SECONDS constant but never actually checked them in _handle_callback(). WeCom retries callback deliveries on timeout, and each retry with the same MsgId was treated as a fresh message and queued for processing. Fix: check _seen_messages before enqueuing. Uses the same TTL- based pattern as MessageDeduplicator (fixed in #10306) — check age before returning duplicate, prune on overflow. Closes #10305 --- gateway/platforms/wecom_callback.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gateway/platforms/wecom_callback.py b/gateway/platforms/wecom_callback.py index 4bb67d5cf..5440792de 100644 --- a/gateway/platforms/wecom_callback.py +++ b/gateway/platforms/wecom_callback.py @@ -258,6 +258,20 @@ class WecomCallbackAdapter(BasePlatformAdapter): ) event = self._build_event(app, decrypted) if event is not None: + # Deduplicate: WeCom retries callbacks on timeout, + # producing duplicate inbound messages (#10305). + if event.message_id: + now = time.time() + if event.message_id in self._seen_messages: + if now - self._seen_messages[event.message_id] < MESSAGE_DEDUP_TTL_SECONDS: + logger.debug("[WecomCallback] Duplicate MsgId %s, skipping", event.message_id) + return web.Response(text="success", content_type="text/plain") + del self._seen_messages[event.message_id] + self._seen_messages[event.message_id] = now + # Prune expired entries when cache grows large + if len(self._seen_messages) > 2000: + cutoff = now - MESSAGE_DEDUP_TTL_SECONDS + self._seen_messages = {k: v for k, v in self._seen_messages.items() if v > cutoff} # Record which app this user belongs to. if event.source and event.source.user_id: map_key = self._user_app_key(