Spaces:
Running
Running
File size: 14,396 Bytes
7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 7f2a14a 3ffbc23 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
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 تنظیم کرده باشید.`);
}); |