@pedrofariasx/qwenproxy 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pedrofariasx/qwenproxy",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Local OpenAI-compatible proxy API that routes requests to Qwen (chat.qwen.ai) via Playwright browser automation.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -68,6 +68,49 @@ const REFRESH_THRESHOLD = 0.7;
68
68
 
69
69
  const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
70
70
 
71
+ function getStealthScript(): string {
72
+ return `
73
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
74
+ Object.defineProperty(navigator, 'plugins', {
75
+ get: () => [1, 2, 3, 4, 5],
76
+ });
77
+ Object.defineProperty(navigator, 'languages', {
78
+ get: () => ['pt-BR', 'pt', 'en-US', 'en'],
79
+ });
80
+ Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
81
+ Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
82
+ Object.defineProperty(navigator, 'platform', { get: () => 'Win32' });
83
+ Object.defineProperty(screen, 'colorDepth', { get: () => 24 });
84
+ Object.defineProperty(screen, 'pixelDepth', { get: () => 24 });
85
+ window.chrome = {
86
+ runtime: {},
87
+ loadTimes: function() {},
88
+ csi: function() {},
89
+ app: {},
90
+ };
91
+ const originalQuery = window.navigator.permissions.query;
92
+ window.navigator.permissions.query = (parameters) =>
93
+ parameters.name === 'notifications'
94
+ ? Promise.resolve({ state: Notification.permission })
95
+ : originalQuery(parameters);
96
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
97
+ WebGLRenderingContext.prototype.getParameter = function(parameter) {
98
+ if (parameter === 37445) return 'Intel Inc.';
99
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
100
+ return getParameter.apply(this, arguments);
101
+ };
102
+ Object.defineProperty(navigator, 'connection', {
103
+ get: () => ({
104
+ effectiveType: '4g',
105
+ rtt: 50,
106
+ downlink: 10,
107
+ saveData: false,
108
+ }),
109
+ });
110
+ delete navigator.__proto__.webdriver;
111
+ `;
112
+ }
113
+
71
114
  export class Mutex {
72
115
  private queue: (() => void)[] = [];
73
116
  private locked = false;
@@ -169,15 +212,15 @@ export async function initPlaywright(headless = true, browserType: BrowserType =
169
212
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
170
213
  ignoreDefaultArgs: ['--enable-automation'],
171
214
  args: [
172
- '--disable-blink-features=AutomationControlled'
215
+ '--disable-blink-features=AutomationControlled',
216
+ '--disable-features=IsolateOrigins,site-per-process',
217
+ '--disable-infobars',
218
+ '--no-first-run',
219
+ '--no-default-browser-check',
173
220
  ]
174
221
  });
175
222
 
176
- await context.addInitScript(() => {
177
- Object.defineProperty(navigator, 'webdriver', {
178
- get: () => undefined,
179
- });
180
- });
223
+ await context.addInitScript(getStealthScript());
181
224
 
182
225
  activePage = await context.newPage();
183
226
 
@@ -586,15 +629,15 @@ export async function initPlaywrightForAccount(account: QwenAccount, headless =
586
629
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
587
630
  ignoreDefaultArgs: ['--enable-automation'],
588
631
  args: [
589
- '--disable-blink-features=AutomationControlled'
632
+ '--disable-blink-features=AutomationControlled',
633
+ '--disable-features=IsolateOrigins,site-per-process',
634
+ '--disable-infobars',
635
+ '--no-first-run',
636
+ '--no-default-browser-check',
590
637
  ]
591
638
  });
592
639
 
593
- await acctContext.addInitScript(() => {
594
- Object.defineProperty(navigator, 'webdriver', {
595
- get: () => undefined,
596
- });
597
- });
640
+ await acctContext.addInitScript(getStealthScript());
598
641
 
599
642
  const acctPage = await acctContext.newPage();
600
643
  accountContexts.set(account.id, acctContext);
@@ -618,15 +661,15 @@ export async function launchManualLoginAccount(accountId: string, browserType: B
618
661
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
619
662
  ignoreDefaultArgs: ['--enable-automation'],
620
663
  args: [
621
- '--disable-blink-features=AutomationControlled'
664
+ '--disable-blink-features=AutomationControlled',
665
+ '--disable-features=IsolateOrigins,site-per-process',
666
+ '--disable-infobars',
667
+ '--no-first-run',
668
+ '--no-default-browser-check',
622
669
  ]
623
670
  });
624
671
 
625
- await acctContext.addInitScript(() => {
626
- Object.defineProperty(navigator, 'webdriver', {
627
- get: () => undefined,
628
- });
629
- });
672
+ await acctContext.addInitScript(getStealthScript());
630
673
 
631
674
  const acctPage = await acctContext.newPage();
632
675
  await acctPage.goto('https://chat.qwen.ai/auth', { waitUntil: 'domcontentloaded' });
@@ -6,6 +6,20 @@ const CACHED_TIMEZONE = new Date().toString().split(' (')[0];
6
6
  const BASE_TIMEOUT_MS = 120000;
7
7
  const TIMEOUT_PER_MB = 30000;
8
8
 
9
+ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
10
+
11
+ function getClientHintsHeaders(): Record<string, string> {
12
+ return {
13
+ 'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
14
+ 'sec-ch-ua-mobile': '?0',
15
+ 'sec-ch-ua-platform': '"Windows"',
16
+ };
17
+ }
18
+
19
+ function getRandomDelay(): number {
20
+ return 30 + Math.floor(Math.random() * 80);
21
+ }
22
+
9
23
  export class RetryableQwenStreamError extends Error {
10
24
  readonly retryAfterMs: number;
11
25
  constructor(message: string, retryAfterMs: number) {
@@ -83,11 +97,13 @@ function cleanupStalePool(accountId: string) {
83
97
  }
84
98
 
85
99
  async function getBasicQwenHeaders(accountId?: string): Promise<Record<string, string>> {
86
- const { cookie, userAgent, bxV } = await getBasicHeaders(accountId);
100
+ const { cookie, userAgent, bxV, bxUa, bxUmidtoken } = await getBasicHeaders(accountId);
87
101
  return {
88
102
  cookie,
89
103
  'user-agent': userAgent,
90
104
  'bx-v': bxV,
105
+ 'bx-ua': bxUa || '',
106
+ 'bx-umidtoken': bxUmidtoken || '',
91
107
  };
92
108
  }
93
109
 
@@ -104,6 +120,9 @@ async function createRealQwenChat(header: Record<string, string>): Promise<strin
104
120
  'user-agent': header['user-agent'],
105
121
  'x-request-id': crypto.randomUUID(),
106
122
  'bx-v': header['bx-v'],
123
+ 'bx-ua': header['bx-ua'] || '',
124
+ 'bx-umidtoken': header['bx-umidtoken'] || '',
125
+ ...getClientHintsHeaders(),
107
126
  },
108
127
  body: JSON.stringify({
109
128
  title: 'Nova Conversa',
@@ -132,7 +151,8 @@ async function refillPoolForAccount(accountId: string) {
132
151
 
133
152
  let headers: Record<string, string>;
134
153
  try {
135
- headers = await getBasicQwenHeaders(accountId === 'global' ? undefined : accountId);
154
+ const acctId = accountId === 'global' ? undefined : accountId;
155
+ headers = await getBasicQwenHeaders(acctId);
136
156
  } catch (err) {
137
157
  console.error(`[WarmPool] header fetch failed for ${accountId}:`, (err as Error).message);
138
158
  return;
@@ -295,7 +315,7 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
295
315
  return cachedModels;
296
316
  }
297
317
 
298
- const { cookie, userAgent, bxV } = await getBasicHeaders(accountId);
318
+ const { cookie, userAgent, bxV, bxUa, bxUmidtoken } = await getBasicHeaders(accountId);
299
319
 
300
320
  const response = await fetch('https://chat.qwen.ai/api/models', {
301
321
  headers: {
@@ -306,8 +326,11 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
306
326
  'user-agent': userAgent,
307
327
  'x-request-id': crypto.randomUUID(),
308
328
  'bx-v': bxV,
329
+ 'bx-ua': bxUa || '',
330
+ 'bx-umidtoken': bxUmidtoken || '',
309
331
  'timezone': CACHED_TIMEZONE,
310
- 'source': 'web'
332
+ 'source': 'web',
333
+ ...getClientHintsHeaders(),
311
334
  }
312
335
  });
313
336
 
@@ -466,6 +489,7 @@ export async function createQwenStream(
466
489
  const url = `https://chat.qwen.ai/api/v2/chat/completions?chat_id=${chatId}`;
467
490
  const controller = new AbortController();
468
491
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
492
+ await sleep(getRandomDelay());
469
493
  const response = await fetch(url, {
470
494
  method: 'POST',
471
495
  headers: {
@@ -483,12 +507,82 @@ export async function createQwenStream(
483
507
  'x-accel-buffering': 'no',
484
508
  'x-request-id': crypto.randomUUID(),
485
509
  'bx-v': chatHeaders['bx-v'],
510
+ 'bx-ua': chatHeaders['bx-ua'] || '',
511
+ 'bx-umidtoken': chatHeaders['bx-umidtoken'] || '',
512
+ ...getClientHintsHeaders(),
486
513
  },
487
514
  body: payloadJson,
488
515
  signal: controller.signal
489
516
  });
490
517
  clearTimeout(timeoutId);
491
518
 
519
+ const responseContentType = response.headers.get('content-type') || '';
520
+ if (response.ok && responseContentType.includes('application/json') && response.body) {
521
+ const cloned = response.clone();
522
+ const peekText = await cloned.text().catch(() => '');
523
+ if (peekText.includes('FAIL_SYS_USER_VALIDATE') || peekText.includes('_____tmd_____') || peekText.includes('RGV587_ERROR')) {
524
+ console.warn('[Qwen] TMD challenge detected, refreshing headers and retrying...');
525
+ try {
526
+ const { headers: freshHeaders } = await getQwenHeaders(true, accountId);
527
+ await sleep(1000 + Math.floor(Math.random() * 2000));
528
+ const retryController = new AbortController();
529
+ const retryTimeoutId = setTimeout(() => retryController.abort(), timeoutMs);
530
+ const retryResponse = await fetch(url, {
531
+ method: 'POST',
532
+ headers: {
533
+ 'accept': 'application/json',
534
+ 'accept-language': 'pt-BR,pt;q=0.9',
535
+ 'content-type': 'application/json',
536
+ 'cookie': freshHeaders['cookie'],
537
+ 'origin': 'https://chat.qwen.ai',
538
+ 'referer': `https://chat.qwen.ai/c/${chatId}`,
539
+ 'sec-fetch-dest': 'empty',
540
+ 'sec-fetch-mode': 'cors',
541
+ 'sec-fetch-site': 'same-origin',
542
+ 'timezone': CACHED_TIMEZONE,
543
+ 'user-agent': freshHeaders['user-agent'],
544
+ 'x-accel-buffering': 'no',
545
+ 'x-request-id': crypto.randomUUID(),
546
+ 'bx-v': freshHeaders['bx-v'],
547
+ 'bx-ua': freshHeaders['bx-ua'] || '',
548
+ 'bx-umidtoken': freshHeaders['bx-umidtoken'] || '',
549
+ ...getClientHintsHeaders(),
550
+ },
551
+ body: payloadJson,
552
+ signal: retryController.signal
553
+ });
554
+ clearTimeout(retryTimeoutId);
555
+
556
+ const retryContentType = retryResponse.headers.get('content-type') || '';
557
+ if (retryResponse.ok && retryContentType.includes('text/event-stream') && retryResponse.body) {
558
+ return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: chatEntry.accountId };
559
+ }
560
+
561
+ const retryPeek = await retryResponse.clone().text().catch(() => '');
562
+ if (retryPeek.includes('FAIL_SYS_USER_VALIDATE') || retryPeek.includes('_____tmd_____')) {
563
+ throw new QwenUpstreamError(
564
+ 'Qwen TMD challenge persists after header refresh. The account may need manual captcha resolution.',
565
+ 'FAIL_SYS_USER_VALIDATE',
566
+ 403,
567
+ );
568
+ }
569
+
570
+ if (retryResponse.ok && retryResponse.body) {
571
+ return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: chatEntry.accountId };
572
+ }
573
+ } catch (retryErr) {
574
+ if (retryErr instanceof QwenUpstreamError) throw retryErr;
575
+ console.error('[Qwen] TMD retry failed:', (retryErr as Error).message);
576
+ }
577
+
578
+ throw new QwenUpstreamError(
579
+ 'Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.',
580
+ 'FAIL_SYS_USER_VALIDATE',
581
+ 403,
582
+ );
583
+ }
584
+ }
585
+
492
586
  if (!response.ok || !response.body) {
493
587
  const errText = await response.text().catch(() => '');
494
588
  const contentType = response.headers.get('content-type') || '';