fix(feishu): bound group name cache clocks

This commit is contained in:
Peter Steinberger
2026-05-30 11:33:30 -04:00
parent 3da34a4673
commit ec15f90a55
2 changed files with 61 additions and 21 deletions
@@ -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 });
+30 -21
View File
@@ -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: {