Ezmary commited on
Commit
3ffbc23
·
verified ·
1 Parent(s): 28b5c3c

Update server/index.js

Browse files
Files changed (1) hide show
  1. server/index.js +220 -59
server/index.js CHANGED
@@ -2,116 +2,277 @@ const express = require('express');
2
  const path = require('node:path');
3
  const { WebSocketServer, WebSocket } = require('ws');
4
  const http = require('node:http');
5
- require('dotenv').config();
6
 
7
  const app = express();
8
  const server = http.createServer(app);
9
  const wss = new WebSocketServer({ server });
10
 
11
- // Serve static files from the React app build directory
12
- app.use(express.static(path.join(__dirname, '../build')));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- // Make API key available to the WebSocket server but not to the client
15
- const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
 
 
16
 
17
- if (!GEMINI_API_KEY) {
18
- console.error('GEMINI_API_KEY environment variable is not set!');
19
- process.exit(1);
20
  }
21
 
22
- // Create a WebSocket connection to Gemini for each client
23
- const createGeminiWebSocket = (clientWs) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  const geminiWs = new WebSocket(
25
- `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=${GEMINI_API_KEY}`
26
  );
27
 
28
- // Set up event handlers before connecting
29
  geminiWs.on('open', () => {
30
- console.log('Connected to Gemini API');
31
- // If there's a pending setup message, send it now
32
  if (geminiWs.pendingSetup) {
33
- console.log('Sending pending setup:', geminiWs.pendingSetup);
34
- geminiWs.send(JSON.stringify(geminiWs.pendingSetup));
35
- geminiWs.pendingSetup = null;
 
 
 
 
 
36
  }
37
  });
38
 
39
  geminiWs.on('message', (data) => {
40
  try {
41
- // Convert the message to a Blob before sending to client
42
  const message = data.toString();
43
- console.log('Received from Gemini:', message);
44
-
45
- // Create a Blob from the message
46
- const blob = Buffer.from(message);
47
- clientWs.send(blob, { binary: true });
 
 
 
 
 
 
 
 
48
  } catch (error) {
49
- console.error('Error handling Gemini message:', error);
50
  }
51
  });
52
 
53
  geminiWs.on('error', (error) => {
54
- console.error('Gemini WebSocket error:', error);
 
 
 
 
55
  });
56
 
57
  geminiWs.on('close', (code, reason) => {
58
- console.log('Gemini WebSocket closed:', code, reason.toString());
 
 
 
 
59
  });
60
 
61
  return geminiWs;
62
  };
63
 
64
  wss.on('connection', (ws) => {
65
- console.log('Client connected');
66
- let geminiWs = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  ws.on('message', async (message) => {
69
  try {
70
- const data = JSON.parse(message);
71
- console.log('Received from client:', data);
72
-
73
- // Initialize Gemini connection when receiving setup message
74
- if (data.setup) {
75
- console.log('Initializing Gemini connection with config:', data.setup);
76
- geminiWs = createGeminiWebSocket(ws);
77
-
78
- // Store setup message to send once connection is established
79
- if (geminiWs.readyState !== WebSocket.OPEN) {
80
- geminiWs.pendingSetup = data;
81
  } else {
82
- geminiWs.send(JSON.stringify(data));
83
  }
84
- return;
85
- }
86
 
87
- // Forward message to Gemini if connection exists
88
- if (geminiWs && geminiWs.readyState === WebSocket.OPEN) {
89
- console.log('Forwarding to Gemini:', data);
90
- geminiWs.send(JSON.stringify(data));
91
- } else if (geminiWs) {
92
- console.log('Waiting for Gemini connection to be ready...');
93
- } else {
94
- console.error('No Gemini connection established');
95
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  } catch (error) {
97
- console.error('Error processing message:', error);
 
 
98
  }
99
  });
100
 
101
  ws.on('close', () => {
102
- console.log('Client disconnected');
103
- if (geminiWs) {
 
104
  geminiWs.close();
105
  }
106
  });
 
 
 
 
 
 
 
 
 
 
107
  });
108
 
109
- // Handle any remaining requests by returning the React app
110
  app.get('*', (req, res) => {
111
- res.sendFile(path.join(__dirname, '../build', 'index.html'));
 
 
 
 
 
 
 
 
 
 
112
  });
113
 
114
- const PORT = process.env.PORT || 3001;
 
115
  server.listen(PORT, () => {
116
- console.log(`Server is running on port ${PORT}`);
117
- });
 
 
2
  const path = require('node:path');
3
  const { WebSocketServer, WebSocket } = require('ws');
4
  const http = require('node:http');
5
+ require('dotenv').config(); // برای تست محلی ممکن است نیاز باشد
6
 
7
  const app = express();
8
  const server = http.createServer(app);
9
  const wss = new WebSocketServer({ server });
10
 
11
+ // --- START: منطق جدید برای چرخش API Key ---
12
+
13
+ // ۱. خواندن تمام کلیدهای API از Secrets (متغیرهای محیطی در هاگینگ فیس)
14
+ const apiKeys = [];
15
+ let i = 1;
16
+ while (process.env[`GEMINI_API_KEY_${i}`]) {
17
+ apiKeys.push(process.env[`GEMINI_API_KEY_${i}`]);
18
+ i++;
19
+ }
20
+
21
+ const numKeys = apiKeys.length;
22
+
23
+ if (numKeys === 0) {
24
+ // اگر هیچ کلیدی پیدا نشد، خطا داده و خارج شو
25
+ console.error(
26
+ 'خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد!' +
27
+ ' لطفاً Secret ها را مانند GEMINI_API_KEY_1, GEMINI_API_KEY_2, ... ' +
28
+ 'در تنظیمات Space خود اضافه کنید.'
29
+ );
30
+ process.exit(1); // برنامه را متوقف کن
31
+ } else {
32
+ console.log(`تعداد ${numKeys} کلید API جیمینای بارگذاری شد.`);
33
+ }
34
+
35
+ // ۲. شمارنده سراسری برای انتخاب کلید بعدی (شروع از اندیس ۰)
36
+ let currentKeyIndex = 0;
37
+
38
+ // ۳. تابع برای گرفتن کلید بعدی به صورت چرخشی
39
+ function getNextApiKey() {
40
+ if (numKeys === 0) {
41
+ console.error('تلاش برای گرفتن کلید API در حالی که هیچ کلیدی بارگذاری نشده است.');
42
+ return null; // یا یک خطا پرتاب کنید
43
+ }
44
+ // محاسبه اندیس کلید با استفاده از باقیمانده تقسیم
45
+ const keyIndexToUse = currentKeyIndex % numKeys;
46
+ const selectedKey = apiKeys[keyIndexToUse];
47
+ console.log(`اختصاص کلید API با اندیس: ${keyIndexToUse}`); // لاگ برای اشکال‌زدایی
48
+
49
+ // افزایش شمارنده برای درخواست بعدی
50
+ currentKeyIndex++;
51
 
52
+ // اختیاری: جلوگیری از خیلی بزرگ شدن شمارنده (گرچه جاوااسکریپت اعداد بزرگ را مدیریت می‌کند)
53
+ // if (currentKeyIndex >= Number.MAX_SAFE_INTEGER - 10) { // نزدیک به حداکثر عدد امن
54
+ // currentKeyIndex = currentKeyIndex % numKeys;
55
+ // }
56
 
57
+ return selectedKey;
 
 
58
  }
59
 
60
+ // --- END: منطق جدید برای چرخش API Key ---
61
+
62
+ // سرو کردن فایل‌های استاتیک از پوشه بیلد React
63
+ app.use(express.static(path.join(__dirname, '../build')));
64
+
65
+ // این تابع حالا کلید API انتخاب شده را به عنوان ورودی می‌گیرد
66
+ const createGeminiWebSocket = (clientWs, apiKey) => {
67
+ // بررسی اینکه آیا کلید معتبری داده شده است
68
+ if (!apiKey) {
69
+ console.error('امکان ایجاد WebSocket جیمینای وجود ندارد: کلید API ارائه نشده است.');
70
+ // می‌توانید یک پیام خطا به کلاینت بفرستید و اتصالش را ببندید
71
+ if (clientWs.readyState === WebSocket.OPEN) {
72
+ try {
73
+ clientWs.send(JSON.stringify({ error: "خطای داخلی سرور: عدم امکان دریافت کلید API." }));
74
+ } catch (sendError) {
75
+ console.error("خطا در ارسال پیام خطا به کلاینت:", sendError);
76
+ }
77
+ clientWs.close();
78
+ }
79
+ return null; // برگرداندن null نشان‌دهنده شکست است
80
+ }
81
+
82
+ console.log(`ایجاد اتصال به جیمینای با کلید: ...${apiKey.slice(-4)}`); // نمایش ۴ کاراکتر آخر کلید برای تایید
83
+
84
+ // استفاده از کلید API اختصاص‌یافته به این کاربر
85
  const geminiWs = new WebSocket(
86
+ `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=${apiKey}`
87
  );
88
 
89
+ // --- بقیه event handler های geminiWs (open, message, error, close) بدون تغییر باقی می‌مانند ---
90
  geminiWs.on('open', () => {
91
+ console.log(`ا��صال به جیمینای برای کلاینت با کلید ...${apiKey.slice(-4)} برقرار شد.`);
92
+ // ارسال پیام setup معلق اگر وجود دارد
93
  if (geminiWs.pendingSetup) {
94
+ console.log('ارسال پیام setup معلق:', geminiWs.pendingSetup);
95
+ try {
96
+ geminiWs.send(JSON.stringify(geminiWs.pendingSetup));
97
+ } catch (sendError) {
98
+ console.error("خطا در ارسال پیام setup معلق به جیمینای:", sendError);
99
+ clientWs.close(); // بستن اتصال کلاینت اگر نتوانستیم با جیمینای ارتباط اولیه برقرار کنیم
100
+ }
101
+ geminiWs.pendingSetup = null; // پاک کردن پیام معلق
102
  }
103
  });
104
 
105
  geminiWs.on('message', (data) => {
106
  try {
107
+ // تبدیل پیام به رشته قبل از ارسال به کلاینت (کلاینت شما انتظار Blob داشت، مطمئن شوید هنوز همینطور است)
108
  const message = data.toString();
109
+ console.log(`دریافت از جیمینای (کلید ...${apiKey.slice(-4)}):`, message); // نمایش ۴ کاراکتر آخر کلید در لاگ
110
+
111
+ // کلاینت شما انتظار Blob داشت. اگر هنوز نیاز به Blob دارید، آن را حفظ کنید:
112
+ const blob = Buffer.from(message); // ایجاد Buffer (شبیه Blob در Node.js)
113
+ if (clientWs.readyState === WebSocket.OPEN) {
114
+ clientWs.send(blob, { binary: true }); // ارسال به عنوان داده باینری
115
+ }
116
+
117
+ // اگر کلاینت شما حالا انتظار JSON یا رشته دارد، کد بالا را تغییر دهید:
118
+ // if (clientWs.readyState === WebSocket.OPEN) {
119
+ // clientWs.send(message); // ارسال به عنوان رشته
120
+ // }
121
+
122
  } catch (error) {
123
+ console.error(`خطا در پردازش پیام جیمینای (کلید ...${apiKey.slice(-4)}):`, error);
124
  }
125
  });
126
 
127
  geminiWs.on('error', (error) => {
128
+ console.error(`خطای WebSocket جیمینای (کلید ...${apiKey.slice(-4)}):`, error);
129
+ // بستن اتصال کلاینت مرتبط در صورت خطای جیمینای
130
+ if (clientWs.readyState === WebSocket.OPEN) {
131
+ clientWs.close();
132
+ }
133
  });
134
 
135
  geminiWs.on('close', (code, reason) => {
136
+ console.log(`اتصال WebSocket جیمینای بسته شد (کلید ...${apiKey.slice(-4)}):`, code, reason.toString());
137
+ // بستن اتصال کلاینت مرتبط وقتی اتصال جیمینای بسته می‌شود
138
+ if (clientWs.readyState === WebSocket.OPEN) {
139
+ clientWs.close();
140
+ }
141
  });
142
 
143
  return geminiWs;
144
  };
145
 
146
  wss.on('connection', (ws) => {
147
+ console.log('یک کلاینت جدید متصل شد.');
148
+
149
+ // --- اختصاص کلید API به محض اتصال کلاینت ---
150
+ const assignedApiKey = getNextApiKey(); // گرفتن کلید بعدی برای *این* کلاینت
151
+
152
+ // اگر به دلایلی کلید دریافت نشد (نباید اتفاق بیفتد اگر چک اولیه انجام شده)
153
+ if (!assignedApiKey) {
154
+ console.error("خطا: عدم موفقیت در اختصاص کلید API به کلاینت جدید. بستن اتصال.");
155
+ ws.close();
156
+ return; // ادامه نده برای این کلاینت
157
+ }
158
+ // لاگ کردن کلید اختصاص یافته (فقط ۴ کاراکتر آخر برای امنیت)
159
+ console.log(`کلید API اختصاص یافته به کلاینت: ...${assignedApiKey.slice(-4)}`);
160
+
161
+ let geminiWs = null; // اتصال جیمینای مخصوص *این* کلاینت
162
 
163
  ws.on('message', async (message) => {
164
  try {
165
+ // بررسی کنیم پیام باینری است یا متنی
166
+ let data;
167
+ if (message instanceof Buffer) {
168
+ // اگر کلاینت پیا�� باینری می‌فرستد، آن را به رشته تبدیل کنید (فرض بر JSON بودن)
169
+ data = JSON.parse(message.toString());
 
 
 
 
 
 
170
  } else {
171
+ data = JSON.parse(message); // اگر پیام متنی است
172
  }
 
 
173
 
174
+ console.log('دریافت از کلاینت:', data);
175
+
176
+ // مقداردهی اولیه اتصال جیمینای برای این کلاینت با استفاده از کلید اختصاص یافته
177
+ if (data.setup) {
178
+ // جلوگیری از مقداردهی مجدد اگر کلاینت دوباره پیام setup فرستاد
179
+ if (geminiWs) {
180
+ console.warn("کلاینت دوباره پیام setup ارسال کرد. نادیده گرفته شد.");
181
+ return;
182
+ }
183
+ console.log('مقداردهی اولیه اتصال جیمینای با تنظیمات:', data.setup);
184
+ // ارسال کلید اختصاص یافته این کلاینت به تابع ایجاد اتصال
185
+ geminiWs = createGeminiWebSocket(ws, assignedApiKey);
186
+
187
+ // ذخیره پیام setup برای ارسال پس از برقراری اتصال (اگر اتصال هنوز آماده نیست)
188
+ if (geminiWs && geminiWs.readyState !== WebSocket.OPEN) {
189
+ // مهم: پیام معلق را به نمونه WebSocket *این* کلاینت متصل کنید
190
+ geminiWs.pendingSetup = data;
191
+ } else if (geminiWs) {
192
+ // اگر اتصال بلافاصله برقرار شد، پیام setup را بفرست
193
+ try {
194
+ geminiWs.send(JSON.stringify(data));
195
+ } catch (sendError) {
196
+ console.error("خطا در ارسال پیام setup اولیه به جیمینای:", sendError);
197
+ ws.close(); // بستن اتصال کلاینت
198
+ }
199
+ } else {
200
+ // اگر createGeminiWebSocket ناموفق بود (مثلا به خاطر خطای کلید در بالا)
201
+ console.error("ایجاد اتصال WebSocket جیمینای پس از پیام setup ناموفق بود.");
202
+ // ws قبلا باید بسته شده باشد توسط createGeminiWebSocket
203
+ }
204
+ return; // پردازش پیام setup تمام شد
205
+ }
206
+
207
+ // ارسال پیام به جیمینای اگر اتصال برای این کلاینت وجود دارد و باز است
208
+ if (geminiWs && geminiWs.readyState === WebSocket.OPEN) {
209
+ console.log('ارسال به جیمینای:', data);
210
+ try {
211
+ geminiWs.send(JSON.stringify(data));
212
+ } catch (sendError) {
213
+ console.error("خطا در ارسال پیام به جیمینای:", sendError);
214
+ ws.close(); // بستن اتصال کلاینت
215
+ }
216
+ } else if (geminiWs) {
217
+ // اتصال جیمینای هنوز برقرار نشده یا بسته شده
218
+ console.log('اتصال جیمینای آماده نیست، امکان ارسال پیام وجود ندارد.');
219
+ // TODO: می‌توانید پیام‌ها را در صف قرار دهید یا خطا به کلاینت برگردانید
220
+ } else {
221
+ // هنوز پیام setup دریافت نشده یا اتصال ناموفق بوده
222
+ console.error('امکان ارسال پیام وجود ندارد: اتصال جیمینای برای این کلاینت برقرار نشده است.');
223
+ // می‌توانید خطا به کلاینت برگردانید
224
+ if (ws.readyState === WebSocket.OPEN) {
225
+ try {
226
+ ws.send(JSON.stringify({ error: "ارتباط با سرویس برقرار نیست. لطفاً دوباره تلاش کنید." }));
227
+ } catch (sendError) {
228
+ console.error("خطا در ارسال پیام خطا به کلاینت:", sendError);
229
+ }
230
+ }
231
+ }
232
  } catch (error) {
233
+ // خطای کلی در پردازش پیام (مثلا JSON نامعتبر)
234
+ console.error('خطا در پردازش پیام کلاینت:', error);
235
+ // از کرش کردن سرور جلوگیری کنید
236
  }
237
  });
238
 
239
  ws.on('close', () => {
240
+ console.log(`کلاینت با کلید ...${assignedApiKey ? assignedApiKey.slice(-4) : 'N/A'} قطع شد.`);
241
+ // بستن اتصال جیمینای مربوط به این کلاینت، اگر باز است
242
+ if (geminiWs && geminiWs.readyState !== WebSocket.CLOSED && geminiWs.readyState !== WebSocket.CLOSING) {
243
  geminiWs.close();
244
  }
245
  });
246
+
247
+ ws.on('error', (error) => {
248
+ console.error(`خطای WebSocket کلاینت (کلید ...${assignedApiKey ? assignedApiKey.slice(-4) : 'N/A'}):`, error);
249
+ // بستن اتصال جیمینای مربوطه در صورت خطای کلاینت
250
+ if (geminiWs && geminiWs.readyState !== WebSocket.CLOSED && geminiWs.readyState !== WebSocket.CLOSING) {
251
+ geminiWs.close();
252
+ }
253
+ // ws احتمالا به طور خودکار بسته می‌شود یا در شرف بسته شدن است
254
+ });
255
+
256
  });
257
 
258
+ // رسیدگی به درخواست‌های باقیمانده با برگرداندن اپ React
259
  app.get('*', (req, res) => {
260
+ // اطمینان حاصل کنید که مسیر درست است
261
+ const indexPath = path.join(__dirname, '../build', 'index.html');
262
+ res.sendFile(indexPath, (err) => {
263
+ if (err) {
264
+ console.error("خطا در ارسال فایل index.html:", err);
265
+ // ارسال یک پاسخ خطای ساده اگر فایل پیدا نشد
266
+ if (!res.headersSent) {
267
+ res.status(err.status || 500).send('خطا در بارگذاری برنامه');
268
+ }
269
+ }
270
+ });
271
  });
272
 
273
+
274
+ const PORT = process.env.PORT || 3001; // در هاگینگ فیس معمولا پورت 7860 استفاده می‌شود، اما اینجا 3001 است
275
  server.listen(PORT, () => {
276
+ console.log(`سرور در حال اجرا روی پورت ${PORT}`);
277
+ console.log(`لطفاً ${numKeys} کلید API با نام‌های GEMINI_API_KEY_1 تا GEMINI_API_KEY_${numKeys} را در Secrets تنظیم کرده باشید.`);
278
+ });