Skip to main content
Glama
rate-limiter.js4.28 kB
/** * 速率限制器 * 防止 API 濫用和過載 */ /** * 速率限制器類 */ export class RateLimiter { /** * @param {number} maxRequests - 時間窗口內最大請求數 * @param {number} windowMs - 時間窗口(毫秒) */ constructor(maxRequests = 100, windowMs = 60000) { this.maxRequests = maxRequests; this.windowMs = windowMs; this.requests = new Map(); // conversationId -> [timestamps] this.blocked = new Set(); // 被阻止的 conversationId this.violations = new Map(); // conversationId -> 違規次數 // 定期清理過期數據 this.cleanupInterval = setInterval(() => this.cleanup(), windowMs); } /** * 檢查是否超過速率限制 * @param {string} conversationId - 對話 ID * @param {string} toolName - 工具名稱(用於記錄) * @returns {Object} { allowed: boolean, remaining: number, resetTime: number } */ checkLimit(conversationId, toolName = 'unknown') { // 檢查是否被永久阻止 if (this.blocked.has(conversationId)) { return { allowed: false, remaining: 0, resetTime: Infinity, reason: 'Blocked due to repeated violations' }; } const now = Date.now(); const requests = this.requests.get(conversationId) || []; // 清理過期請求 const validRequests = requests.filter(ts => now - ts < this.windowMs); // 檢查是否超過限制 if (validRequests.length >= this.maxRequests) { this.recordViolation(conversationId); const oldestRequest = Math.min(...validRequests); const resetTime = oldestRequest + this.windowMs; return { allowed: false, remaining: 0, resetTime, reason: `Rate limit exceeded: ${validRequests.length}/${this.maxRequests} requests in ${this.windowMs}ms` }; } // 記錄新請求 validRequests.push(now); this.requests.set(conversationId, validRequests); return { allowed: true, remaining: this.maxRequests - validRequests.length, resetTime: now + this.windowMs, reason: null }; } /** * 記錄違規 * @param {string} conversationId - 對話 ID */ recordViolation(conversationId) { const violations = (this.violations.get(conversationId) || 0) + 1; this.violations.set(conversationId, violations); // 超過 5 次違規則永久阻止 if (violations >= 5) { this.blocked.add(conversationId); console.error(`[RateLimiter] Blocked ${conversationId} after ${violations} violations`); } } /** * 清理過期數據 */ cleanup() { const now = Date.now(); let cleaned = 0; // 清理過期請求記錄 for (const [conversationId, requests] of this.requests.entries()) { const validRequests = requests.filter(ts => now - ts < this.windowMs); if (validRequests.length === 0) { this.requests.delete(conversationId); cleaned++; } else { this.requests.set(conversationId, validRequests); } } if (cleaned > 0) { console.error(`[RateLimiter] Cleaned ${cleaned} expired conversation records`); } } /** * 解除對話阻止 * @param {string} conversationId - 對話 ID */ unblock(conversationId) { this.blocked.delete(conversationId); this.violations.delete(conversationId); this.requests.delete(conversationId); } /** * 獲取統計信息 * @returns {Object} */ getStats() { return { activeConversations: this.requests.size, blockedConversations: this.blocked.size, totalViolations: Array.from(this.violations.values()).reduce((sum, v) => sum + v, 0), config: { maxRequests: this.maxRequests, windowMs: this.windowMs } }; } /** * 停止清理定時器 */ stop() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } } /** * 全局速率限制器實例 */ export const globalRateLimiter = new RateLimiter(100, 60000); // 每分鐘最多 100 個請求 export default RateLimiter;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/win10ogod/memory-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server