Ezmary's picture
Update server/index.js
3ffbc23 verified
raw
history blame
14.4 kB
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 تنظیم کرده باشید.`);
});