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(app); const wss = new WebSocketServer({ server }); // --- START: منطق جدید برای چرخش API Key --- // ۱. خواندن تمام کلیدهای API از Secrets (متغیرهای محیطی در هاگینگ فیس) const apiKeys = []; let i = 1; while (process.env[`GEMINI_API_KEY_${i}`]) { apiKeys.push(process.env[`GEMINI_API_KEY_${i}`]); i++; } const numKeys = apiKeys.length; if (numKeys === 0) { // اگر هیچ کلیدی پیدا نشد، خطا داده و خارج شو console.error( 'خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد!' + ' لطفاً Secret ها را مانند GEMINI_API_KEY_1, GEMINI_API_KEY_2, ... ' + 'در تنظیمات Space خود اضافه کنید.' ); process.exit(1); // برنامه را متوقف کن } else { console.log(`تعداد ${numKeys} کلید API جیمینای بارگذاری شد.`); } // ۲. شمارنده سراسری برای انتخاب کلید بعدی (شروع از اندیس ۰) let currentKeyIndex = 0; // ۳. تابع برای گرفتن کلید بعدی به صورت چرخشی function getNextApiKey() { if (numKeys === 0) { console.error('تلاش برای گرفتن کلید API در حالی که هیچ کلیدی بارگذاری نشده است.'); return null; // یا یک خطا پرتاب کنید } // محاسبه اندیس کلید با استفاده از باقیمانده تقسیم const keyIndexToUse = currentKeyIndex % numKeys; const selectedKey = apiKeys[keyIndexToUse]; console.log(`اختصاص کلید API با اندیس: ${keyIndexToUse}`); // لاگ برای اشکال‌زدایی // افزایش شمارنده برای درخواست بعدی currentKeyIndex++; // اختیاری: جلوگیری از خیلی بزرگ شدن شمارنده (گرچه جاوااسکریپت اعداد بزرگ را مدیریت می‌کند) // if (currentKeyIndex >= Number.MAX_SAFE_INTEGER - 10) { // نزدیک به حداکثر عدد امن // currentKeyIndex = currentKeyIndex % numKeys; // } return selectedKey; } // --- END: منطق جدید برای چرخش API Key --- // سرو کردن فایل‌های استاتیک از پوشه بیلد React app.use(express.static(path.join(__dirname, '../build'))); // این تابع حالا کلید API انتخاب شده را به عنوان ورودی می‌گیرد const createGeminiWebSocket = (clientWs, apiKey) => { // بررسی اینکه آیا کلید معتبری داده شده است if (!apiKey) { console.error('امکان ایجاد WebSocket جیمینای وجود ندارد: کلید API ارائه نشده است.'); // می‌توانید یک پیام خطا به کلاینت بفرستید و اتصالش را ببندید if (clientWs.readyState === WebSocket.OPEN) { try { clientWs.send(JSON.stringify({ error: "خطای داخلی سرور: عدم امکان دریافت کلید API." })); } catch (sendError) { console.error("خطا در ارسال پیام خطا به کلاینت:", sendError); } clientWs.close(); } return null; // برگرداندن null نشان‌دهنده شکست است } console.log(`ایجاد اتصال به جیمینای با کلید: ...${apiKey.slice(-4)}`); // نمایش ۴ کاراکتر آخر کلید برای تایید // استفاده از کلید API اختصاص‌یافته به این کاربر const geminiWs = new WebSocket( `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=${apiKey}` ); // --- بقیه event handler های geminiWs (open, message, error, close) بدون تغییر باقی می‌مانند --- geminiWs.on('open', () => { console.log(`اتصال به جیمینای برای کلاینت با کلید ...${apiKey.slice(-4)} برقرار شد.`); // ارسال پیام setup معلق اگر وجود دارد if (geminiWs.pendingSetup) { console.log('ارسال پیام setup معلق:', geminiWs.pendingSetup); try { geminiWs.send(JSON.stringify(geminiWs.pendingSetup)); } catch (sendError) { console.error("خطا در ارسال پیام setup معلق به جیمینای:", sendError); clientWs.close(); // بستن اتصال کلاینت اگر نتوانستیم با جیمینای ارتباط اولیه برقرار کنیم } geminiWs.pendingSetup = null; // پاک کردن پیام معلق } }); geminiWs.on('message', (data) => { try { // تبدیل پیام به رشته قبل از ارسال به کلاینت (کلاینت شما انتظار Blob داشت، مطمئن شوید هنوز همینطور است) const message = data.toString(); console.log(`دریافت از جیمینای (کلید ...${apiKey.slice(-4)}):`, message); // نمایش ۴ کاراکتر آخر کلید در لاگ // کلاینت شما انتظار Blob داشت. اگر هنوز نیاز به Blob دارید، آن را حفظ کنید: const blob = Buffer.from(message); // ایجاد Buffer (شبیه Blob در Node.js) if (clientWs.readyState === WebSocket.OPEN) { clientWs.send(blob, { binary: true }); // ارسال به عنوان داده باینری } // اگر کلاینت شما حالا انتظار JSON یا رشته دارد، کد بالا را تغییر دهید: // if (clientWs.readyState === WebSocket.OPEN) { // clientWs.send(message); // ارسال به عنوان رشته // } } catch (error) { console.error(`خطا در پردازش پیام جیمینای (کلید ...${apiKey.slice(-4)}):`, error); } }); geminiWs.on('error', (error) => { console.error(`خطای WebSocket جیمینای (کلید ...${apiKey.slice(-4)}):`, error); // بستن اتصال کلاینت مرتبط در صورت خطای جیمینای if (clientWs.readyState === WebSocket.OPEN) { clientWs.close(); } }); geminiWs.on('close', (code, reason) => { console.log(`اتصال WebSocket جیمینای بسته شد (کلید ...${apiKey.slice(-4)}):`, code, reason.toString()); // بستن اتصال کلاینت مرتبط وقتی اتصال جیمینای بسته می‌شود if (clientWs.readyState === WebSocket.OPEN) { clientWs.close(); } }); return geminiWs; }; wss.on('connection', (ws) => { console.log('یک کلاینت جدید متصل شد.'); // --- اختصاص کلید API به محض اتصال کلاینت --- const assignedApiKey = getNextApiKey(); // گرفتن کلید بعدی برای *این* کلاینت // اگر به دلایلی کلید دریافت نشد (نباید اتفاق بیفتد اگر چک اولیه انجام شده) if (!assignedApiKey) { console.error("خطا: عدم موفقیت در اختصاص کلید API به کلاینت جدید. بستن اتصال."); ws.close(); return; // ادامه نده برای این کلاینت } // لاگ کردن کلید اختصاص یافته (فقط ۴ کاراکتر آخر برای امنیت) console.log(`کلید API اختصاص یافته به کلاینت: ...${assignedApiKey.slice(-4)}`); let geminiWs = null; // اتصال جیمینای مخصوص *این* کلاینت ws.on('message', async (message) => { try { // بررسی کنیم پیام باینری است یا متنی let data; if (message instanceof Buffer) { // اگر کلاینت پیام باینری می‌فرستد، آن را به رشته تبدیل کنید (فرض بر JSON بودن) data = JSON.parse(message.toString()); } else { data = JSON.parse(message); // اگر پیام متنی است } console.log('دریافت از کلاینت:', data); // مقداردهی اولیه اتصال جیمینای برای این کلاینت با استفاده از کلید اختصاص یافته if (data.setup) { // جلوگیری از مقداردهی مجدد اگر کلاینت دوباره پیام setup فرستاد if (geminiWs) { console.warn("کلاینت دوباره پیام setup ارسال کرد. نادیده گرفته شد."); return; } console.log('مقداردهی اولیه اتصال جیمینای با تنظیمات:', data.setup); // ارسال کلید اختصاص یافته این کلاینت به تابع ایجاد اتصال geminiWs = createGeminiWebSocket(ws, assignedApiKey); // ذخیره پیام setup برای ارسال پس از برقراری اتصال (اگر اتصال هنوز آماده نیست) if (geminiWs && geminiWs.readyState !== WebSocket.OPEN) { // مهم: پیام معلق را به نمونه WebSocket *این* کلاینت متصل کنید geminiWs.pendingSetup = data; } else if (geminiWs) { // اگر اتصال بلافاصله برقرار شد، پیام setup را بفرست try { geminiWs.send(JSON.stringify(data)); } catch (sendError) { console.error("خطا در ارسال پیام setup اولیه به جیمینای:", sendError); ws.close(); // بستن اتصال کلاینت } } else { // اگر createGeminiWebSocket ناموفق بود (مثلا به خاطر خطای کلید در بالا) console.error("ایجاد اتصال WebSocket جیمینای پس از پیام setup ناموفق بود."); // ws قبلا باید بسته شده باشد توسط createGeminiWebSocket } return; // پردازش پیام setup تمام شد } // ارسال پیام به جیمینای اگر اتصال برای این کلاینت وجود دارد و باز است if (geminiWs && geminiWs.readyState === WebSocket.OPEN) { console.log('ارسال به جیمینای:', data); try { geminiWs.send(JSON.stringify(data)); } catch (sendError) { console.error("خطا در ارسال پیام به جیمینای:", sendError); ws.close(); // بستن اتصال کلاینت } } else if (geminiWs) { // اتصال جیمینای هنوز برقرار نشده یا بسته شده console.log('اتصال جیمینای آماده نیست، امکان ارسال پیام وجود ندارد.'); // TODO: می‌توانید پیام‌ها را در صف قرار دهید یا خطا به کلاینت برگردانید } else { // هنوز پیام setup دریافت نشده یا اتصال ناموفق بوده console.error('امکان ارسال پیام وجود ندارد: اتصال جیمینای برای این کلاینت برقرار نشده است.'); // می‌توانید خطا به کلاینت برگردانید if (ws.readyState === WebSocket.OPEN) { try { ws.send(JSON.stringify({ error: "ارتباط با سرویس برقرار نیست. لطفاً دوباره تلاش کنید." })); } catch (sendError) { console.error("خطا در ارسال پیام خطا به کلاینت:", sendError); } } } } catch (error) { // خطای کلی در پردازش پیام (مثلا JSON نامعتبر) console.error('خطا در پردازش پیام کلاینت:', error); // از کرش کردن سرور جلوگیری کنید } }); ws.on('close', () => { console.log(`کلاینت با کلید ...${assignedApiKey ? assignedApiKey.slice(-4) : 'N/A'} قطع شد.`); // بستن اتصال جیمینای مربوط به این کلاینت، اگر باز است if (geminiWs && geminiWs.readyState !== WebSocket.CLOSED && geminiWs.readyState !== WebSocket.CLOSING) { geminiWs.close(); } }); ws.on('error', (error) => { console.error(`خطای WebSocket کلاینت (کلید ...${assignedApiKey ? assignedApiKey.slice(-4) : 'N/A'}):`, error); // بستن اتصال جیمینای مربوطه در صورت خطای کلاینت if (geminiWs && geminiWs.readyState !== WebSocket.CLOSED && geminiWs.readyState !== WebSocket.CLOSING) { geminiWs.close(); } // ws احتمالا به طور خودکار بسته می‌شود یا در شرف بسته شدن است }); }); // رسیدگی به درخواست‌های باقیمانده با برگرداندن اپ React app.get('*', (req, res) => { // اطمینان حاصل کنید که مسیر درست است const indexPath = path.join(__dirname, '../build', 'index.html'); res.sendFile(indexPath, (err) => { if (err) { console.error("خطا در ارسال فایل index.html:", err); // ارسال یک پاسخ خطای ساده اگر فایل پیدا نشد if (!res.headersSent) { res.status(err.status || 500).send('خطا در بارگذاری برنامه'); } } }); }); const PORT = process.env.PORT || 3001; // در هاگینگ فیس معمولا پورت 7860 استفاده می‌شود، اما اینجا 3001 است server.listen(PORT, () => { console.log(`سرور در حال اجرا روی پورت ${PORT}`); console.log(`لطفاً ${numKeys} کلید API با نام‌های GEMINI_API_KEY_1 تا GEMINI_API_KEY_${numKeys} را در Secrets تنظیم کرده باشید.`); });