154 lines
5.1 KiB
JavaScript
154 lines
5.1 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
* SPDX-License-Identifier: MIT
|
|
*
|
|
* Account-scoped LRU cache for Feishu group/chat metadata.
|
|
*
|
|
* Caches the result of `im.chat.get` (chat_mode, group_message_type, etc.)
|
|
* to avoid repeated OAPI calls for every inbound message.
|
|
*
|
|
* Key fields cached:
|
|
* - `chat_mode`: "group" | "topic" | "p2p"
|
|
* - `group_message_type`: "chat" | "thread" (only for chat_mode=group)
|
|
*/
|
|
import { LarkClient } from './lark-client';
|
|
import { larkLogger } from './lark-logger';
|
|
const log = larkLogger('core/chat-info-cache');
|
|
// ---------------------------------------------------------------------------
|
|
// Cache implementation
|
|
// ---------------------------------------------------------------------------
|
|
const DEFAULT_MAX_SIZE = 500;
|
|
const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
class ChatInfoCache {
|
|
map = new Map();
|
|
maxSize;
|
|
ttlMs;
|
|
constructor(maxSize = DEFAULT_MAX_SIZE, ttlMs = DEFAULT_TTL_MS) {
|
|
this.maxSize = maxSize;
|
|
this.ttlMs = ttlMs;
|
|
}
|
|
get(chatId) {
|
|
const entry = this.map.get(chatId);
|
|
if (!entry)
|
|
return undefined;
|
|
if (entry.expireAt <= Date.now()) {
|
|
this.map.delete(chatId);
|
|
return undefined;
|
|
}
|
|
// LRU refresh
|
|
this.map.delete(chatId);
|
|
this.map.set(chatId, entry);
|
|
return entry.info;
|
|
}
|
|
set(chatId, info) {
|
|
this.map.delete(chatId);
|
|
this.map.set(chatId, { info, expireAt: Date.now() + this.ttlMs });
|
|
this.evict();
|
|
}
|
|
clear() {
|
|
this.map.clear();
|
|
}
|
|
evict() {
|
|
while (this.map.size > this.maxSize) {
|
|
const oldest = this.map.keys().next().value;
|
|
if (oldest !== undefined)
|
|
this.map.delete(oldest);
|
|
}
|
|
}
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Account-scoped singleton registry
|
|
// ---------------------------------------------------------------------------
|
|
const registry = new Map();
|
|
function getChatInfoCache(accountId) {
|
|
let c = registry.get(accountId);
|
|
if (!c) {
|
|
c = new ChatInfoCache();
|
|
registry.set(accountId, c);
|
|
}
|
|
return c;
|
|
}
|
|
/** Clear chat-info caches (called from LarkClient.clearCache). */
|
|
export function clearChatInfoCache(accountId) {
|
|
if (accountId !== undefined) {
|
|
registry.get(accountId)?.clear();
|
|
registry.delete(accountId);
|
|
}
|
|
else {
|
|
for (const c of registry.values())
|
|
c.clear();
|
|
registry.clear();
|
|
}
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Public API
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Determine whether a group supports thread sessions.
|
|
*
|
|
* Returns `true` when the group is a topic group (`chat_mode=topic`) or
|
|
* a normal group with thread message mode (`group_message_type=thread`).
|
|
*
|
|
* Results are cached per-account with a 1-hour TTL to minimise OAPI calls.
|
|
*/
|
|
export async function isThreadCapableGroup(params) {
|
|
const { cfg, chatId, accountId } = params;
|
|
const info = await getChatInfo({ cfg, chatId, accountId });
|
|
if (!info)
|
|
return false;
|
|
return info.chatMode === 'topic' || info.groupMessageType === 'thread';
|
|
}
|
|
/**
|
|
* Fetch (or read from cache) the chat metadata for a given chat ID.
|
|
*
|
|
* Returns `undefined` when the API call fails (best-effort).
|
|
*/
|
|
export async function getChatInfo(params) {
|
|
const { cfg, chatId, accountId } = params;
|
|
const effectiveAccountId = accountId ?? 'default';
|
|
const cache = getChatInfoCache(effectiveAccountId);
|
|
const cached = cache.get(chatId);
|
|
if (cached)
|
|
return cached;
|
|
try {
|
|
const sdk = LarkClient.fromCfg(cfg, accountId).sdk;
|
|
const response = await sdk.im.chat.get({
|
|
path: { chat_id: chatId },
|
|
});
|
|
const data = response?.data;
|
|
const chatMode = data?.chat_mode ?? 'group';
|
|
const groupMessageType = data?.group_message_type;
|
|
const info = {
|
|
chatMode: chatMode,
|
|
groupMessageType: groupMessageType,
|
|
};
|
|
cache.set(chatId, info);
|
|
log.info(`resolved ${chatId} → chat_mode=${chatMode}, group_message_type=${groupMessageType ?? 'N/A'}`);
|
|
return info;
|
|
}
|
|
catch (err) {
|
|
log.error(`failed to get chat info for ${chatId}: ${String(err)}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// getChatTypeFeishu
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Determine the chat type (p2p or group) for a given chat ID.
|
|
*
|
|
* Delegates to the shared {@link getChatInfo} cache (account-scoped LRU with
|
|
* 1-hour TTL) so that chat metadata is fetched at most once across all
|
|
* call-sites (dispatch, reaction handler, etc.).
|
|
*
|
|
* Falls back to "p2p" if the API call fails.
|
|
*/
|
|
export async function getChatTypeFeishu(params) {
|
|
const { cfg, chatId, accountId } = params;
|
|
const info = await getChatInfo({ cfg, chatId, accountId });
|
|
if (!info)
|
|
return 'p2p';
|
|
return info.chatMode === 'group' || info.chatMode === 'topic' ? 'group' : 'p2p';
|
|
}
|