@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pedrofariasx/qwenproxy",
3
- "version": "1.5.0",
3
+ "version": "1.5.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": {
@@ -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
 
@@ -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 chatEntry: WarmPoolEntry;
419
- try {
420
- chatEntry = await getWarmedChat(accountId);
421
- } catch (err: any) {
422
- if (err.message?.includes('chat is in progress') || err.message?.includes('The chat is in progress')) {
423
- const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
424
- throw new RetryableQwenStreamError(`Qwen: ${err.message}`, retryAfterMs);
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
- throw err;
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: chatEntry.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: chatEntry.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: chatEntry.accountId };
743
+ return { stream: response.body, headers: chatHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
707
744
  }