// server/index.js (نسخه بهینه شده: انتخاب تصادفی کلیدها + پایداری بالا) const express = require('express'); const path = require('node:path'); const { WebSocketServer, WebSocket } = require('ws'); const http = require('node:http'); require('dotenv').config(); const app = express(); // افزایش محدودیت‌ها برای جلوگیری از خطای هدر در درخواست‌های سنگین const server = http.createServer({ maxHeaderSize: 16384 // 16KB }, app); const wss = new WebSocketServer({ server, clientTracking: true, // برای مدیریت هارت‌بیت و بستن اتصالات مرده perMessageDeflate: false // غیرفعال کردن فشرده‌سازی برای کاهش بار CPU و حافظه }); // --- بخش مدیریت تنظیمات شخصیت‌ها --- const instructionSecretNames = { default: 'PERSONALITY_DEFAULT', teacher: 'PERSONALITY_TEACHER', poetic: 'PERSONALITY_POETIC', funny: 'PERSONALITY_FUNNY', }; const personalityInstructions = {}; console.log('🔄 در حال خواندن دستورالعمل‌های شخصیت از Secrets...'); Object.keys(instructionSecretNames).forEach(key => { const secretName = instructionSecretNames[key]; const instruction = process.env[secretName]; if (instruction) { personalityInstructions[key] = instruction; } else { personalityInstructions[key] = `دستورالعمل '${key}' یافت نشد.`; } }); // --- بخش مدیریت کلیدهای API (Random Selection) --- const apiKeysEnv = process.env.ALL_GEMINI_API_KEYS; // تبدیل رشته کلیدها به آرایه و حذف فاصله‌های اضافی const apiKeys = apiKeysEnv ? apiKeysEnv.split(',').map(key => key.trim()).filter(key => key) : []; if (apiKeys.length === 0) { console.error('🔴 خطای حیاتی: هیچ کلید API یافت نشد! لطفا متغیر ALL_GEMINI_API_KEYS را تنظیم کنید.'); process.exit(1); } console.log(`🚀 سرور با ${apiKeys.length} کلید API آماده‌سازی شد. (حالت انتخاب تصادفی)`); // --- مکانیزم Heartbeat برای جلوگیری از قطعی و هنگ کردن سرور --- // این تابع هر ۳۰ ثانیه اتصالات مرده را پاک می‌کند تا حافظه آزاد شود function heartbeat() { this.isAlive = true; } const interval = setInterval(function ping() { wss.clients.forEach(function each(ws) { if (ws.isAlive === false) { // اگر کلاینت پاسخ نداد، اتصال را قطع کن تا حافظه آزاد شود return ws.terminate(); } ws.isAlive = false; ws.ping(); }); }, 30000); wss.on('close', function close() { clearInterval(interval); }); // --- توابع اتصال به گوگل --- /** * اتصال سوکت کلاینت و سوکت جمینای را به هم متصل می‌کند */ function attachGeminiEventHandlers(clientWs, geminiWs, apiKeyUsed) { // انتقال پیام از گوگل به کلاینت geminiWs.on('message', (data) => { if (clientWs.readyState === WebSocket.OPEN) { clientWs.send(data, { binary: true }); } }); geminiWs.on('error', (error) => { console.error(`🔴 خطای جمینای (کلید ...${apiKeyUsed.slice(-4)}):`, error.message); // در صورت خطا، سوکت کلاینت را نمی‌بندیم تا شاید بتواند دوباره تلاش کند، // اما معمولا کلاینت خودش قطع می‌شود. if (clientWs.readyState === WebSocket.OPEN) clientWs.close(); }); geminiWs.on('close', (code) => { if (clientWs.readyState === WebSocket.OPEN) clientWs.close(); }); geminiWs.on('ping', () => { try { geminiWs.pong(); } catch (e) {} }); } /** * تلاش برای اتصال به جمینای با انتخاب تصادفی و امتحان مجدد * @param {WebSocket} clientWs سوکت کاربر * @param {Object} setupData داده‌های اولیه (کانفیگ) * @param {Number} startIndex ایندکس شروع (که به صورت تصادفی انتخاب شده) * @param {Number} attemptCount شمارنده تلاش‌ها */ async function tryConnectToGemini(clientWs, setupData, startIndex, attemptCount = 0) { // اگر کلاینت در حین تلاش قطع شد، ادامه نده (جلوگیری از لیک حافظه) if (clientWs.readyState !== WebSocket.OPEN) return null; // اگر تمام کلیدها تست شدند و هیچکدام کار نکرد if (attemptCount >= apiKeys.length) { console.error(`⛔ تمام ${apiKeys.length} کلید API با شکست مواجه شدند.`); if (clientWs.readyState === WebSocket.OPEN) { clientWs.send(JSON.stringify({ error: "سرویس موقتاً در دسترس نیست (ظرفیت تکمیل)." })); clientWs.close(); } return null; } // منطق انتخاب کلید: // بار اول: ایندکس کاملا تصادفی (startIndex) // دفعات بعد (در صورت خطا): کلید بعدی در لیست به صورت چرخشی const keyIndexToTry = (startIndex + attemptCount) % apiKeys.length; const apiKey = apiKeys[keyIndexToTry]; const url = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=${apiKey}`; return new Promise((resolve) => { const geminiWs = new WebSocket(url); // تایم‌اوت سخت: اگر گوگل تا ۸ ثانیه هیچ پاسخی نداد (هنگ کرد)، برو کلید بعدی const timeout = setTimeout(() => { console.warn(`⏳ تایم‌اوت اتصال (کلید ...${apiKey.slice(-4)}). رفتن به کلید بعدی...`); geminiWs.terminate(); resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1)); }, 8000); geminiWs.on('open', () => { clearTimeout(timeout); if (clientWs.readyState !== WebSocket.OPEN) { geminiWs.close(); return resolve(null); } console.log(`🔗 اتصال موفق با کلید شماره ${keyIndexToTry} (تلاش ${attemptCount + 1})`); try { geminiWs.send(JSON.stringify(setupData)); attachGeminiEventHandlers(clientWs, geminiWs, apiKey); resolve(geminiWs); } catch (e) { console.error("خطا در ارسال تنظیمات اولیه:", e); geminiWs.close(); resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1)); } }); geminiWs.on('error', (err) => { clearTimeout(timeout); console.warn(`⚠️ خطای اتصال (کلید ...${apiKey.slice(-4)}): ${err.message || 'Unknown'}. تلاش با کلید بعدی...`); // فراخوانی بازگشتی برای تست کلید بعدی resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1)); }); }); } // --- سرو کردن فایل‌های استاتیک --- app.use(express.static(path.join(__dirname, '../build'))); // API Endpoint دستورالعمل‌ها app.get('/api/instructions', (req, res) => { res.setHeader('Cache-Control', 'no-store'); res.json(personalityInstructions); }); // --- مدیریت WebSocket کلاینت --- wss.on('connection', (ws, req) => { ws.isAlive = true; ws.on('pong', heartbeat); // *تغییر اصلی:* انتخاب یک نقطه شروع کاملاً تصادفی برای هر کاربر // این کار باعث می‌شود بار روی سرور پخش شود و نیاز به حافظه برای نگهداری نوبت نباشد. const randomStartIndex = Math.floor(Math.random() * apiKeys.length); let geminiWs = null; let isConnecting = false; ws.on('message', async (message) => { try { if (!Buffer.isBuffer(message) || message[0] === 123) { const msgStr = message.toString(); if (msgStr.startsWith('{')) { const data = JSON.parse(msgStr); if (data.setup) { if (isConnecting || geminiWs) return; isConnecting = true; // ارسال ایندکس تصادفی به تابع اتصال geminiWs = await tryConnectToGemini(ws, data, randomStartIndex); isConnecting = false; return; } } } if (geminiWs && geminiWs.readyState === WebSocket.OPEN) { geminiWs.send(message); } } catch (e) { console.error("خطا در پردازش پیام کلاینت:", e.message); } }); ws.on('close', () => { if (geminiWs) { // بستن اجباری اتصال گوگل برای آزادسازی منابع try { geminiWs.terminate(); } catch(e) {} geminiWs = null; } }); ws.on('error', (error) => { if (geminiWs) { try { geminiWs.terminate(); } catch(e) {} geminiWs = null; } }); }); // ارسال تمام درخواست‌های دیگر به React app.get('*', (req, res) => res.sendFile(path.join(__dirname, '../build', 'index.html'))); const PORT = process.env.PORT || 3001; server.listen(PORT, () => console.log(`🚀 سرور (حالت تصادفی) روی پورت ${PORT} اجرا شد.`));