LerinaOwO commited on
Commit
ae8d8df
·
verified ·
1 Parent(s): 6df40b1

Update stealth-proxy/index.js

Browse files
Files changed (1) hide show
  1. stealth-proxy/index.js +237 -189
stealth-proxy/index.js CHANGED
@@ -7,41 +7,64 @@
7
  * 原理:
8
  * 1. 启动时用 stealth 浏览器访问 cursor.com,通过 JS Challenge 获取 _vcrcs cookie
9
  * 2. 在同一浏览器上下文内通过 page.evaluate(fetch) 代理 API 请求
10
- * 3. 定时刷新 challenge(_vcrcs 有效期 3600s,每 50 分钟刷新)
11
  * 4. 支持 SSE 流式响应透传
12
  */
13
 
14
  const express = require('express');
15
  const crypto = require('crypto');
 
16
 
17
- const PORT = parseInt(process.env.PORT || '3011');
18
  const CHALLENGE_URL = process.env.CHALLENGE_URL || 'https://cursor.com/cn/docs';
19
- const REFRESH_INTERVAL = parseInt(process.env.REFRESH_INTERVAL || '3000000'); // 50 分钟
20
- const CHALLENGE_WAIT = parseInt(process.env.CHALLENGE_WAIT || '55000'); // challenge 最长等待时间
21
-
22
- let browser, context, challengePage, workerPage;
 
 
 
 
 
 
23
  let ready = false;
 
 
 
 
24
  let startTime = Date.now();
25
  let challengeCount = 0;
26
  let requestCount = 0;
27
 
28
  const pendingRequests = new Map();
29
 
30
- // ==================== 浏览器管理 ====================
 
 
31
 
32
- const fs = require('fs');
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  // 自动检测系统 Chrome 路径(优先使用,指纹更真实)
35
  function findSystemChrome() {
36
  const paths = [
37
- // macOS
38
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
39
- // Linux
40
  '/usr/bin/google-chrome',
41
  '/usr/bin/google-chrome-stable',
42
  '/usr/bin/chromium',
43
  '/usr/bin/chromium-browser',
44
- // Windows
45
  'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
46
  'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
47
  ];
@@ -58,7 +81,112 @@ async function loadStealth() {
58
  return chromium;
59
  }
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  async function initBrowser() {
 
 
62
  const chromium = await loadStealth();
63
  const chromePath = findSystemChrome();
64
 
@@ -81,6 +209,7 @@ async function initBrowser() {
81
 
82
  console.log('[Stealth] Launching browser...');
83
  browser = await chromium.launch(launchOptions);
 
84
 
85
  context = await browser.newContext({
86
  userAgent:
@@ -89,7 +218,6 @@ async function initBrowser() {
89
  viewport: { width: 1920, height: 1080 },
90
  });
91
 
92
- // ---- Challenge 页面:获取 _vcrcs ----
93
  challengePage = await context.newPage();
94
  console.log(`[Stealth] Passing Vercel challenge: ${CHALLENGE_URL}`);
95
  await challengePage.goto(CHALLENGE_URL, {
@@ -97,205 +225,134 @@ async function initBrowser() {
97
  timeout: 60000,
98
  });
99
 
100
- // 模拟人类行为,帮助通过 bot 检测
101
  await simulateHumanBehavior(challengePage);
102
 
103
- // 等待 cookie(challenge JS 会异步设置 _vcrcs)
104
- const ok = await waitForCookie();
105
- if (!ok) {
106
- // 重试:刷新页面 + 再次模拟行为
107
  console.log('[Stealth] First attempt failed, retrying challenge...');
108
  await challengePage.reload({ waitUntil: 'domcontentloaded', timeout: 60000 });
109
  await simulateHumanBehavior(challengePage);
110
- const retryOk = await waitForCookie();
111
- if (!retryOk) {
112
  throw new Error('Failed to obtain _vcrcs cookie after retry');
113
  }
114
  }
115
- challengeCount++;
116
-
117
- // ---- Worker 页面:代理 API 请求 ----
118
- // cookie 已在 challenge 页面获取,worker 页面只需加载到 domcontentloaded 即可
119
- workerPage = await context.newPage();
120
- await workerPage.goto(CHALLENGE_URL, {
121
- waitUntil: 'domcontentloaded',
122
- timeout: 60000,
123
- });
124
 
125
- // 注册流式回调(Node.js 侧接收浏览器内 fetch 的数据块)
126
- await workerPage.exposeFunction(
127
- '__proxyCallback',
128
- (requestId, type, data) => {
129
- const pending = pendingRequests.get(requestId);
130
- if (!pending) return;
131
-
132
- switch (type) {
133
- case 'headers': {
134
- const { status, contentType } = JSON.parse(data);
135
- const headers = {
136
- 'Cache-Control': 'no-cache',
137
- Connection: 'keep-alive',
138
- };
139
- if (contentType) headers['Content-Type'] = contentType;
140
- pending.res.writeHead(status, headers);
141
- break;
142
- }
143
- case 'chunk':
144
- pending.res.write(data);
145
- break;
146
- case 'end':
147
- pending.res.end();
148
- pending.resolve();
149
- break;
150
- case 'error':
151
- if (!pending.res.headersSent) {
152
- pending.res.writeHead(502, {
153
- 'Content-Type': 'application/json',
154
- });
155
- }
156
- pending.res.end(
157
- JSON.stringify({ error: { message: data } }),
158
- );
159
- pending.resolve();
160
- break;
161
- }
162
- },
163
- );
164
 
165
  ready = true;
166
  console.log('[Stealth] Ready! Accepting proxy requests.');
167
  }
168
 
169
- async function waitForCookie(maxWait) {
170
- maxWait = maxWait || CHALLENGE_WAIT;
171
- const start = Date.now();
172
- while (Date.now() - start < maxWait) {
173
- const cookies = await context.cookies();
174
- const vcrcs = cookies.find((c) => c.name === '_vcrcs');
175
- if (vcrcs) {
176
- console.log(
177
- '[Stealth] _vcrcs obtained:',
178
- vcrcs.value.substring(0, 40) + '...',
179
- );
180
- return true;
181
- }
182
- await new Promise((r) => setTimeout(r, 2000));
183
  }
184
- console.error('[Stealth] Failed to obtain _vcrcs within timeout');
185
- return false;
186
  }
187
 
188
- // 模拟人类行为:鼠标移动、点击、滚动,帮助通过 Vercel bot 检测
189
- async function simulateHumanBehavior(page) {
190
- try {
191
- // 随机延迟
192
- await new Promise(r => setTimeout(r, 500 + Math.random() * 1000));
193
 
194
- // 模拟鼠标移动轨迹(多个随机点)
195
- const points = Array.from({ length: 5 }, () => ({
196
- x: 100 + Math.random() * 600,
197
- y: 100 + Math.random() * 400,
198
- }));
199
- for (const p of points) {
200
- await page.mouse.move(p.x, p.y, { steps: 5 + Math.floor(Math.random() * 10) });
201
- await new Promise(r => setTimeout(r, 100 + Math.random() * 300));
202
- }
 
203
 
204
- // 模拟滚动
205
- await page.mouse.wheel(0, 100 + Math.random() * 200);
206
- await new Promise(r => setTimeout(r, 300 + Math.random() * 500));
207
- await page.mouse.wheel(0, -(50 + Math.random() * 100));
 
 
208
 
209
- // 模拟点击页面空白区域
210
- await page.mouse.click(300 + Math.random() * 400, 300 + Math.random() * 200);
211
- await new Promise(r => setTimeout(r, 200 + Math.random() * 500));
 
212
 
213
- console.log('[Stealth] Human behavior simulation done');
214
- } catch (e) {
215
- // 模拟行为失败不影响主流程
216
- console.log('[Stealth] Human simulation skipped:', e.message);
217
  }
218
- }
219
 
220
- async function refreshChallenge() {
221
- console.log('[Stealth] Refreshing challenge...');
222
- try {
223
- await challengePage.goto(CHALLENGE_URL, {
224
- waitUntil: 'networkidle',
225
- timeout: 30000,
226
- });
227
- const ok = await waitForCookie();
228
- if (ok) {
229
- challengeCount++;
230
  console.log(
231
- `[Stealth] Challenge refreshed (total: ${challengeCount})`,
232
  );
 
 
 
 
 
233
  } else {
234
- console.error('[Stealth] Challenge refresh failed - cookie not obtained');
235
  }
236
- } catch (e) {
237
- console.error('[Stealth] Challenge refresh error:', e.message);
238
  }
239
- }
240
 
241
- async function restartBrowser() {
242
- console.log('[Stealth] Restarting browser...');
243
- ready = false;
244
- try {
245
- if (browser) await browser.close().catch(() => {});
246
- } catch (_) {}
247
- browser = null;
248
- context = null;
249
- challengePage = null;
250
- workerPage = null;
251
- await initBrowser();
252
  }
253
 
254
- // ==================== HTTP 服务 ====================
255
-
256
  const app = express();
257
  app.use(express.json({ limit: '10mb' }));
258
 
259
- // 健康检查
260
  app.get('/health', async (_req, res) => {
261
- let cookie = null;
262
- if (context) {
263
- const cookies = await context.cookies().catch(() => []);
264
- const vcrcs = cookies.find((c) => c.name === '_vcrcs');
265
- if (vcrcs) cookie = vcrcs.value.substring(0, 40) + '...';
266
- }
267
  res.json({
268
- status: ready ? 'ok' : 'initializing',
269
  uptime: Math.floor((Date.now() - startTime) / 1000),
270
  challengeCount,
271
  requestCount,
272
- cookie,
 
273
  });
274
  });
275
 
276
- // 代理请求
277
  app.post('/proxy/chat', async (req, res) => {
278
- if (!ready) {
279
  res.status(503).json({
280
- error: { message: 'Stealth proxy not ready, please wait' },
281
  });
282
  return;
283
  }
284
 
285
  const requestId = crypto.randomUUID();
286
- requestCount++;
287
 
288
- // 客户端断开时清理
289
- let aborted = false;
290
  req.on('close', () => {
291
- aborted = true;
 
 
 
 
 
 
 
292
  });
293
 
294
  const promise = new Promise((resolve) => {
295
  pendingRequests.set(requestId, { res, resolve });
296
  });
297
 
298
- // 在浏览器上下文内发起 fetch 并流式回传
299
  workerPage
300
  .evaluate(
301
  async ({ body, requestId }) => {
@@ -317,12 +374,9 @@ app.post('/proxy/chat', async (req, res) => {
317
 
318
  if (!r.body) {
319
  const text = await r.text();
320
- if (text)
321
- await window.__proxyCallback(
322
- requestId,
323
- 'chunk',
324
- text,
325
- );
326
  await window.__proxyCallback(requestId, 'end', '');
327
  return;
328
  }
@@ -333,12 +387,9 @@ app.post('/proxy/chat', async (req, res) => {
333
  const { done, value } = await reader.read();
334
  if (done) break;
335
  const chunk = decoder.decode(value, { stream: true });
336
- if (chunk)
337
- await window.__proxyCallback(
338
- requestId,
339
- 'chunk',
340
- chunk,
341
- );
342
  }
343
  await window.__proxyCallback(requestId, 'end', '');
344
  } catch (e) {
@@ -372,22 +423,19 @@ app.post('/proxy/chat', async (req, res) => {
372
  pendingRequests.delete(requestId);
373
  });
374
 
375
- // ==================== 启动 ====================
376
-
377
- const MAX_INIT_RETRIES = parseInt(process.env.MAX_INIT_RETRIES || '5');
378
-
379
  (async () => {
380
- // 自动重试启动:网络不稳定时多试几次
381
- for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
382
  try {
383
  console.log(`[Stealth] Initialization attempt ${attempt}/${MAX_INIT_RETRIES}...`);
384
  await initBrowser();
385
- break; // 成功,跳出重试循环
386
  } catch (e) {
387
  console.error(`[Stealth] Attempt ${attempt} failed:`, e.message);
388
- // 清理失败的浏览器实例
389
- if (browser) await browser.close().catch(() => {});
390
- browser = null; context = null; challengePage = null; workerPage = null;
 
 
391
 
392
  if (attempt >= MAX_INIT_RETRIES) {
393
  console.error(`[Stealth] All ${MAX_INIT_RETRIES} attempts failed, exiting.`);
@@ -395,7 +443,7 @@ const MAX_INIT_RETRIES = parseInt(process.env.MAX_INIT_RETRIES || '5');
395
  }
396
  const delay = attempt * 5;
397
  console.log(`[Stealth] Retrying in ${delay}s...`);
398
- await new Promise(r => setTimeout(r, delay * 1000));
399
  }
400
  }
401
 
@@ -403,24 +451,24 @@ const MAX_INIT_RETRIES = parseInt(process.env.MAX_INIT_RETRIES || '5');
403
  console.log(`[Stealth] Proxy listening on port ${PORT}`);
404
  });
405
 
406
- // 定时刷新 challenge
407
- setInterval(refreshChallenge, REFRESH_INTERVAL);
408
-
409
- // 浏览器崩溃恢复
410
- browser.on('disconnected', () => {
411
- console.error('[Stealth] Browser disconnected! Restarting...');
412
- ready = false;
413
- setTimeout(restartBrowser, 3000);
414
- });
415
  })();
416
 
417
- // 优雅退出
418
  const shutdown = async () => {
419
  console.log('[Stealth] Shutting down...');
 
420
  ready = false;
421
- if (browser) await browser.close().catch(() => {});
 
 
 
 
422
  process.exit(0);
423
  };
424
 
425
  process.on('SIGTERM', shutdown);
426
- process.on('SIGINT', shutdown);
 
7
  * 原理:
8
  * 1. 启动时用 stealth 浏览器访问 cursor.com,通过 JS Challenge 获取 _vcrcs cookie
9
  * 2. 在同一浏览器上下文内通过 page.evaluate(fetch) 代理 API 请求
10
+ * 3. 定时完整重建浏览器上下文,避免只刷新 challengePage 导致 workerPage 状态失配
11
  * 4. 支持 SSE 流式响应透传
12
  */
13
 
14
  const express = require('express');
15
  const crypto = require('crypto');
16
+ const fs = require('fs');
17
 
18
+ const PORT = parseInt(process.env.PORT || '3011', 10);
19
  const CHALLENGE_URL = process.env.CHALLENGE_URL || 'https://cursor.com/cn/docs';
20
+ const REFRESH_INTERVAL = parseInt(process.env.REFRESH_INTERVAL || '3000000', 10); // 50 分钟
21
+ const CHALLENGE_WAIT = parseInt(process.env.CHALLENGE_WAIT || '55000', 10); // challenge 最长等待时间
22
+ const MAX_INIT_RETRIES = parseInt(process.env.MAX_INIT_RETRIES || '5', 10);
23
+ const RESTART_POSTPONE_MS = parseInt(process.env.RESTART_POSTPONE_MS || '60000', 10); // 有活跃请求时延后 60 秒
24
+
25
+ let browser = null;
26
+ let context = null;
27
+ let challengePage = null;
28
+ let workerPage = null;
29
+ let refreshTimer = null;
30
  let ready = false;
31
+ let expectedBrowserClose = false;
32
+ let isShuttingDown = false;
33
+ let restartPromise = null;
34
+ let scheduledRestartPending = false;
35
  let startTime = Date.now();
36
  let challengeCount = 0;
37
  let requestCount = 0;
38
 
39
  const pendingRequests = new Map();
40
 
41
+ function sleep(ms) {
42
+ return new Promise((resolve) => setTimeout(resolve, ms));
43
+ }
44
 
45
+ function getActiveRequestCount() {
46
+ return pendingRequests.size;
47
+ }
48
+
49
+ function getCookiePreview(value) {
50
+ if (!value) return null;
51
+ return `${value.substring(0, 40)}...`;
52
+ }
53
+
54
+ async function getVcrcsCookie() {
55
+ if (!context) return null;
56
+ const cookies = await context.cookies().catch(() => []);
57
+ return cookies.find((c) => c.name === '_vcrcs') || null;
58
+ }
59
 
60
  // 自动检测系统 Chrome 路径(优先使用,指纹更真实)
61
  function findSystemChrome() {
62
  const paths = [
 
63
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
 
64
  '/usr/bin/google-chrome',
65
  '/usr/bin/google-chrome-stable',
66
  '/usr/bin/chromium',
67
  '/usr/bin/chromium-browser',
 
68
  'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
69
  'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
70
  ];
 
81
  return chromium;
82
  }
83
 
84
+ function attachBrowserHandlers(currentBrowser) {
85
+ currentBrowser.on('disconnected', () => {
86
+ if (expectedBrowserClose || isShuttingDown) {
87
+ return;
88
+ }
89
+ console.error('[Stealth] Browser disconnected unexpectedly! Restarting...');
90
+ ready = false;
91
+ setTimeout(() => {
92
+ restartBrowser('browser disconnected').catch((err) => {
93
+ console.error('[Stealth] Restart after disconnect failed:', err.message);
94
+ });
95
+ }, 3000);
96
+ });
97
+ }
98
+
99
+ async function waitForCookie(maxWait) {
100
+ const timeout = maxWait || CHALLENGE_WAIT;
101
+ const start = Date.now();
102
+
103
+ while (Date.now() - start < timeout) {
104
+ const vcrcs = await getVcrcsCookie();
105
+ if (vcrcs) {
106
+ console.log('[Stealth] _vcrcs obtained:', getCookiePreview(vcrcs.value));
107
+ return vcrcs;
108
+ }
109
+ await sleep(2000);
110
+ }
111
+
112
+ console.error('[Stealth] Failed to obtain _vcrcs within timeout');
113
+ return null;
114
+ }
115
+
116
+ // 模拟人类行为:鼠标移动、点击、滚动,帮助通过 Vercel bot 检测
117
+ async function simulateHumanBehavior(page) {
118
+ try {
119
+ await sleep(500 + Math.random() * 1000);
120
+
121
+ const points = Array.from({ length: 5 }, () => ({
122
+ x: 100 + Math.random() * 600,
123
+ y: 100 + Math.random() * 400,
124
+ }));
125
+ for (const p of points) {
126
+ await page.mouse.move(p.x, p.y, { steps: 5 + Math.floor(Math.random() * 10) });
127
+ await sleep(100 + Math.random() * 300);
128
+ }
129
+
130
+ await page.mouse.wheel(0, 100 + Math.random() * 200);
131
+ await sleep(300 + Math.random() * 500);
132
+ await page.mouse.wheel(0, -(50 + Math.random() * 100));
133
+
134
+ await page.mouse.click(300 + Math.random() * 400, 300 + Math.random() * 200);
135
+ await sleep(200 + Math.random() * 500);
136
+
137
+ console.log('[Stealth] Human behavior simulation done');
138
+ } catch (e) {
139
+ console.log('[Stealth] Human simulation skipped:', e.message);
140
+ }
141
+ }
142
+
143
+ async function createWorkerPage() {
144
+ workerPage = await context.newPage();
145
+ await workerPage.goto(CHALLENGE_URL, {
146
+ waitUntil: 'domcontentloaded',
147
+ timeout: 60000,
148
+ });
149
+
150
+ await workerPage.exposeFunction('__proxyCallback', (requestId, type, data) => {
151
+ const pending = pendingRequests.get(requestId);
152
+ if (!pending) return;
153
+
154
+ switch (type) {
155
+ case 'headers': {
156
+ const { status, contentType } = JSON.parse(data);
157
+ const headers = {
158
+ 'Cache-Control': 'no-cache',
159
+ Connection: 'keep-alive',
160
+ };
161
+ if (contentType) headers['Content-Type'] = contentType;
162
+ pending.res.writeHead(status, headers);
163
+ break;
164
+ }
165
+ case 'chunk':
166
+ pending.res.write(data);
167
+ break;
168
+ case 'end':
169
+ pending.res.end();
170
+ pending.resolve();
171
+ break;
172
+ case 'error':
173
+ if (!pending.res.headersSent) {
174
+ pending.res.writeHead(502, {
175
+ 'Content-Type': 'application/json',
176
+ });
177
+ }
178
+ pending.res.end(JSON.stringify({ error: { message: data } }));
179
+ pending.resolve();
180
+ break;
181
+ default:
182
+ break;
183
+ }
184
+ });
185
+ }
186
+
187
  async function initBrowser() {
188
+ ready = false;
189
+
190
  const chromium = await loadStealth();
191
  const chromePath = findSystemChrome();
192
 
 
209
 
210
  console.log('[Stealth] Launching browser...');
211
  browser = await chromium.launch(launchOptions);
212
+ attachBrowserHandlers(browser);
213
 
214
  context = await browser.newContext({
215
  userAgent:
 
218
  viewport: { width: 1920, height: 1080 },
219
  });
220
 
 
221
  challengePage = await context.newPage();
222
  console.log(`[Stealth] Passing Vercel challenge: ${CHALLENGE_URL}`);
223
  await challengePage.goto(CHALLENGE_URL, {
 
225
  timeout: 60000,
226
  });
227
 
 
228
  await simulateHumanBehavior(challengePage);
229
 
230
+ let vcrcs = await waitForCookie();
231
+ if (!vcrcs) {
 
 
232
  console.log('[Stealth] First attempt failed, retrying challenge...');
233
  await challengePage.reload({ waitUntil: 'domcontentloaded', timeout: 60000 });
234
  await simulateHumanBehavior(challengePage);
235
+ vcrcs = await waitForCookie();
236
+ if (!vcrcs) {
237
  throw new Error('Failed to obtain _vcrcs cookie after retry');
238
  }
239
  }
240
+ challengeCount += 1;
 
 
 
 
 
 
 
 
241
 
242
+ await createWorkerPage();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
  ready = true;
245
  console.log('[Stealth] Ready! Accepting proxy requests.');
246
  }
247
 
248
+ async function closeBrowserSilently() {
249
+ if (!browser) return;
250
+ try {
251
+ expectedBrowserClose = true;
252
+ await browser.close().catch(() => {});
253
+ } finally {
254
+ expectedBrowserClose = false;
 
 
 
 
 
 
 
255
  }
 
 
256
  }
257
 
258
+ async function restartBrowser(reason = 'manual') {
259
+ if (restartPromise) {
260
+ console.log(`[Stealth] Restart already in progress, joining existing restart (${reason})...`);
261
+ return restartPromise;
262
+ }
263
 
264
+ restartPromise = (async () => {
265
+ console.log(`[Stealth] Restarting browser (${reason})...`);
266
+ ready = false;
267
+ await closeBrowserSilently();
268
+ browser = null;
269
+ context = null;
270
+ challengePage = null;
271
+ workerPage = null;
272
+ await initBrowser();
273
+ })();
274
 
275
+ try {
276
+ await restartPromise;
277
+ } finally {
278
+ restartPromise = null;
279
+ }
280
+ }
281
 
282
+ async function scheduleFullRestart(reason) {
283
+ if (isShuttingDown) {
284
+ return;
285
+ }
286
 
287
+ if (restartPromise) {
288
+ console.log(`[Stealth] Skip scheduled restart (${reason}): restart already in progress.`);
289
+ return restartPromise;
 
290
  }
 
291
 
292
+ const active = getActiveRequestCount();
293
+ if (active > 0) {
294
+ if (!scheduledRestartPending) {
295
+ scheduledRestartPending = true;
 
 
 
 
 
 
296
  console.log(
297
+ `[Stealth] Postponing scheduled restart (${reason}) because ${active} request(s) are active. Will retry in ${Math.floor(RESTART_POSTPONE_MS / 1000)}s.`,
298
  );
299
+ setTimeout(() => {
300
+ scheduleFullRestart('postponed scheduled restart').catch((err) => {
301
+ console.error('[Stealth] Postponed scheduled restart failed:', err.message);
302
+ });
303
+ }, RESTART_POSTPONE_MS);
304
  } else {
305
+ console.log(`[Stealth] Scheduled restart (${reason}) already postponed.`);
306
  }
307
+ return;
 
308
  }
 
309
 
310
+ scheduledRestartPending = false;
311
+ await restartBrowser(reason);
312
+ console.log('[Stealth] Scheduled full browser restart completed.');
 
 
 
 
 
 
 
 
313
  }
314
 
 
 
315
  const app = express();
316
  app.use(express.json({ limit: '10mb' }));
317
 
 
318
  app.get('/health', async (_req, res) => {
319
+ const vcrcs = await getVcrcsCookie();
 
 
 
 
 
320
  res.json({
321
+ status: ready ? 'ok' : restartPromise ? 'restarting' : 'initializing',
322
  uptime: Math.floor((Date.now() - startTime) / 1000),
323
  challengeCount,
324
  requestCount,
325
+ activeRequests: getActiveRequestCount(),
326
+ cookie: getCookiePreview(vcrcs && vcrcs.value),
327
  });
328
  });
329
 
 
330
  app.post('/proxy/chat', async (req, res) => {
331
+ if (!ready || restartPromise || !workerPage) {
332
  res.status(503).json({
333
+ error: { message: 'Stealth proxy is restarting, please retry shortly' },
334
  });
335
  return;
336
  }
337
 
338
  const requestId = crypto.randomUUID();
339
+ requestCount += 1;
340
 
 
 
341
  req.on('close', () => {
342
+ const pending = pendingRequests.get(requestId);
343
+ if (pending && !pending.res.writableEnded) {
344
+ try {
345
+ pending.res.end();
346
+ } catch (_) {}
347
+ pending.resolve();
348
+ }
349
+ pendingRequests.delete(requestId);
350
  });
351
 
352
  const promise = new Promise((resolve) => {
353
  pendingRequests.set(requestId, { res, resolve });
354
  });
355
 
 
356
  workerPage
357
  .evaluate(
358
  async ({ body, requestId }) => {
 
374
 
375
  if (!r.body) {
376
  const text = await r.text();
377
+ if (text) {
378
+ await window.__proxyCallback(requestId, 'chunk', text);
379
+ }
 
 
 
380
  await window.__proxyCallback(requestId, 'end', '');
381
  return;
382
  }
 
387
  const { done, value } = await reader.read();
388
  if (done) break;
389
  const chunk = decoder.decode(value, { stream: true });
390
+ if (chunk) {
391
+ await window.__proxyCallback(requestId, 'chunk', chunk);
392
+ }
 
 
 
393
  }
394
  await window.__proxyCallback(requestId, 'end', '');
395
  } catch (e) {
 
423
  pendingRequests.delete(requestId);
424
  });
425
 
 
 
 
 
426
  (async () => {
427
+ for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt += 1) {
 
428
  try {
429
  console.log(`[Stealth] Initialization attempt ${attempt}/${MAX_INIT_RETRIES}...`);
430
  await initBrowser();
431
+ break;
432
  } catch (e) {
433
  console.error(`[Stealth] Attempt ${attempt} failed:`, e.message);
434
+ await closeBrowserSilently();
435
+ browser = null;
436
+ context = null;
437
+ challengePage = null;
438
+ workerPage = null;
439
 
440
  if (attempt >= MAX_INIT_RETRIES) {
441
  console.error(`[Stealth] All ${MAX_INIT_RETRIES} attempts failed, exiting.`);
 
443
  }
444
  const delay = attempt * 5;
445
  console.log(`[Stealth] Retrying in ${delay}s...`);
446
+ await sleep(delay * 1000);
447
  }
448
  }
449
 
 
451
  console.log(`[Stealth] Proxy listening on port ${PORT}`);
452
  });
453
 
454
+ refreshTimer = setInterval(() => {
455
+ scheduleFullRestart('scheduled refresh').catch((err) => {
456
+ console.error('[Stealth] Scheduled full restart failed:', err.message);
457
+ });
458
+ }, REFRESH_INTERVAL);
 
 
 
 
459
  })();
460
 
 
461
  const shutdown = async () => {
462
  console.log('[Stealth] Shutting down...');
463
+ isShuttingDown = true;
464
  ready = false;
465
+ if (refreshTimer) {
466
+ clearInterval(refreshTimer);
467
+ refreshTimer = null;
468
+ }
469
+ await closeBrowserSilently();
470
  process.exit(0);
471
  };
472
 
473
  process.on('SIGTERM', shutdown);
474
+ process.on('SIGINT', shutdown);