fix(feishu): bound card action chat cache clocks

This commit is contained in:
Peter Steinberger
2026-05-30 11:36:11 -04:00
parent 8a99c0d17a
commit c94c43d3bb
2 changed files with 63 additions and 6 deletions
@@ -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: {
+30 -6
View File
@@ -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<typeof resolveFeishuRuntimeAccount>;
@@ -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(