// 按调用方的固定窗口速率限制。判定委托给可注入原子后端:单进程用内存, // 多进程把 SOMA_RATE_LIMIT_STORE 指向共享文件系统,或由宿主注入 Redis/数据库实现。 import { MemoryRateLimitBackend, SharedFileRateLimitBackend, type RateLimitBackend, } from './RateLimitBackend'; import { trustedActorKey, type TrustedRequestContext } from './TrustedRequestContext'; export interface RateLimitDecision { allowed: boolean; remaining: number; retryAfterSeconds: number; } export interface RateLimitRule { limit: number; windowMs: number; } export class RateLimiter { private backend: RateLimitBackend; private readonly now: () => number; private configuredStore: string | undefined; constructor(now: () => number = Date.now, backend: RateLimitBackend = new MemoryRateLimitBackend()) { this.now = now; this.backend = backend; } async hit(key: string, rule: RateLimitRule | null): Promise { if (rule === null || rule.limit <= 0) { return { allowed: true, remaining: Number.POSITIVE_INFINITY, retryAfterSeconds: 0 }; } this.refreshConfiguredBackend(); return this.backend.hit(key, rule, this.now()); } setBackend(backend: RateLimitBackend): void { this.configuredStore = undefined; this.backend = backend; } async reset(): Promise { this.refreshConfiguredBackend(); await this.backend.reset?.(); } private refreshConfiguredBackend(): void { const store = process.env.SOMA_RATE_LIMIT_STORE?.trim() || undefined; if (store === this.configuredStore) return; this.configuredStore = store; this.backend = store ? new SharedFileRateLimitBackend(store) : new MemoryRateLimitBackend(); } } export function parseRateLimit(raw: string | undefined, fallback: RateLimitRule): RateLimitRule | null { const text = (raw ?? '').trim().toLowerCase(); if (text === '') return fallback; if (text === 'off' || text === '0') return null; const matched = /^(\d+)\/(\d+)$/.exec(text); if (!matched) { console.warn(`[soma-ai][接口] 限流配置「${raw}」看不懂(应形如 30/60 或 off),本项限流已关闭`); return null; } const limit = Number(matched[1]); const seconds = Number(matched[2]); if (limit <= 0 || seconds <= 0) return null; return { limit, windowMs: seconds * 1000 }; } /** * 认证主体优先。匿名来源只接受后续 TrustedRequestContext 验证过的值; * 原始 x-forwarded-for/x-real-ip 不再直接进入安全或配额主键。 */ export function callerKey( actor: TrustedRequestContext | null, subjectName?: string, ): string { if (subjectName) return `subject:${subjectName}`; const actorKey = trustedActorKey(actor); return actorKey ? `trusted:${actorKey}` : 'anonymous'; } export const sharedRateLimiter = new RateLimiter(); export type { RateLimitBackend } from './RateLimitBackend'; export { MemoryRateLimitBackend, SharedFileRateLimitBackend } from './RateLimitBackend';