3 changed files with 123 additions and 2 deletions
@ -0,0 +1,80 @@ |
|||
/** |
|||
* 角色卡限流(独立于 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<string, RateCounter>(); |
|||
const ipCounters = new Map<string, RateCounter>(); |
|||
|
|||
function getTodayStr(): string { |
|||
return new Date().toISOString().slice(0, 10); |
|||
} |
|||
|
|||
function getCounter(map: Map<string, RateCounter>, 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; |
|||
Loading…
Reference in new issue