From c94c43d3bb285e28fe922d0a3661f44efa8a7fef Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 11:36:11 -0400 Subject: [PATCH] fix(feishu): bound card action chat cache clocks --- extensions/feishu/src/bot.card-action.test.ts | 33 +++++++++++++++++ extensions/feishu/src/card-action.ts | 36 +++++++++++++++---- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/extensions/feishu/src/bot.card-action.test.ts b/extensions/feishu/src/bot.card-action.test.ts index eed9ce6a3972..d44cde6156d4 100644 --- a/extensions/feishu/src/bot.card-action.test.ts +++ b/extensions/feishu/src/bot.card-action.test.ts @@ -376,6 +376,39 @@ describe("Feishu Card Action Handler", () => { expect(createFeishuClientMock).toHaveBeenCalledTimes(1); }); + it("does not cache resolved chat type when expiry would exceed a valid Date", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(8_640_000_000_000_000)); + try { + const getChat = vi.fn().mockResolvedValue({ code: 0, data: { chat_type: "p2p" } }); + createFeishuClientMock.mockReturnValue({ + im: { + chat: { + get: getChat, + }, + }, + }); + const firstEvent = createCardActionEvent({ + token: "tok9b-boundary-1", + chatId: "oc_dm_chat_boundary", + actionValue: { text: "/help" }, + }); + const secondEvent = createCardActionEvent({ + token: "tok9b-boundary-2", + chatId: "oc_dm_chat_boundary", + actionValue: { text: "/help" }, + }); + + await handleFeishuCardAction({ cfg, event: firstEvent, runtime }); + await handleFeishuCardAction({ cfg, event: secondEvent, runtime }); + + expect(getChat).toHaveBeenCalledTimes(2); + expect(handleFeishuMessage).toHaveBeenCalledTimes(2); + } finally { + vi.useRealTimers(); + } + }); + it("uses resolved DM chat type when building approval cards without stored context", async () => { createFeishuClientMock.mockReturnValueOnce({ im: { diff --git a/extensions/feishu/src/card-action.ts b/extensions/feishu/src/card-action.ts index acc30412721b..08a7a376b8f7 100644 --- a/extensions/feishu/src/card-action.ts +++ b/extensions/feishu/src/card-action.ts @@ -1,3 +1,7 @@ +import { + asDateTimestampMs, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js"; @@ -47,6 +51,7 @@ export class FeishuRetryableCardActionError extends Error { export function resetProcessedFeishuCardActionTokensForTests(): void { processedCardActionTokens.clear(); + resolvedChatTypeCache.clear(); } function pruneProcessedCardActionTokens(now: number): void { @@ -185,8 +190,14 @@ const CHAT_TYPE_CACHE_TTL_MS = 30 * 60_000; const CHAT_TYPE_CACHE_MAX_SIZE = 5_000; function pruneChatTypeCache(now: number): void { + const validNow = asDateTimestampMs(now); + if (validNow === undefined) { + resolvedChatTypeCache.clear(); + return; + } for (const [key, entry] of resolvedChatTypeCache.entries()) { - if (entry.expiresAt <= now) { + const expiresAt = asDateTimestampMs(entry.expiresAt); + if (expiresAt === undefined || expiresAt <= validNow) { resolvedChatTypeCache.delete(key); } } @@ -206,6 +217,18 @@ function sanitizeLogValue(v: string): string { return v.replace(/[\r\n]/g, " ").slice(0, 500); } +function cacheResolvedCardActionChatType( + cacheKey: string, + value: "p2p" | "group", + now: number, +): void { + const expiresAt = resolveExpiresAtMsFromDurationMs(CHAT_TYPE_CACHE_TTL_MS, { nowMs: now }); + resolvedChatTypeCache.delete(cacheKey); + if (expiresAt !== undefined) { + resolvedChatTypeCache.set(cacheKey, { value, expiresAt }); + } +} + async function resolveCardActionChatType(params: { event: FeishuCardActionEvent; account: ReturnType; @@ -226,9 +249,13 @@ async function resolveCardActionChatType(params: { const now = Date.now(); pruneChatTypeCache(now); const cached = resolvedChatTypeCache.get(cacheKey); - if (cached) { + const cachedExpiresAt = cached ? asDateTimestampMs(cached.expiresAt) : undefined; + if (cached && cachedExpiresAt !== undefined) { return cached.value; } + if (cached) { + resolvedChatTypeCache.delete(cacheKey); + } try { const response = (await createFeishuClient(params.account).im.chat.get({ @@ -239,10 +266,7 @@ async function resolveCardActionChatType(params: { normalizeResolvedCardActionChatType(response.data?.chat_mode) ?? normalizeResolvedCardActionChatType(response.data?.chat_type); if (resolvedChatType) { - resolvedChatTypeCache.set(cacheKey, { - value: resolvedChatType, - expiresAt: now + CHAT_TYPE_CACHE_TTL_MS, - }); + cacheResolvedCardActionChatType(cacheKey, resolvedChatType, now); return resolvedChatType; } params.log(