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