@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 +1 -1
- package/src/services/playwright.ts +61 -18
- package/src/services/qwen.ts +98 -4
package/package.json
CHANGED
|
@@ -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' });
|
package/src/services/qwen.ts
CHANGED
|
@@ -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
|
-
|
|
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') || '';
|