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