diff --git a/extensions/feishu/src/bot-group-name.test.ts b/extensions/feishu/src/bot-group-name.test.ts index ac647ac26a7a..31e8157102ff 100644 --- a/extensions/feishu/src/bot-group-name.test.ts +++ b/extensions/feishu/src/bot-group-name.test.ts @@ -84,6 +84,37 @@ describe("resolveGroupName", () => { expect(mockGetChatInfo).toHaveBeenCalledOnce(); // only 1 API call }); + it("does not cache group names when the expiry would exceed a valid Date", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(8_640_000_000_000_000)); + try { + mockGetChatInfo.mockResolvedValue({ name: "Boundary Group" }); + + const first = await resolveGroupName({ account, chatId: "oc_boundary", log }); + const second = await resolveGroupName({ account, chatId: "oc_boundary", log }); + + expect(first).toBe("Boundary Group"); + expect(second).toBe("Boundary Group"); + expect(mockGetChatInfo).toHaveBeenCalledTimes(2); + } finally { + vi.useRealTimers(); + } + }); + + it("evicts cached group names when the current clock is invalid", async () => { + mockGetChatInfo.mockResolvedValue({ name: "Cached Group" }); + await resolveGroupName({ account, chatId: "oc_invalid_clock", log }); + const dateNow = vi.spyOn(Date, "now").mockReturnValue(Number.NaN); + try { + const result = await resolveGroupName({ account, chatId: "oc_invalid_clock", log }); + + expect(result).toBe("Cached Group"); + } finally { + dateNow.mockRestore(); + } + expect(mockGetChatInfo).toHaveBeenCalledTimes(2); + }); + it("caches negative result (API failure) and skips retry", async () => { mockGetChatInfo.mockRejectedValue(new Error("fail")); await resolveGroupName({ account, chatId: "oc_test5", log }); diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 6ca5e65dfb21..c353f1dee6ba 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -10,7 +10,11 @@ import { resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute, } from "openclaw/plugin-sdk/conversation-runtime"; -import { parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime"; +import { + asDateTimestampMs, + parseStrictNonNegativeInteger, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow, @@ -108,9 +112,14 @@ function isFeishuTopicSessionScope(scope: FeishuGroupSessionScope): boolean { } function evictGroupNameCache(): void { - const now = Date.now(); + const now = asDateTimestampMs(Date.now()); + if (now === undefined) { + groupNameCache.clear(); + return; + } for (const [key, val] of groupNameCache) { - if (val.expiresAt <= now) { + const expiresAt = asDateTimestampMs(val.expiresAt); + if (expiresAt === undefined || expiresAt <= now) { groupNameCache.delete(key); } } @@ -128,9 +137,12 @@ function evictGroupNameCache(): void { } } -function setCacheEntry(key: string, value: { name: string; expiresAt: number }): void { +function setCacheEntry(key: string, name: string): void { + const expiresAt = resolveExpiresAtMsFromDurationMs(GROUP_NAME_CACHE_TTL_MS); groupNameCache.delete(key); - groupNameCache.set(key, value); + if (expiresAt !== undefined) { + groupNameCache.set(key, { name, expiresAt }); + } } export function clearGroupNameCache(): void { @@ -150,37 +162,34 @@ export async function resolveGroupName(params: { const cacheKey = `${account.accountId}:${chatId}`; const cached = groupNameCache.get(cacheKey); - if (cached && cached.expiresAt > Date.now()) { - return cached.name || undefined; + if (cached) { + const now = asDateTimestampMs(Date.now()); + const expiresAt = asDateTimestampMs(cached.expiresAt); + if (now !== undefined && expiresAt !== undefined && expiresAt > now) { + return cached.name || undefined; + } + groupNameCache.delete(cacheKey); } + let resolvedName: string | undefined; try { const client = createFeishuClient(account); const chatInfo = await getChatInfo(client, chatId); const name = chatInfo?.name?.trim(); if (name) { - setCacheEntry(cacheKey, { - name, - expiresAt: Date.now() + GROUP_NAME_CACHE_TTL_MS, - }); + setCacheEntry(cacheKey, name); + resolvedName = name; } else { - setCacheEntry(cacheKey, { - name: "", - expiresAt: Date.now() + GROUP_NAME_CACHE_TTL_MS, - }); + setCacheEntry(cacheKey, ""); } } catch (err) { log(`feishu[${account.accountId}]: getChatInfo failed for ${chatId}: ${String(err)}`); - setCacheEntry(cacheKey, { - name: "", - expiresAt: Date.now() + GROUP_NAME_CACHE_TTL_MS, - }); + setCacheEntry(cacheKey, ""); } - const result = groupNameCache.get(cacheKey)?.name || undefined; evictGroupNameCache(); - return result; + return resolvedName; } async function resolveFeishuAudioPreflightTranscript(params: {