File size: 9,442 Bytes
5058361
7f2a14a
 
 
 
0fd4af6
7f2a14a
 
5058361
 
f0896a3
 
 
7f2a14a
f0896a3
 
5058361
 
f0896a3
 
 
0ded4c9
9e404a1
 
 
 
0ded4c9
f0896a3
0ded4c9
f0896a3
0ded4c9
9e404a1
 
 
 
 
f0896a3
9e404a1
0ded4c9
3ffbc23
5058361
0fd4af6
5058361
0fd4af6
f0896a3
0ded4c9
5058361
9e404a1
3ffbc23
5058361
f0896a3
5058361
 
f0896a3
 
 
 
 
 
 
5058361
f0896a3
 
 
 
 
 
5058361
f0896a3
 
 
 
 
 
 
 
5058361
f0896a3
0fd4af6
f0896a3
 
 
 
 
9e404a1
f0896a3
 
 
5058361
 
f0896a3
 
 
 
 
9e404a1
f0896a3
5058361
 
 
0fd4af6
f0896a3
 
5058361
 
 
 
 
f0896a3
0fd4af6
5058361
f0896a3
 
5058361
9e404a1
f0896a3
 
 
 
 
9e404a1
 
f0896a3
5058361
 
 
9e404a1
 
5058361
f0896a3
 
 
 
 
5058361
9e404a1
5058361
 
9e404a1
 
f0896a3
9e404a1
 
5058361
f0896a3
 
 
 
 
5058361
f0896a3
9e404a1
 
f0896a3
 
9e404a1
f0896a3
 
 
9e404a1
0fd4af6
f0896a3
 
9e404a1
5058361
 
9e404a1
 
 
0fd4af6
3ffbc23
f0896a3
affece8
335046c
f0896a3
 
5058361
f0896a3
 
 
 
 
5058361
 
affece8
5058361
 
 
f0896a3
9e404a1
f0896a3
 
 
9e404a1
f0896a3
 
 
 
 
5058361
f0896a3
5058361
 
f0896a3
 
 
 
 
 
 
 
9e404a1
f0896a3
 
 
 
 
 
 
5058361
 
 
 
f0896a3
 
 
 
 
5058361
 
 
 
9e404a1
7f2a14a
 
f0896a3
0ded4c9
7f2a14a
0fd4af6
5058361
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
// server/index.js (نسخه بهینه شده: انتخاب تصادفی کلیدها + پایداری بالا)
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({
    maxHeaderSize: 16384 // 16KB
}, app);

const wss = new WebSocketServer({ 
    server,
    clientTracking: true, // برای مدیریت هارت‌بیت و بستن اتصالات مرده
    perMessageDeflate: false // غیرفعال کردن فشرده‌سازی برای کاهش بار CPU و حافظه
});

// --- بخش مدیریت تنظیمات شخصیت‌ها ---
const instructionSecretNames = {
  default: 'PERSONALITY_DEFAULT',
  teacher: 'PERSONALITY_TEACHER',
  poetic: 'PERSONALITY_POETIC',
  funny: 'PERSONALITY_FUNNY',
};

const personalityInstructions = {};
console.log('🔄 در حال خواندن دستورالعمل‌های شخصیت از Secrets...');
Object.keys(instructionSecretNames).forEach(key => {
  const secretName = instructionSecretNames[key];
  const instruction = process.env[secretName];
  if (instruction) {
    personalityInstructions[key] = instruction;
  } else {
    personalityInstructions[key] = `دستورالعمل '${key}' یافت نشد.`;
  }
});

// --- بخش مدیریت کلیدهای API (Random Selection) ---
const apiKeysEnv = process.env.ALL_GEMINI_API_KEYS;
// تبدیل رشته کلیدها به آرایه و حذف فاصله‌های اضافی
const apiKeys = apiKeysEnv ? apiKeysEnv.split(',').map(key => key.trim()).filter(key => key) : [];

if (apiKeys.length === 0) {
  console.error('🔴 خطای حیاتی: هیچ کلید API یافت نشد! لطفا متغیر ALL_GEMINI_API_KEYS را تنظیم کنید.');
  process.exit(1);
}
console.log(`🚀 سرور با ${apiKeys.length} کلید API آماده‌سازی شد. (حالت انتخاب تصادفی)`);

// --- مکانیزم Heartbeat برای جلوگیری از قطعی و هنگ کردن سرور ---
// این تابع هر ۳۰ ثانیه اتصالات مرده را پاک می‌کند تا حافظه آزاد شود
function heartbeat() {
  this.isAlive = true;
}

const interval = setInterval(function ping() {
  wss.clients.forEach(function each(ws) {
    if (ws.isAlive === false) {
        // اگر کلاینت پاسخ نداد، اتصال را قطع کن تا حافظه آزاد شود
        return ws.terminate();
    }
    
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', function close() {
  clearInterval(interval);
});

// --- توابع اتصال به گوگل ---

/**
 * اتصال سوکت کلاینت و سوکت جمینای را به هم متصل می‌کند
 */
function attachGeminiEventHandlers(clientWs, geminiWs, apiKeyUsed) {
  // انتقال پیام از گوگل به کلاینت
  geminiWs.on('message', (data) => {
    if (clientWs.readyState === WebSocket.OPEN) {
        clientWs.send(data, { binary: true });
    }
  });

  geminiWs.on('error', (error) => {
    console.error(`🔴 خطای جمینای (کلید ...${apiKeyUsed.slice(-4)}):`, error.message);
    // در صورت خطا، سوکت کلاینت را نمی‌بندیم تا شاید بتواند دوباره تلاش کند، 
    // اما معمولا کلاینت خودش قطع می‌شود.
    if (clientWs.readyState === WebSocket.OPEN) clientWs.close();
  });

  geminiWs.on('close', (code) => {
    if (clientWs.readyState === WebSocket.OPEN) clientWs.close();
  });
  
  geminiWs.on('ping', () => {
      try { geminiWs.pong(); } catch (e) {}
  });
}

/**
 * تلاش برای اتصال به جمینای با انتخاب تصادفی و امتحان مجدد
 * @param {WebSocket} clientWs سوکت کاربر
 * @param {Object} setupData داده‌های اولیه (کانفیگ)
 * @param {Number} startIndex ایندکس شروع (که به صورت تصادفی انتخاب شده)
 * @param {Number} attemptCount شمارنده تلاش‌ها
 */
async function tryConnectToGemini(clientWs, setupData, startIndex, attemptCount = 0) {
  // اگر کلاینت در حین تلاش قطع شد، ادامه نده (جلوگیری از لیک حافظه)
  if (clientWs.readyState !== WebSocket.OPEN) return null;

  // اگر تمام کلیدها تست شدند و هیچکدام کار نکرد
  if (attemptCount >= apiKeys.length) {
    console.error(`⛔ تمام ${apiKeys.length} کلید API با شکست مواجه شدند.`);
    if (clientWs.readyState === WebSocket.OPEN) {
        clientWs.send(JSON.stringify({ error: "سرویس موقتاً در دسترس نیست (ظرفیت تکمیل)." }));
        clientWs.close();
    }
    return null;
  }

  // منطق انتخاب کلید:
  // بار اول: ایندکس کاملا تصادفی (startIndex)
  // دفعات بعد (در صورت خطا): کلید بعدی در لیست به صورت چرخشی
  const keyIndexToTry = (startIndex + attemptCount) % apiKeys.length;
  const apiKey = apiKeys[keyIndexToTry];
  
  const url = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=${apiKey}`;

  return new Promise((resolve) => {
    const geminiWs = new WebSocket(url);
    
    // تایم‌اوت سخت: اگر گوگل تا ۸ ثانیه هیچ پاسخی نداد (هنگ کرد)، برو کلید بعدی
    const timeout = setTimeout(() => {
      console.warn(`⏳ تایم‌اوت اتصال (کلید ...${apiKey.slice(-4)}). رفتن به کلید بعدی...`);
      geminiWs.terminate(); 
      resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
    }, 8000);

    geminiWs.on('open', () => {
      clearTimeout(timeout);
      
      if (clientWs.readyState !== WebSocket.OPEN) {
          geminiWs.close();
          return resolve(null);
      }
      
      console.log(`🔗 اتصال موفق با کلید شماره ${keyIndexToTry} (تلاش ${attemptCount + 1})`);
      
      try {
        geminiWs.send(JSON.stringify(setupData));
        attachGeminiEventHandlers(clientWs, geminiWs, apiKey);
        resolve(geminiWs);
      } catch (e) {
        console.error("خطا در ارسال تنظیمات اولیه:", e);
        geminiWs.close();
        resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
      }
    });

    geminiWs.on('error', (err) => {
      clearTimeout(timeout);
      console.warn(`⚠️ خطای اتصال (کلید ...${apiKey.slice(-4)}): ${err.message || 'Unknown'}. تلاش با کلید بعدی...`);
      // فراخوانی بازگشتی برای تست کلید بعدی
      resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
    });
  });
}

// --- سرو کردن فایل‌های استاتیک ---
app.use(express.static(path.join(__dirname, '../build')));

// API Endpoint دستورالعمل‌ها
app.get('/api/instructions', (req, res) => {
    res.setHeader('Cache-Control', 'no-store'); 
    res.json(personalityInstructions);
});

// --- مدیریت WebSocket کلاینت ---
wss.on('connection', (ws, req) => {
  ws.isAlive = true;
  ws.on('pong', heartbeat);

  // *تغییر اصلی:* انتخاب یک نقطه شروع کاملاً تصادفی برای هر کاربر
  // این کار باعث می‌شود بار روی سرور پخش شود و نیاز به حافظه برای نگهداری نوبت نباشد.
  const randomStartIndex = Math.floor(Math.random() * apiKeys.length);
  
  let geminiWs = null;
  let isConnecting = false;

  ws.on('message', async (message) => {
    try {
      if (!Buffer.isBuffer(message) || message[0] === 123) { 
        const msgStr = message.toString();
        if (msgStr.startsWith('{')) {
            const data = JSON.parse(msgStr);
            if (data.setup) {
                if (isConnecting || geminiWs) return; 
                isConnecting = true;
                // ارسال ایندکس تصادفی به تابع اتصال
                geminiWs = await tryConnectToGemini(ws, data, randomStartIndex);
                isConnecting = false;
                return;
            }
        }
      }
      
      if (geminiWs && geminiWs.readyState === WebSocket.OPEN) {
        geminiWs.send(message);
      }
    } catch (e) {
      console.error("خطا در پردازش پیام کلاینت:", e.message);
    }
  });

  ws.on('close', () => {
    if (geminiWs) {
        // بستن اجباری اتصال گوگل برای آزادسازی منابع
        try {
            geminiWs.terminate(); 
        } catch(e) {}
        geminiWs = null;
    }
  });

  ws.on('error', (error) => {
    if (geminiWs) {
        try { geminiWs.terminate(); } catch(e) {}
        geminiWs = null;
    }
  });
});

// ارسال تمام درخواست‌های دیگر به React
app.get('*', (req, res) => res.sendFile(path.join(__dirname, '../build', 'index.html')));

const PORT = process.env.PORT || 3001;
server.listen(PORT, () => console.log(`🚀 سرور (حالت تصادفی) روی پورت ${PORT} اجرا شد.`));