@pedrofariasx/qwenproxy 1.3.0 → 1.3.1

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.1",
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 200 + Math.floor(Math.random() * 600);
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,19 @@ 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
+ try {
156
+ const { headers: fullHeaders } = await getQwenHeaders(false, acctId);
157
+ headers = {
158
+ cookie: fullHeaders['cookie'] || '',
159
+ 'user-agent': fullHeaders['user-agent'] || '',
160
+ 'bx-v': fullHeaders['bx-v'] || '',
161
+ 'bx-ua': fullHeaders['bx-ua'] || '',
162
+ 'bx-umidtoken': fullHeaders['bx-umidtoken'] || '',
163
+ };
164
+ } catch {
165
+ headers = await getBasicQwenHeaders(acctId);
166
+ }
136
167
  } catch (err) {
137
168
  console.error(`[WarmPool] header fetch failed for ${accountId}:`, (err as Error).message);
138
169
  return;
@@ -295,7 +326,7 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
295
326
  return cachedModels;
296
327
  }
297
328
 
298
- const { cookie, userAgent, bxV } = await getBasicHeaders(accountId);
329
+ const { cookie, userAgent, bxV, bxUa, bxUmidtoken } = await getBasicHeaders(accountId);
299
330
 
300
331
  const response = await fetch('https://chat.qwen.ai/api/models', {
301
332
  headers: {
@@ -306,8 +337,11 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
306
337
  'user-agent': userAgent,
307
338
  'x-request-id': crypto.randomUUID(),
308
339
  'bx-v': bxV,
340
+ 'bx-ua': bxUa || '',
341
+ 'bx-umidtoken': bxUmidtoken || '',
309
342
  'timezone': CACHED_TIMEZONE,
310
- 'source': 'web'
343
+ 'source': 'web',
344
+ ...getClientHintsHeaders(),
311
345
  }
312
346
  });
313
347
 
@@ -466,6 +500,7 @@ export async function createQwenStream(
466
500
  const url = `https://chat.qwen.ai/api/v2/chat/completions?chat_id=${chatId}`;
467
501
  const controller = new AbortController();
468
502
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
503
+ await sleep(getRandomDelay());
469
504
  const response = await fetch(url, {
470
505
  method: 'POST',
471
506
  headers: {
@@ -483,12 +518,82 @@ export async function createQwenStream(
483
518
  'x-accel-buffering': 'no',
484
519
  'x-request-id': crypto.randomUUID(),
485
520
  'bx-v': chatHeaders['bx-v'],
521
+ 'bx-ua': chatHeaders['bx-ua'] || '',
522
+ 'bx-umidtoken': chatHeaders['bx-umidtoken'] || '',
523
+ ...getClientHintsHeaders(),
486
524
  },
487
525
  body: payloadJson,
488
526
  signal: controller.signal
489
527
  });
490
528
  clearTimeout(timeoutId);
491
529
 
530
+ const responseContentType = response.headers.get('content-type') || '';
531
+ if (response.ok && responseContentType.includes('application/json') && response.body) {
532
+ const cloned = response.clone();
533
+ const peekText = await cloned.text().catch(() => '');
534
+ if (peekText.includes('FAIL_SYS_USER_VALIDATE') || peekText.includes('_____tmd_____') || peekText.includes('RGV587_ERROR')) {
535
+ console.warn('[Qwen] TMD challenge detected, refreshing headers and retrying...');
536
+ try {
537
+ const { headers: freshHeaders } = await getQwenHeaders(true, accountId);
538
+ await sleep(1000 + Math.floor(Math.random() * 2000));
539
+ const retryController = new AbortController();
540
+ const retryTimeoutId = setTimeout(() => retryController.abort(), timeoutMs);
541
+ const retryResponse = await fetch(url, {
542
+ method: 'POST',
543
+ headers: {
544
+ 'accept': 'application/json',
545
+ 'accept-language': 'pt-BR,pt;q=0.9',
546
+ 'content-type': 'application/json',
547
+ 'cookie': freshHeaders['cookie'],
548
+ 'origin': 'https://chat.qwen.ai',
549
+ 'referer': `https://chat.qwen.ai/c/${chatId}`,
550
+ 'sec-fetch-dest': 'empty',
551
+ 'sec-fetch-mode': 'cors',
552
+ 'sec-fetch-site': 'same-origin',
553
+ 'timezone': CACHED_TIMEZONE,
554
+ 'user-agent': freshHeaders['user-agent'],
555
+ 'x-accel-buffering': 'no',
556
+ 'x-request-id': crypto.randomUUID(),
557
+ 'bx-v': freshHeaders['bx-v'],
558
+ 'bx-ua': freshHeaders['bx-ua'] || '',
559
+ 'bx-umidtoken': freshHeaders['bx-umidtoken'] || '',
560
+ ...getClientHintsHeaders(),
561
+ },
562
+ body: payloadJson,
563
+ signal: retryController.signal
564
+ });
565
+ clearTimeout(retryTimeoutId);
566
+
567
+ const retryContentType = retryResponse.headers.get('content-type') || '';
568
+ if (retryResponse.ok && retryContentType.includes('text/event-stream') && retryResponse.body) {
569
+ return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: chatEntry.accountId };
570
+ }
571
+
572
+ const retryPeek = await retryResponse.clone().text().catch(() => '');
573
+ if (retryPeek.includes('FAIL_SYS_USER_VALIDATE') || retryPeek.includes('_____tmd_____')) {
574
+ throw new QwenUpstreamError(
575
+ 'Qwen TMD challenge persists after header refresh. The account may need manual captcha resolution.',
576
+ 'FAIL_SYS_USER_VALIDATE',
577
+ 403,
578
+ );
579
+ }
580
+
581
+ if (retryResponse.ok && retryResponse.body) {
582
+ return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: chatEntry.accountId };
583
+ }
584
+ } catch (retryErr) {
585
+ if (retryErr instanceof QwenUpstreamError) throw retryErr;
586
+ console.error('[Qwen] TMD retry failed:', (retryErr as Error).message);
587
+ }
588
+
589
+ throw new QwenUpstreamError(
590
+ 'Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.',
591
+ 'FAIL_SYS_USER_VALIDATE',
592
+ 403,
593
+ );
594
+ }
595
+ }
596
+
492
597
  if (!response.ok || !response.body) {
493
598
  const errText = await response.text().catch(() => '');
494
599
  const contentType = response.headers.get('content-type') || '';