feat: add sender profile cache with LRU eviction

- SenderNameCache: TTL 10min, LRU max 2048 entries, getOrFetch pattern
- resolveSenderName: API call with cache integration
- Trigger uses cache for approval card resolutions and audit metadata
- Add unit tests (9) for cache lifecycle, LRU, expiry, and null handling
This commit is contained in:
2026-07-08 17:22:23 +08:00
parent 2736006262
commit 4479f88f4f
4 changed files with 264 additions and 7 deletions
+38
View File
@@ -10,6 +10,7 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
import { dirname } from "node:path";
import type { FastifyBaseLogger } from "fastify";
import { DEFAULT_MAX_MESSAGE_LENGTH, splitAtBoundary } from "./textStream.js";
import type { SenderNameCache } from "./senderCache.js";
export interface FeishuConfig {
readonly appId: string;
@@ -95,6 +96,14 @@ export interface CardActionEvent {
readonly token?: string;
}
type ContactUserResponse = {
readonly data?: {
readonly user?: {
readonly name?: string | undefined;
} | undefined;
} | undefined;
} | null;
// --- Message helpers ---
export function buildOutboundPayload(content: string): OutboundPayload {
@@ -356,6 +365,35 @@ export async function removeReaction(rt: FeishuRuntime, messageId: string, react
}
}
/** Resolve a Feishu sender display name from open_id, optionally using a cache. */
export async function resolveSenderName(
rt: FeishuRuntime,
openId: string,
cache?: SenderNameCache,
): Promise<string | null> {
if (openId === "") return null;
const fetchName = async (id: string): Promise<string | null> => {
try {
const res = await rt.client.request({
method: "GET",
url: `/open-apis/contact/v3/users/${encodeURIComponent(id)}`,
params: { user_id_type: "open_id" },
}) as ContactUserResponse;
const name = res?.data?.user?.name;
return typeof name === "string" && name !== "" ? name : null;
} catch (e) {
rt.logger.warn({ openId: id, err: e instanceof Error ? e.message : String(e) }, "resolveSenderName failed");
return null;
}
};
if (cache !== undefined) {
return cache.getOrFetch(openId, fetchName);
}
return fetchName(openId);
}
// --- File helpers ---
/** Download a file/image from a Feishu message to a local path. */