Hamed744 commited on
Commit
5058361
·
verified ·
1 Parent(s): 66d48c7

Update server/index.js

Browse files
Files changed (1) hide show
  1. server/index.js +55 -53
server/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // server/index.js
2
  const express = require('express');
3
  const path = require('node:path');
4
  const { WebSocketServer, WebSocket } = require('ws');
@@ -6,14 +6,16 @@ const http = require('node:http');
6
  require('dotenv').config();
7
 
8
  const app = express();
9
- // افزایش محدودیت‌های سرور برای هندل کردن تعداد بالای درخواست
 
10
  const server = http.createServer({
11
  maxHeaderSize: 16384 // 16KB
12
  }, app);
13
 
14
  const wss = new WebSocketServer({
15
  server,
16
- clientTracking: true // برای مدیریت هارت‌بیت ضروری است
 
17
  });
18
 
19
  // --- بخش مدیریت تنظیمات شخصیت‌ها ---
@@ -31,33 +33,24 @@ Object.keys(instructionSecretNames).forEach(key => {
31
  const instruction = process.env[secretName];
32
  if (instruction) {
33
  personalityInstructions[key] = instruction;
34
- console.log(`✅ دستورالعمل '${key}' با موفقیت خوانده شد.`);
35
  } else {
36
  personalityInstructions[key] = `دستورالعمل '${key}' یافت نشد.`;
37
- console.warn(`⚠️ هشدار: Secret با نام '${secretName}' تنظیم نشده است.`);
38
  }
39
  });
40
 
41
- // --- بخش مدیریت کلیدهای API (Load Balancing) ---
42
  const apiKeysEnv = process.env.ALL_GEMINI_API_KEYS;
 
43
  const apiKeys = apiKeysEnv ? apiKeysEnv.split(',').map(key => key.trim()).filter(key => key) : [];
44
 
45
  if (apiKeys.length === 0) {
46
- console.error('🔴 خطای حیاتی: هیچ کلید API در متغیر ALL_GEMINI_API_KEYS یافت نشد!');
47
  process.exit(1);
48
  }
49
- console.log(`🚀 سرور با ${apiKeys.length} کلید API آماده‌سازی شد.`);
50
-
51
- let currentKeyIndex = 0;
52
- const getNextKeyIndex = () => {
53
- // چرخش ساده بین کلیدها (Round Robin)
54
- const index = currentKeyIndex;
55
- currentKeyIndex = (currentKeyIndex + 1) % apiKeys.length;
56
- return index;
57
- };
58
 
59
- // --- مکانیزم Heartbeat برای جلوگیری از قطعی سرور ---
60
- // این تابع اتصالات مرده را شناسایی و حذف می‌کند
61
  function heartbeat() {
62
  this.isAlive = true;
63
  }
@@ -65,14 +58,14 @@ function heartbeat() {
65
  const interval = setInterval(function ping() {
66
  wss.clients.forEach(function each(ws) {
67
  if (ws.isAlive === false) {
68
- console.log('💀 قطع اتصال مرده (Zombie Connection) برای آزادسازی حافظه.');
69
  return ws.terminate();
70
  }
71
 
72
  ws.isAlive = false;
73
  ws.ping();
74
  });
75
- }, 30000); // چک کردن هر ۳۰ ثانیه
76
 
77
  wss.on('close', function close() {
78
  clearInterval(interval);
@@ -81,7 +74,7 @@ wss.on('close', function close() {
81
  // --- توابع اتصال به گوگل ---
82
 
83
  /**
84
- * اتصال کلاینت و جمینای را به هم متصل می‌کند
85
  */
86
  function attachGeminiEventHandlers(clientWs, geminiWs, apiKeyUsed) {
87
  // انتقال پیام از گوگل به کلاینت
@@ -93,27 +86,32 @@ function attachGeminiEventHandlers(clientWs, geminiWs, apiKeyUsed) {
93
 
94
  geminiWs.on('error', (error) => {
95
  console.error(`🔴 خطای جمینای (کلید ...${apiKeyUsed.slice(-4)}):`, error.message);
 
 
96
  if (clientWs.readyState === WebSocket.OPEN) clientWs.close();
97
  });
98
 
99
  geminiWs.on('close', (code) => {
100
- if (code !== 1000 && code !== 1005) {
101
- console.log(`🟡 گوگل اتصال را بست (کلید ...${apiKeyUsed.slice(-4)}). کد: ${code}`);
102
- }
103
  if (clientWs.readyState === WebSocket.OPEN) clientWs.close();
104
  });
105
 
106
- // هن��ل کردن پونگ برای زنده نگه داشتن اتصال گوگل (اختیاری ولی مفید)
107
- geminiWs.on('ping', () => geminiWs.pong());
 
108
  }
109
 
110
  /**
111
- * تلاش برای اتصال به جمینای با قابلیت امتحان مجدد (Retry Logic)
 
 
 
 
112
  */
113
  async function tryConnectToGemini(clientWs, setupData, startIndex, attemptCount = 0) {
114
- // اگر کلاینت در حین تلاش قطع شد، ادامه نده
115
  if (clientWs.readyState !== WebSocket.OPEN) return null;
116
 
 
117
  if (attemptCount >= apiKeys.length) {
118
  console.error(`⛔ تمام ${apiKeys.length} کلید API با شکست مواجه شدند.`);
119
  if (clientWs.readyState === WebSocket.OPEN) {
@@ -123,29 +121,33 @@ async function tryConnectToGemini(clientWs, setupData, startIndex, attemptCount
123
  return null;
124
  }
125
 
 
 
 
126
  const keyIndexToTry = (startIndex + attemptCount) % apiKeys.length;
127
  const apiKey = apiKeys[keyIndexToTry];
 
128
  const url = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=${apiKey}`;
129
 
130
  return new Promise((resolve) => {
131
  const geminiWs = new WebSocket(url);
132
 
133
- // تایم‌اوت اتصال (اگر گوگل تا ۸ ثانیه جواب نداد، برو کلید بعدی)
134
  const timeout = setTimeout(() => {
135
- console.warn(`⏳ تایم‌اوت اتصال (کلید ...${apiKey.slice(-4)}). تلاش مجدد...`);
136
- geminiWs.terminate(); // کشتن اتصال گیر کرده
137
  resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
138
  }, 8000);
139
 
140
  geminiWs.on('open', () => {
141
  clearTimeout(timeout);
142
- // بررسی مجدد وضعیت کلاینت قبل از ارسال
143
  if (clientWs.readyState !== WebSocket.OPEN) {
144
  geminiWs.close();
145
  return resolve(null);
146
  }
147
 
148
- console.log(`🔗 متصل شد به گوگل با کلید اندیس ${keyIndexToTry}`);
149
 
150
  try {
151
  geminiWs.send(JSON.stringify(setupData));
@@ -160,8 +162,8 @@ async function tryConnectToGemini(clientWs, setupData, startIndex, attemptCount
160
 
161
  geminiWs.on('error', (err) => {
162
  clearTimeout(timeout);
163
- // فقط لاگ کن و برو بعدی، هندلرهای اصلی هنوز ست نشدن
164
- console.warn(`⚠️ خطای اتصال (کلید ...${apiKey.slice(-4)}): ${err.message || 'Unknown'}`);
165
  resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
166
  });
167
  });
@@ -172,40 +174,39 @@ app.use(express.static(path.join(__dirname, '../build')));
172
 
173
  // API Endpoint دستورالعمل‌ها
174
  app.get('/api/instructions', (req, res) => {
175
- res.setHeader('Cache-Control', 'no-store'); // جلوگیری از کش شدن قدیمی
176
  res.json(personalityInstructions);
177
  });
178
 
179
  // --- مدیریت WebSocket کلاینت ---
180
  wss.on('connection', (ws, req) => {
181
- ws.isAlive = true; // نشان‌گذاری به عنوان زنده
182
- ws.on('pong', heartbeat); // پاسخ به پینگ سرور
183
 
184
- const startingKeyIndex = getNextKeyIndex();
185
- // const clientIp = req.socket.remoteAddress; // برای لاگ کردن IP در صورت نیاز
 
186
 
187
  let geminiWs = null;
188
  let isConnecting = false;
189
 
190
  ws.on('message', async (message) => {
191
  try {
192
- // اگر پیام متنی است (JSON)
193
  if (!Buffer.isBuffer(message) || message[0] === 123) {
194
  const msgStr = message.toString();
195
- // چک کردن ساده برای تشخیص JSON
196
  if (msgStr.startsWith('{')) {
197
  const data = JSON.parse(msgStr);
198
  if (data.setup) {
199
- if (isConnecting || geminiWs) return; // جلوگیری از دوبار وصل شدن
200
  isConnecting = true;
201
- geminiWs = await tryConnectToGemini(ws, data, startingKeyIndex);
 
202
  isConnecting = false;
203
  return;
204
  }
205
  }
206
  }
207
 
208
- // اگر به گوگل وصلیم، پیام را بفرست
209
  if (geminiWs && geminiWs.readyState === WebSocket.OPEN) {
210
  geminiWs.send(message);
211
  }
@@ -214,20 +215,21 @@ wss.on('connection', (ws, req) => {
214
  }
215
  });
216
 
217
- // وقتی کاربر مرورگر را می‌بندد یا اینترنتش قطع می‌شود
218
  ws.on('close', () => {
219
- // console.log('👋 کلاینت قطع شد.');
220
  if (geminiWs) {
221
- if (geminiWs.readyState === WebSocket.OPEN || geminiWs.readyState === WebSocket.CONNECTING) {
222
- geminiWs.close();
223
- }
 
224
  geminiWs = null;
225
  }
226
  });
227
 
228
  ws.on('error', (error) => {
229
- console.error(`خطای سوکت کلاینت: ${error.message}`);
230
- if (geminiWs) geminiWs.close();
 
 
231
  });
232
  });
233
 
@@ -235,4 +237,4 @@ wss.on('connection', (ws, req) => {
235
  app.get('*', (req, res) => res.sendFile(path.join(__dirname, '../build', 'index.html')));
236
 
237
  const PORT = process.env.PORT || 3001;
238
- server.listen(PORT, () => console.log(`🚀 سرور (نسخه پایدار) روی پورت ${PORT} اجرا شد.`));
 
1
+ // server/index.js (نسخه بهینه شده: انتخاب تصادفی کلیدها + پایداری بالا)
2
  const express = require('express');
3
  const path = require('node:path');
4
  const { WebSocketServer, WebSocket } = require('ws');
 
6
  require('dotenv').config();
7
 
8
  const app = express();
9
+
10
+ // افزایش محدودیت‌ها برای جلوگیری از خطای هدر در درخواست‌های سنگین
11
  const server = http.createServer({
12
  maxHeaderSize: 16384 // 16KB
13
  }, app);
14
 
15
  const wss = new WebSocketServer({
16
  server,
17
+ clientTracking: true, // برای مدیریت هارت‌بیت و بستن اتصالات مرده
18
+ perMessageDeflate: false // غیرفعال کردن فشرده‌سازی برای کاهش بار CPU و حافظه
19
  });
20
 
21
  // --- بخش مدیریت تنظیمات شخصیت‌ها ---
 
33
  const instruction = process.env[secretName];
34
  if (instruction) {
35
  personalityInstructions[key] = instruction;
 
36
  } else {
37
  personalityInstructions[key] = `دستورالعمل '${key}' یافت نشد.`;
 
38
  }
39
  });
40
 
41
+ // --- بخش مدیریت کلیدهای API (Random Selection) ---
42
  const apiKeysEnv = process.env.ALL_GEMINI_API_KEYS;
43
+ // تبدیل رشته کلیدها به آرایه و حذف فاصله‌های اضافی
44
  const apiKeys = apiKeysEnv ? apiKeysEnv.split(',').map(key => key.trim()).filter(key => key) : [];
45
 
46
  if (apiKeys.length === 0) {
47
+ console.error('🔴 خطای حیاتی: هیچ کلید API یافت نشد! لطفا متغیر ALL_GEMINI_API_KEYS را تنظیم کنید.');
48
  process.exit(1);
49
  }
50
+ console.log(`🚀 سرور با ${apiKeys.length} کلید API آماده‌سازی شد. (حالت انتخاب تصادفی)`);
 
 
 
 
 
 
 
 
51
 
52
+ // --- مکانیزم Heartbeat برای جلوگیری از قطعی و هنگ کردن سرور ---
53
+ // این تابع هر ۳۰ ثانیه اتصالات مرده را پاک می‌کند تا حافظه آزاد شود
54
  function heartbeat() {
55
  this.isAlive = true;
56
  }
 
58
  const interval = setInterval(function ping() {
59
  wss.clients.forEach(function each(ws) {
60
  if (ws.isAlive === false) {
61
+ // اگر کلاینت پاسخ نداد، اتصال را قطع کن تا حافظه آزاد شود
62
  return ws.terminate();
63
  }
64
 
65
  ws.isAlive = false;
66
  ws.ping();
67
  });
68
+ }, 30000);
69
 
70
  wss.on('close', function close() {
71
  clearInterval(interval);
 
74
  // --- توابع اتصال به گوگل ---
75
 
76
  /**
77
+ * اتصال سوکت کلاینت و سوکت جمینای را به هم متصل می‌کند
78
  */
79
  function attachGeminiEventHandlers(clientWs, geminiWs, apiKeyUsed) {
80
  // انتقال پیام از گوگل به کلاینت
 
86
 
87
  geminiWs.on('error', (error) => {
88
  console.error(`🔴 خطای جمینای (کلید ...${apiKeyUsed.slice(-4)}):`, error.message);
89
+ // در صورت خطا، سوکت کلاینت را نمی‌بندیم تا شاید بتواند دوباره تلاش کند،
90
+ // اما معمولا کلاینت خودش قطع می‌شود.
91
  if (clientWs.readyState === WebSocket.OPEN) clientWs.close();
92
  });
93
 
94
  geminiWs.on('close', (code) => {
 
 
 
95
  if (clientWs.readyState === WebSocket.OPEN) clientWs.close();
96
  });
97
 
98
+ geminiWs.on('ping', () => {
99
+ try { geminiWs.pong(); } catch (e) {}
100
+ });
101
  }
102
 
103
  /**
104
+ * تلاش برای اتصال به جمینای با انتخاب تصادفی و امتحان مجدد
105
+ * @param {WebSocket} clientWs سوکت کاربر
106
+ * @param {Object} setupData داده‌های اولیه (کانفیگ)
107
+ * @param {Number} startIndex ایندکس شروع (که به صورت تصادفی انتخاب شده)
108
+ * @param {Number} attemptCount شمارنده تلاش‌ها
109
  */
110
  async function tryConnectToGemini(clientWs, setupData, startIndex, attemptCount = 0) {
111
+ // اگر کلاینت در حین تلاش قطع شد، ادامه نده (جلوگیری از لیک حافظه)
112
  if (clientWs.readyState !== WebSocket.OPEN) return null;
113
 
114
+ // اگر تمام کلیدها تست شدند و هیچکدام کار نکرد
115
  if (attemptCount >= apiKeys.length) {
116
  console.error(`⛔ تمام ${apiKeys.length} کلید API با شکست مواجه شدند.`);
117
  if (clientWs.readyState === WebSocket.OPEN) {
 
121
  return null;
122
  }
123
 
124
+ // منطق انتخاب کلید:
125
+ // بار اول: ایندکس کاملا تصادفی (startIndex)
126
+ // دفعات بعد (در صورت خطا): کلید بعدی در لیست به صورت چرخشی
127
  const keyIndexToTry = (startIndex + attemptCount) % apiKeys.length;
128
  const apiKey = apiKeys[keyIndexToTry];
129
+
130
  const url = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=${apiKey}`;
131
 
132
  return new Promise((resolve) => {
133
  const geminiWs = new WebSocket(url);
134
 
135
+ // تایم‌اوت سخت: اگر گوگل تا ۸ ثانیه هیچ پاسخی نداد (هنگ کرد)، برو کلید بعدی
136
  const timeout = setTimeout(() => {
137
+ console.warn(`⏳ تایم‌اوت اتصال (کلید ...${apiKey.slice(-4)}). رفتن به کلید بعدی...`);
138
+ geminiWs.terminate();
139
  resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
140
  }, 8000);
141
 
142
  geminiWs.on('open', () => {
143
  clearTimeout(timeout);
144
+
145
  if (clientWs.readyState !== WebSocket.OPEN) {
146
  geminiWs.close();
147
  return resolve(null);
148
  }
149
 
150
+ console.log(`🔗 اتصال موفق با کلید شماره ${keyIndexToTry} (تلاش ${attemptCount + 1})`);
151
 
152
  try {
153
  geminiWs.send(JSON.stringify(setupData));
 
162
 
163
  geminiWs.on('error', (err) => {
164
  clearTimeout(timeout);
165
+ console.warn(`⚠️ خطای اتصال (کلید ...${apiKey.slice(-4)}): ${err.message || 'Unknown'}. تلاش با کلید بعدی...`);
166
+ // فراخوانی بازگشتی برای تست کلید بعدی
167
  resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
168
  });
169
  });
 
174
 
175
  // API Endpoint دستورالعمل‌ها
176
  app.get('/api/instructions', (req, res) => {
177
+ res.setHeader('Cache-Control', 'no-store');
178
  res.json(personalityInstructions);
179
  });
180
 
181
  // --- مدیریت WebSocket کلاینت ---
182
  wss.on('connection', (ws, req) => {
183
+ ws.isAlive = true;
184
+ ws.on('pong', heartbeat);
185
 
186
+ // *تغییر اصلی:* انتخاب یک نقطه شروع کاملاً تصادفی برای هر کاربر
187
+ // این کار باعث می‌شود بار روی سرور پخش شود و نیاز به حافظه برای نگهداری نوبت نباشد.
188
+ const randomStartIndex = Math.floor(Math.random() * apiKeys.length);
189
 
190
  let geminiWs = null;
191
  let isConnecting = false;
192
 
193
  ws.on('message', async (message) => {
194
  try {
 
195
  if (!Buffer.isBuffer(message) || message[0] === 123) {
196
  const msgStr = message.toString();
 
197
  if (msgStr.startsWith('{')) {
198
  const data = JSON.parse(msgStr);
199
  if (data.setup) {
200
+ if (isConnecting || geminiWs) return;
201
  isConnecting = true;
202
+ // ارسال ایندکس تصادفی به تابع اتصال
203
+ geminiWs = await tryConnectToGemini(ws, data, randomStartIndex);
204
  isConnecting = false;
205
  return;
206
  }
207
  }
208
  }
209
 
 
210
  if (geminiWs && geminiWs.readyState === WebSocket.OPEN) {
211
  geminiWs.send(message);
212
  }
 
215
  }
216
  });
217
 
 
218
  ws.on('close', () => {
 
219
  if (geminiWs) {
220
+ // بستن اجباری اتصال گوگل برای آزادسازی منابع
221
+ try {
222
+ geminiWs.terminate();
223
+ } catch(e) {}
224
  geminiWs = null;
225
  }
226
  });
227
 
228
  ws.on('error', (error) => {
229
+ if (geminiWs) {
230
+ try { geminiWs.terminate(); } catch(e) {}
231
+ geminiWs = null;
232
+ }
233
  });
234
  });
235
 
 
237
  app.get('*', (req, res) => res.sendFile(path.join(__dirname, '../build', 'index.html')));
238
 
239
  const PORT = process.env.PORT || 3001;
240
+ server.listen(PORT, () => console.log(`🚀 سرور (حالت تصادفی) روی پورت ${PORT} اجرا شد.`));