Spaces:
Running
Running
| 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 تنظیم کرده باشید.`); | |
| }); |