/** * 角色卡限流(独立于 agent 的 rate-limit.ts)。 * * 仅对未登录用户(访客)生效,与 agent 采用相同的阈值与实现: * - 每会话上限 20 条 * - 每 IP 每日上限 50 条 * 内存计数(Map),按日期字符串跨天自动重置;进程重启即清零(与 agent 一致)。 */ const SESSION_LIMIT = 20; const IP_DAILY_LIMIT = 50; interface RateCounter { count: number; date: string; } const sessionCounters = new Map(); const ipCounters = new Map(); function getTodayStr(): string { return new Date().toISOString().slice(0, 10); } function getCounter(map: Map, key: string): RateCounter { const today = getTodayStr(); let counter = map.get(key); if (!counter || counter.date !== today) { counter = { count: 0, date: today }; map.set(key, counter); } return counter; } function buildSessionKey(cardSlug: string, sessionId: string): string { return `character:${cardSlug}:${sessionId}`; } function buildIpKey(cardSlug: string, ip: string): string { return `character:${cardSlug}:${ip}`; } export interface CharacterRateLimitResult { sessionRemaining: number; ipRemaining: number; blocked: boolean; } export function checkCharacterRateLimit( cardSlug: string, sessionId: string, ip: string, ): CharacterRateLimitResult { const sessionKey = buildSessionKey(cardSlug, sessionId); const ipKey = buildIpKey(cardSlug, ip); const sessionCounter = getCounter(sessionCounters, sessionKey); const ipCounter = getCounter(ipCounters, ipKey); const sessionRemaining = Math.max(0, SESSION_LIMIT - sessionCounter.count); const ipRemaining = Math.max(0, IP_DAILY_LIMIT - ipCounter.count); const blocked = sessionRemaining === 0 || ipRemaining === 0; return { sessionRemaining, ipRemaining, blocked }; } export function incrementCharacterRateLimit( cardSlug: string, sessionId: string, ip: string, ): void { const sessionKey = buildSessionKey(cardSlug, sessionId); const ipKey = buildIpKey(cardSlug, ip); const sessionCounter = getCounter(sessionCounters, sessionKey); const ipCounter = getCounter(ipCounters, ipKey); sessionCounter.count++; ipCounter.count++; } export const CHARACTER_SESSION_LIMIT = SESSION_LIMIT; export const CHARACTER_IP_DAILY_LIMIT = IP_DAILY_LIMIT;