fix(slack): bound conversation info cache (#101562)

This commit is contained in:
xingzhou 2026-07-07 20:55:00 +08:00 committed by GitHub
parent 407443264c
commit 4e29b2f5ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 76 additions and 7 deletions

View file

@ -178,6 +178,55 @@ describe("resolveSlackChannelType", () => {
expect(conversationsInfoMock).not.toHaveBeenCalled();
});
it("evicts least-recently-used conversation info entries after the cache limit", async () => {
const cacheMaxEntries = 1024;
const cfg = {
channels: {
slack: {
botToken: "xoxb-test",
},
},
} as never;
conversationsInfoMock.mockImplementation(async ({ channel }) => ({
channel: {
id: channel,
},
}));
for (let index = 0; index < cacheMaxEntries; index++) {
await resolveSlackConversationInfo({
cfg,
channelId: `C${index.toString().padStart(8, "0")}`,
});
}
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries);
await resolveSlackConversationInfo({
cfg,
channelId: "C00000000",
});
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries);
await resolveSlackConversationInfo({
cfg,
channelId: `C${cacheMaxEntries.toString().padStart(8, "0")}`,
});
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries + 1);
await resolveSlackConversationInfo({
cfg,
channelId: "C00000001",
});
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries + 2);
await resolveSlackConversationInfo({
cfg,
channelId: "C00000000",
});
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries + 2);
});
it("preserves the channel-type wrapper contract", async () => {
conversationsInfoMock.mockResolvedValueOnce({
channel: {

View file

@ -1,4 +1,5 @@
// Slack plugin module implements channel type behavior.
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
@ -13,8 +14,27 @@ export type SlackConversationInfo = {
user?: string;
};
const SLACK_CONVERSATION_INFO_CACHE_MAX_ENTRIES = 1024;
const SLACK_CONVERSATION_INFO_CACHE = new Map<string, SlackConversationInfo>();
function getCachedSlackConversationInfo(cacheKey: string): SlackConversationInfo | undefined {
const cached = SLACK_CONVERSATION_INFO_CACHE.get(cacheKey);
if (cached) {
SLACK_CONVERSATION_INFO_CACHE.delete(cacheKey);
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, cached);
}
return cached;
}
function setCachedSlackConversationInfo(
cacheKey: string,
conversationInfo: SlackConversationInfo,
): void {
SLACK_CONVERSATION_INFO_CACHE.delete(cacheKey);
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, conversationInfo);
pruneMapToMaxSize(SLACK_CONVERSATION_INFO_CACHE, SLACK_CONVERSATION_INFO_CACHE_MAX_ENTRIES);
}
export async function resolveSlackConversationInfo(params: {
cfg: OpenClawConfig;
accountId?: string | null;
@ -26,7 +46,7 @@ export async function resolveSlackConversationInfo(params: {
}
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
const cacheKey = `${account.accountId}:${channelId}`;
const cached = SLACK_CONVERSATION_INFO_CACHE.get(cacheKey);
const cached = getCachedSlackConversationInfo(cacheKey);
if (cached) {
return cached;
}
@ -42,7 +62,7 @@ export async function resolveSlackConversationInfo(params: {
groupChannels.includes(`mpim:${channelIdLower}`))
) {
const result = { type: "group" } as const;
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
setCachedSlackConversationInfo(cacheKey, result);
return result;
}
@ -59,7 +79,7 @@ export async function resolveSlackConversationInfo(params: {
})
) {
const result = { type: "channel" } as const;
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
setCachedSlackConversationInfo(cacheKey, result);
return result;
}
@ -70,7 +90,7 @@ export async function resolveSlackConversationInfo(params: {
if (!token) {
const result = { type: isNativeImChannel ? "dm" : "unknown" } as const;
if (!isNativeImChannel) {
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
setCachedSlackConversationInfo(cacheKey, result);
}
return result;
}
@ -89,7 +109,7 @@ export async function resolveSlackConversationInfo(params: {
: undefined;
const result: SlackConversationInfo = user ? { type: "dm", user } : { type: "dm" };
if (user) {
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
setCachedSlackConversationInfo(cacheKey, result);
}
return result;
}
@ -97,12 +117,12 @@ export async function resolveSlackConversationInfo(params: {
const channel = info.channel as { is_im?: boolean; is_mpim?: boolean } | undefined;
const type = channel?.is_im ? "dm" : channel?.is_mpim ? "group" : "channel";
const result = { type } as const;
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
setCachedSlackConversationInfo(cacheKey, result);
return result;
} catch {
const result = { type: isNativeImChannel ? "dm" : "unknown" } as const;
if (!isNativeImChannel) {
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
setCachedSlackConversationInfo(cacheKey, result);
}
return result;
}