@pedrofariasx/qwenproxy 1.5.0 → 1.5.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 +111 -0
- package/src/services/qwen.ts +53 -16
package/package.json
CHANGED
|
@@ -355,7 +355,118 @@ async function loginToQwenUI(email: string, password: string): Promise<boolean>
|
|
|
355
355
|
return false;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
let guestContext: BrowserContext | null = null;
|
|
359
|
+
let guestPage: Page | null = null;
|
|
360
|
+
let guestHeadersCache: { headers: Record<string, string>, timestamp: number } | null = null;
|
|
361
|
+
const GUEST_HEADERS_TTL = 30 * 60 * 1000;
|
|
362
|
+
|
|
363
|
+
export async function getGuestHeaders(): Promise<Record<string, string>> {
|
|
364
|
+
if (guestHeadersCache && (Date.now() - guestHeadersCache.timestamp) < GUEST_HEADERS_TTL) {
|
|
365
|
+
return guestHeadersCache.headers;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!guestPage) {
|
|
369
|
+
const profilePath = path.resolve('qwen_profiles', '_guest');
|
|
370
|
+
const { engine, channel } = resolveBrowserEngine('chromium');
|
|
371
|
+
guestContext = await engine.launchPersistentContext(profilePath, {
|
|
372
|
+
headless: config.browser.headless,
|
|
373
|
+
channel,
|
|
374
|
+
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
|
|
375
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
376
|
+
args: ['--disable-blink-features=AutomationControlled', '--disable-features=IsolateOrigins,site-per-process', '--disable-infobars', '--no-first-run', '--no-default-browser-check']
|
|
377
|
+
});
|
|
378
|
+
await guestContext.addInitScript(getStealthScript());
|
|
379
|
+
guestPage = await guestContext.newPage();
|
|
380
|
+
|
|
381
|
+
await guestPage.goto('https://chat.qwen.ai/c/guest', { waitUntil: 'domcontentloaded' });
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const keepSessionBtn = await guestPage.$('button:has-text("Manter sessão terminada"), button:has-text("Keep session ended"), button:has-text("Manter sessão encerrada")');
|
|
385
|
+
if (keepSessionBtn) {
|
|
386
|
+
await keepSessionBtn.click();
|
|
387
|
+
console.log('[Playwright] Guest: Clicked "Manter sessão terminada"');
|
|
388
|
+
await sleep(1000);
|
|
389
|
+
}
|
|
390
|
+
} catch (e) {
|
|
391
|
+
// Modal might not be there
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return new Promise((resolve, reject) => {
|
|
396
|
+
const timeout = setTimeout(() => reject(new Error('Timeout getting guest headers')), 30000);
|
|
397
|
+
|
|
398
|
+
const routeHandler = async (route: any, request: any) => {
|
|
399
|
+
clearTimeout(timeout);
|
|
400
|
+
const reqHeaders = request.headers();
|
|
401
|
+
console.log('[Playwright] Guest intercepted request:', request.url());
|
|
402
|
+
|
|
403
|
+
const extractedHeaders = {
|
|
404
|
+
'cookie': reqHeaders['cookie'] || '',
|
|
405
|
+
'bx-ua': reqHeaders['bx-ua'] || '',
|
|
406
|
+
'bx-umidtoken': reqHeaders['bx-umidtoken'] || '',
|
|
407
|
+
'bx-v': reqHeaders['bx-v'] || '2.5.36',
|
|
408
|
+
'user-agent': reqHeaders['user-agent'] || 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
if (extractedHeaders['bx-ua']) {
|
|
412
|
+
console.log('[Playwright] Guest: Successfully captured bx-ua');
|
|
413
|
+
guestHeadersCache = { headers: extractedHeaders, timestamp: Date.now() };
|
|
414
|
+
await route.abort('aborted');
|
|
415
|
+
await guestPage!.unroute('**/api/v2/chat/completions*', routeHandler);
|
|
416
|
+
|
|
417
|
+
import('./qwen.js').then(m => m.disableNativeTools('guest').catch(() => {}));
|
|
418
|
+
|
|
419
|
+
resolve(extractedHeaders);
|
|
420
|
+
} else {
|
|
421
|
+
console.log('[Playwright] Guest: Request missing bx-ua, continuing route. Headers:', Object.keys(reqHeaders));
|
|
422
|
+
await route.continue();
|
|
423
|
+
// If it's the completions request and we still don't have bx-ua, we might need to resolve anyway
|
|
424
|
+
// or the UI interaction failed to trigger the SDK.
|
|
425
|
+
if (request.url().includes('/api/v2/chat/completions')) {
|
|
426
|
+
console.warn('[Playwright] Guest: Completions request made without bx-ua. Resolving with available headers.');
|
|
427
|
+
guestHeadersCache = { headers: extractedHeaders, timestamp: Date.now() };
|
|
428
|
+
await guestPage!.unroute('**/api/v2/chat/completions*', routeHandler);
|
|
429
|
+
resolve(extractedHeaders);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
guestPage!.route('**/api/v2/chat/completions*', routeHandler).then(async () => {
|
|
435
|
+
const inputSelector = 'textarea:visible, [contenteditable="true"]:visible';
|
|
436
|
+
try {
|
|
437
|
+
await guestPage!.waitForSelector(inputSelector, { timeout: 10000 });
|
|
438
|
+
await guestPage!.focus(inputSelector);
|
|
439
|
+
await guestPage!.fill(inputSelector, '');
|
|
440
|
+
await guestPage!.type(inputSelector, 'a', { delay: 50 });
|
|
441
|
+
await sleep(1000);
|
|
442
|
+
|
|
443
|
+
const selectors = ['.message-input-right-button-send .send-button', '.chat-prompt-send-button', 'button.send-button'];
|
|
444
|
+
let clicked = false;
|
|
445
|
+
for (const selector of selectors) {
|
|
446
|
+
const btn = await guestPage!.$(selector);
|
|
447
|
+
if (btn && await btn.isVisible()) {
|
|
448
|
+
await btn.click({ force: true, delay: 50 }).catch(() => {});
|
|
449
|
+
clicked = true;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!clicked) {
|
|
454
|
+
await guestPage!.keyboard.press('Enter');
|
|
455
|
+
}
|
|
456
|
+
} catch (e) {
|
|
457
|
+
clearTimeout(timeout);
|
|
458
|
+
reject(e);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
358
464
|
export async function getQwenHeaders(forceNew = false, accountId?: string): Promise<{ headers: Record<string, string>, chatSessionId: string, parentMessageId: string | null }> {
|
|
465
|
+
if (accountId === 'guest') {
|
|
466
|
+
const headers = await getGuestHeaders();
|
|
467
|
+
return { headers, chatSessionId: 'guest-session', parentMessageId: null };
|
|
468
|
+
}
|
|
469
|
+
|
|
359
470
|
const cacheKey = accountId || 'global';
|
|
360
471
|
const cache = getAccountHeaderCache(cacheKey);
|
|
361
472
|
|
package/src/services/qwen.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getQwenHeaders, getBasicHeaders } from './playwright.js';
|
|
1
|
+
import { getQwenHeaders, getBasicHeaders, getGuestHeaders } from './playwright.js';
|
|
2
2
|
import { MAX_PAYLOAD_SIZE } from '../core/model-registry.js';
|
|
3
3
|
import { markAccountRateLimited } from '../core/account-manager.js';
|
|
4
4
|
import crypto from 'crypto';
|
|
@@ -415,19 +415,56 @@ export async function createQwenStream(
|
|
|
415
415
|
files?: QwenFileEntry[],
|
|
416
416
|
pendingMultimodal?: Array<Array<{ type: string; text?: string; image_url?: { url: string }; video_url?: { url: string }; audio_url?: { url: string }; file_url?: { url: string } }>>
|
|
417
417
|
): Promise<{ stream: ReadableStream, headers: Record<string, string>, uiSessionId: string, controller: AbortController, accountId: string }> {
|
|
418
|
-
let
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
418
|
+
let chatId: string;
|
|
419
|
+
let chatHeaders: Record<string, string>;
|
|
420
|
+
|
|
421
|
+
if (accountId === 'guest') {
|
|
422
|
+
chatHeaders = await getGuestHeaders();
|
|
423
|
+
const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
|
|
424
|
+
method: 'POST',
|
|
425
|
+
headers: {
|
|
426
|
+
'accept': 'application/json, text/plain, */*',
|
|
427
|
+
'accept-language': 'pt-BR,pt;q=0.9',
|
|
428
|
+
'content-type': 'application/json',
|
|
429
|
+
cookie: chatHeaders['cookie'],
|
|
430
|
+
origin: 'https://chat.qwen.ai',
|
|
431
|
+
referer: 'https://chat.qwen.ai/c/guest',
|
|
432
|
+
'user-agent': chatHeaders['user-agent'],
|
|
433
|
+
'x-request-id': crypto.randomUUID(),
|
|
434
|
+
'bx-v': chatHeaders['bx-v'],
|
|
435
|
+
'bx-ua': chatHeaders['bx-ua'],
|
|
436
|
+
'bx-umidtoken': chatHeaders['bx-umidtoken'],
|
|
437
|
+
...getClientHintsHeaders(),
|
|
438
|
+
},
|
|
439
|
+
body: JSON.stringify({
|
|
440
|
+
title: 'Guest Chat',
|
|
441
|
+
models: [modelId.replace('-no-thinking', '')],
|
|
442
|
+
chat_mode: 'guest',
|
|
443
|
+
chat_type: 't2t',
|
|
444
|
+
timestamp: Date.now(),
|
|
445
|
+
project_id: '',
|
|
446
|
+
}),
|
|
447
|
+
signal: AbortSignal.timeout(30000),
|
|
448
|
+
});
|
|
449
|
+
if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
|
|
450
|
+
const json = await response.json();
|
|
451
|
+
chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
452
|
+
if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
453
|
+
} else {
|
|
454
|
+
let chatEntry: WarmPoolEntry;
|
|
455
|
+
try {
|
|
456
|
+
chatEntry = await getWarmedChat(accountId);
|
|
457
|
+
} catch (err: any) {
|
|
458
|
+
if (err.message?.includes('chat is in progress') || err.message?.includes('The chat is in progress')) {
|
|
459
|
+
const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
|
|
460
|
+
throw new RetryableQwenStreamError(`Qwen: ${err.message}`, retryAfterMs);
|
|
461
|
+
}
|
|
462
|
+
throw err;
|
|
425
463
|
}
|
|
426
|
-
|
|
464
|
+
chatId = chatEntry.chatId;
|
|
465
|
+
chatHeaders = chatEntry.headers;
|
|
427
466
|
}
|
|
428
467
|
|
|
429
|
-
const chatId = chatEntry.chatId;
|
|
430
|
-
const chatHeaders = chatEntry.headers;
|
|
431
468
|
const actualParentId: string | null = null;
|
|
432
469
|
|
|
433
470
|
// Process pending multimodal uploads — requires full headers with bx-ua/bx-umidtoken
|
|
@@ -473,7 +510,7 @@ export async function createQwenStream(
|
|
|
473
510
|
version: '2.1',
|
|
474
511
|
incremental_output: true,
|
|
475
512
|
chat_id: chatId,
|
|
476
|
-
chat_mode: 'normal',
|
|
513
|
+
chat_mode: accountId === 'guest' ? 'guest' : 'normal',
|
|
477
514
|
model: model,
|
|
478
515
|
parent_id: actualParentId,
|
|
479
516
|
messages: [
|
|
@@ -528,7 +565,7 @@ export async function createQwenStream(
|
|
|
528
565
|
'content-type': 'application/json',
|
|
529
566
|
'cookie': chatHeaders['cookie'],
|
|
530
567
|
'origin': 'https://chat.qwen.ai',
|
|
531
|
-
'referer': `https://chat.qwen.ai/c/${chatId}`,
|
|
568
|
+
'referer': accountId === 'guest' ? 'https://chat.qwen.ai/c/guest' : `https://chat.qwen.ai/c/${chatId}`,
|
|
532
569
|
'sec-fetch-dest': 'empty',
|
|
533
570
|
'sec-fetch-mode': 'cors',
|
|
534
571
|
'sec-fetch-site': 'same-origin',
|
|
@@ -584,7 +621,7 @@ export async function createQwenStream(
|
|
|
584
621
|
|
|
585
622
|
const retryContentType = retryResponse.headers.get('content-type') || '';
|
|
586
623
|
if (retryResponse.ok && retryContentType.includes('text/event-stream') && retryResponse.body) {
|
|
587
|
-
return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId:
|
|
624
|
+
return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: accountId || 'guest' };
|
|
588
625
|
}
|
|
589
626
|
|
|
590
627
|
const retryPeek = await retryResponse.clone().text().catch(() => '');
|
|
@@ -617,7 +654,7 @@ export async function createQwenStream(
|
|
|
617
654
|
} catch (e) {
|
|
618
655
|
if (e instanceof QwenUpstreamError) throw e;
|
|
619
656
|
}
|
|
620
|
-
return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId:
|
|
657
|
+
return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: accountId || 'guest' };
|
|
621
658
|
}
|
|
622
659
|
} catch (retryErr) {
|
|
623
660
|
if (retryErr instanceof QwenUpstreamError) throw retryErr;
|
|
@@ -703,5 +740,5 @@ export async function createQwenStream(
|
|
|
703
740
|
throw new Error(`Failed to fetch from Qwen: ${response.status} ${response.statusText} - ${errText}`);
|
|
704
741
|
}
|
|
705
742
|
|
|
706
|
-
return { stream: response.body, headers: chatHeaders, uiSessionId: chatId, controller, accountId:
|
|
743
|
+
return { stream: response.body, headers: chatHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
|
|
707
744
|
}
|