LerinaOwO commited on
Commit
95fc36c
·
verified ·
1 Parent(s): ae8d8df

Update stealth-proxy/index.js

Browse files
Files changed (1) hide show
  1. stealth-proxy/index.js +189 -237
stealth-proxy/index.js CHANGED
@@ -7,64 +7,41 @@
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,112 +58,7 @@ async function loadStealth() {
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,7 +81,6 @@ async function initBrowser() {
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,6 +89,7 @@ async function initBrowser() {
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,134 +97,205 @@ async function initBrowser() {
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,9 +317,12 @@ app.post('/proxy/chat', async (req, res) => {
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,9 +333,12 @@ app.post('/proxy/chat', async (req, res) => {
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,19 +372,22 @@ app.post('/proxy/chat', async (req, res) => {
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,7 +395,7 @@ app.post('/proxy/chat', async (req, res) => {
443
  }
444
  const delay = attempt * 5;
445
  console.log(`[Stealth] Retrying in ${delay}s...`);
446
- await sleep(delay * 1000);
447
  }
448
  }
449
 
@@ -451,24 +403,24 @@ app.post('/proxy/chat', async (req, res) => {
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);
 
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
  return chromium;
59
  }
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  async function initBrowser() {
 
 
62
  const chromium = await loadStealth();
63
  const chromePath = findSystemChrome();
64
 
 
81
 
82
  console.log('[Stealth] Launching browser...');
83
  browser = await chromium.launch(launchOptions);
 
84
 
85
  context = await browser.newContext({
86
  userAgent:
 
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
  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
 
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
  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
  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
  }
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
  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);