openclaw/extensions/imessage/src/monitor/self-chat-cache.ts

104 lines
2.8 KiB
TypeScript

import { createHash } from "node:crypto";
import { formatIMessageChatTarget } from "../targets.js";
type SelfChatCacheKeyParts = {
accountId: string;
sender: string;
isGroup: boolean;
chatId?: number;
};
export type SelfChatLookup = SelfChatCacheKeyParts & {
text?: string;
createdAt?: number;
};
export type SelfChatCache = {
remember: (lookup: SelfChatLookup) => void;
has: (lookup: SelfChatLookup) => boolean;
};
const SELF_CHAT_TTL_MS = 10_000;
const MAX_SELF_CHAT_CACHE_ENTRIES = 512;
const CLEANUP_MIN_INTERVAL_MS = 1_000;
function normalizeText(text: string | undefined): string | null {
if (!text) {
return null;
}
const normalized = text.replace(/\r\n?/g, "\n").trim();
return normalized ? normalized : null;
}
function isUsableTimestamp(createdAt: number | undefined): createdAt is number {
return typeof createdAt === "number" && Number.isFinite(createdAt);
}
function digestText(text: string): string {
return createHash("sha256").update(text).digest("hex");
}
function buildScope(parts: SelfChatCacheKeyParts): string {
if (!parts.isGroup) {
return `${parts.accountId}:imessage:${parts.sender}`;
}
const chatTarget = formatIMessageChatTarget(parts.chatId) || "chat_id:unknown";
return `${parts.accountId}:${chatTarget}:imessage:${parts.sender}`;
}
class DefaultSelfChatCache implements SelfChatCache {
private cache = new Map<string, number>();
private lastCleanupAt = 0;
private buildKey(lookup: SelfChatLookup): string | null {
const text = normalizeText(lookup.text);
if (!text || !isUsableTimestamp(lookup.createdAt)) {
return null;
}
return `${buildScope(lookup)}:${lookup.createdAt}:${digestText(text)}`;
}
remember(lookup: SelfChatLookup): void {
const key = this.buildKey(lookup);
if (!key) {
return;
}
this.cache.set(key, Date.now());
this.maybeCleanup();
}
has(lookup: SelfChatLookup): boolean {
this.maybeCleanup();
const key = this.buildKey(lookup);
if (!key) {
return false;
}
const timestamp = this.cache.get(key);
return typeof timestamp === "number" && Date.now() - timestamp <= SELF_CHAT_TTL_MS;
}
private maybeCleanup(): void {
const now = Date.now();
if (now - this.lastCleanupAt < CLEANUP_MIN_INTERVAL_MS) {
return;
}
this.lastCleanupAt = now;
for (const [key, timestamp] of this.cache.entries()) {
if (now - timestamp > SELF_CHAT_TTL_MS) {
this.cache.delete(key);
}
}
while (this.cache.size > MAX_SELF_CHAT_CACHE_ENTRIES) {
const oldestKey = this.cache.keys().next().value;
if (typeof oldestKey !== "string") {
break;
}
this.cache.delete(oldestKey);
}
}
}
export function createSelfChatCache(): SelfChatCache {
return new DefaultSelfChatCache();
}