@pedrofariasx/qwenproxy 1.6.2 → 1.6.4

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.6.2",
3
+ "version": "1.6.4",
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": {
@@ -8,9 +8,10 @@ const envSchema = z.object({
8
8
  USER_DATA_DIR: z.string().default('./qwen_profiles'),
9
9
  USER_AGENT: z.string().default('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'),
10
10
  LOG_CONSOLE: z.string().default('false'),
11
- NAVIGATION_TIMEOUT: z.string().default('30000'),
12
- PAGE_TIMEOUT: z.string().default('15000'),
13
- HTTP_TIMEOUT: z.string().default('10000'),
11
+ NAVIGATION_TIMEOUT: z.string().default('45000'),
12
+ PAGE_TIMEOUT: z.string().default('30000'),
13
+ HTTP_TIMEOUT: z.string().default('30000'),
14
+ HEADERS_TIMEOUT: z.string().default('60000'),
14
15
  CHAT_TIMEOUT: z.string().default('120000'),
15
16
  CACHE_TTL: z.string().default('3600'),
16
17
  RESPONSE_TTL: z.string().default('1800'),
@@ -59,6 +60,7 @@ export const config = {
59
60
  navigation: parseInt(env.NAVIGATION_TIMEOUT),
60
61
  page: parseInt(env.PAGE_TIMEOUT),
61
62
  http: parseInt(env.HTTP_TIMEOUT),
63
+ headers: parseInt(env.HEADERS_TIMEOUT),
62
64
  chat: parseInt(env.CHAT_TIMEOUT),
63
65
  },
64
66
  cache: {
@@ -385,7 +385,7 @@ async function checkValidSession(): Promise<boolean> {
385
385
  const cookies = await activePage.context().cookies();
386
386
  const hasAuthCookie = cookies.some(c => c.name.toLowerCase().includes('token') || c.name.toLowerCase().includes('session'));
387
387
  if (!hasAuthCookie) return false;
388
- await activePage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded', timeout: 10000 });
388
+ await activePage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded', timeout: config.timeouts.navigation });
389
389
  const isLogged = !activePage.url().includes('auth') && !activePage.url().includes('login');
390
390
  return isLogged;
391
391
  } catch {
@@ -450,7 +450,7 @@ async function loginToQwenUI(email: string, password: string): Promise<boolean>
450
450
  }
451
451
 
452
452
  try {
453
- await activePage.waitForSelector('input[type="email"], input[placeholder*="Email"]', { timeout: 5000 });
453
+ await activePage.waitForSelector('input[type="email"], input[placeholder*="Email"]', { timeout: config.timeouts.page });
454
454
  } catch {
455
455
  if (activePage.url().includes('/auth')) throw new Error('Email input not found');
456
456
  console.log('[Playwright] Already logged in');
@@ -462,7 +462,7 @@ async function loginToQwenUI(email: string, password: string): Promise<boolean>
462
462
  await activePage.keyboard.press('Enter');
463
463
  await sleep(1000);
464
464
 
465
- await activePage.waitForSelector('input[type="password"]', { timeout: 10000 });
465
+ await activePage.waitForSelector('input[type="password"]', { timeout: config.timeouts.page });
466
466
  console.log('[Playwright] UI: Filling password...');
467
467
  await activePage.fill('input[type="password"]', password);
468
468
  await activePage.keyboard.press('Enter');
@@ -502,7 +502,7 @@ export async function getGuestHeaders(): Promise<Record<string, string>> {
502
502
  await guestContext.addInitScript(getStealthScript());
503
503
  guestPage = await guestContext.newPage();
504
504
 
505
- await guestPage.goto('https://chat.qwen.ai/c/guest', { waitUntil: 'domcontentloaded' });
505
+ await guestPage.goto('https://chat.qwen.ai/c/guest', { waitUntil: 'domcontentloaded', timeout: config.timeouts.navigation });
506
506
 
507
507
  try {
508
508
  const keepSessionBtn = await guestPage.$('button:has-text("Manter sessão terminada"), button:has-text("Keep session ended"), button:has-text("Manter sessão encerrada")');
@@ -517,7 +517,7 @@ export async function getGuestHeaders(): Promise<Record<string, string>> {
517
517
  }
518
518
 
519
519
  return new Promise((resolve, reject) => {
520
- const timeout = setTimeout(() => reject(new Error('Timeout getting guest headers')), 30000);
520
+ const timeout = setTimeout(() => reject(new Error('Timeout getting guest headers')), config.timeouts.headers);
521
521
 
522
522
  const routeHandler = async (route: any, request: any) => {
523
523
  clearTimeout(timeout);
@@ -558,27 +558,25 @@ export async function getGuestHeaders(): Promise<Record<string, string>> {
558
558
  guestPage!.route('**/api/v2/chat/completions*', routeHandler).then(async () => {
559
559
  const inputSelector = 'textarea:visible, [contenteditable="true"]:visible';
560
560
  try {
561
- await guestPage!.waitForSelector(inputSelector, { timeout: 10000 });
561
+ await guestPage!.waitForSelector(inputSelector, { timeout: config.timeouts.page });
562
562
  await guestPage!.focus(inputSelector);
563
563
  await guestPage!.fill(inputSelector, '');
564
564
  await guestPage!.type(inputSelector, 'a', { delay: 50 });
565
- await sleep(1500);
565
+ await sleep(1000);
566
566
 
567
- // Try pressing Enter first as it is highly reliable
568
- await guestPage!.focus(inputSelector);
569
- await guestPage!.keyboard.press('Enter');
570
-
571
567
  const selectors = ['.message-input-right-button-send .send-button', '.chat-prompt-send-button', 'button.send-button'];
568
+ let clicked = false;
572
569
  for (const selector of selectors) {
573
- try {
574
- const btn = await guestPage!.$(selector);
575
- if (btn && await btn.isVisible()) {
576
- await btn.click({ force: true, delay: 50 }).catch(() => {});
577
- }
578
- } catch (e) {
579
- // ignore click errors
570
+ const btn = await guestPage!.$(selector);
571
+ if (btn && await btn.isVisible()) {
572
+ await btn.click({ force: true, delay: 50 }).catch(() => {});
573
+ clicked = true;
574
+ break;
580
575
  }
581
576
  }
577
+ if (!clicked) {
578
+ await guestPage!.keyboard.press('Enter');
579
+ }
582
580
  } catch (e) {
583
581
  clearTimeout(timeout);
584
582
  reject(e);
@@ -753,7 +751,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
753
751
 
754
752
  console.log(`[Playwright] Waiting for chat input for ${cacheKey}...`);
755
753
  const inputSelector = 'textarea:visible, [contenteditable="true"]:visible';
756
- await page.waitForSelector(inputSelector, { timeout: 30000 }).catch(() => {
754
+ await page.waitForSelector(inputSelector, { timeout: config.timeouts.page }).catch(() => {
757
755
  console.error(`[Playwright] Chat input not found for ${cacheKey}. Current URL:`, page.url());
758
756
  throw new Error(`Timeout waiting for chat input for ${cacheKey}. Are you logged in?`);
759
757
  });
@@ -769,7 +767,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
769
767
  console.error('[Playwright] Failed to save error screenshot:', err.message);
770
768
  }
771
769
  reject(new Error(`Timeout waiting for Qwen headers for ${cacheKey}`));
772
- }, 60000);
770
+ }, config.timeouts.headers);
773
771
 
774
772
  console.log(`[Playwright] Setting up route interception for ${cacheKey}...`);
775
773
  const routeHandler = async (route: any, request: any) => {
@@ -827,45 +825,47 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
827
825
  console.log(`[Playwright] Triggering request for ${cacheKey}...`);
828
826
  const inputSelector = 'textarea:visible, [contenteditable="true"]:visible';
829
827
 
830
- try {
831
- await page.focus(inputSelector);
832
- await page.fill(inputSelector, '');
833
- await page.type(inputSelector, 'a', { delay: 50 });
834
- console.log(`[Playwright] Typed char for ${cacheKey}, waiting for UI to update...`);
835
- await sleep(1500);
828
+ await page.focus(inputSelector);
829
+ await page.fill(inputSelector, '');
830
+ await page.type(inputSelector, 'a', { delay: 100 });
831
+ console.log(`[Playwright] Typed char for ${cacheKey}, waiting for UI to update...`);
832
+ await sleep(2000);
836
833
 
837
- // Try pressing Enter first on the input field as it is highly reliable
838
- console.log(`[Playwright] Pressing Enter for ${cacheKey}...`);
839
- await page.focus(inputSelector);
840
- await page.keyboard.press('Enter');
834
+ const selectors = [
835
+ '.message-input-right-button-send .send-button',
836
+ '.chat-prompt-send-button',
837
+ 'button.send-button'
838
+ ];
841
839
 
842
- // Also attempt to click the send button in case Enter didn't submit
843
- const selectors = [
844
- '.message-input-right-button-send .send-button',
845
- '.chat-prompt-send-button',
846
- 'button.send-button'
847
- ];
840
+ let clicked = false;
841
+ for (const selector of selectors) {
842
+ try {
843
+ const btn = await page.$(selector);
844
+ if (btn && await btn.isVisible()) {
845
+ console.log(`[Playwright] Attempting click on: ${selector}`);
846
+
847
+ await page.evaluate((sel) => {
848
+ const element = document.querySelector(sel) as HTMLElement;
849
+ if (element) {
850
+ element.focus();
851
+ element.click();
852
+ }
853
+ }, selector);
848
854
 
849
- for (const selector of selectors) {
850
- try {
851
- const btn = await page.$(selector);
852
- if (btn && await btn.isVisible()) {
853
- console.log(`[Playwright] Also attempting click on: ${selector}`);
854
- await page.evaluate((sel) => {
855
- const element = document.querySelector(sel) as HTMLElement;
856
- if (element) {
857
- element.focus();
858
- element.click();
859
- }
860
- }, selector);
861
- await btn.click({ force: true, delay: 50 }).catch(() => {});
862
- }
863
- } catch (e) {
864
- // ignore click errors
855
+ await btn.click({ force: true, delay: 50 }).catch(() => {});
856
+
857
+ clicked = true;
858
+ break;
865
859
  }
860
+ } catch (e) {
861
+ console.error(`[Playwright] Error clicking ${selector} for ${cacheKey}:`, e);
866
862
  }
867
- } catch (triggerErr: any) {
868
- console.error(`[Playwright] Failed to trigger request for ${cacheKey}:`, triggerErr.message);
863
+ }
864
+
865
+ if (!clicked) {
866
+ console.log(`[Playwright] No send button found/clicked for ${cacheKey}, fallback to Enter...`);
867
+ await page.focus(inputSelector);
868
+ await page.keyboard.press('Enter');
869
869
  }
870
870
  });
871
871
  });
@@ -906,13 +906,13 @@ export async function initPlaywrightForAccount(account: QwenAccount, headless =
906
906
 
907
907
  // Navigate to Qwen home to validate session and populate cookies
908
908
  try {
909
- await acctPage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded', timeout: 15000 });
909
+ await acctPage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded', timeout: config.timeouts.navigation });
910
910
  const url = acctPage.url();
911
911
  if (url.includes('auth') || url.includes('login')) {
912
912
  if (account.email && account.password) {
913
913
  console.log(`[Playwright] Session expired for ${account.email}, re-logging in...`);
914
914
  await loginToQwenWithContext(acctContext, acctPage, account.email, account.password);
915
- await acctPage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded', timeout: 15000 });
915
+ await acctPage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded', timeout: config.timeouts.navigation });
916
916
  } else {
917
917
  console.warn(`[Playwright] Session expired for account ${account.id} but no credentials available for re-login.`);
918
918
  }
@@ -1122,7 +1122,7 @@ export async function browserStreamFetch(
1122
1122
  streamCallbacks.delete(reqId);
1123
1123
  abortControllers.delete(reqId);
1124
1124
  metaResolve({ status: 0, statusText: 'Timeout', contentType: '', headers: {} });
1125
- }, options.timeoutMs || 130000);
1125
+ }, options.timeoutMs || config.timeouts.chat);
1126
1126
 
1127
1127
  streamCallbacks.set(reqId, {
1128
1128
  onMeta: (meta) => {
@@ -1,6 +1,7 @@
1
1
  import { getQwenHeaders, getBasicHeaders, getGuestHeaders, getPageForAccount, browserFetch, browserStreamFetch, CHROME_CLIENT_HINTS, CHROME_UA } from './playwright.js';
2
2
  import { MAX_PAYLOAD_SIZE } from '../core/model-registry.js';
3
3
  import { markAccountRateLimited } from '../core/account-manager.js';
4
+ import { config } from '../core/config.js';
4
5
  import crypto from 'crypto';
5
6
 
6
7
  const CACHED_TIMEZONE = new Date().toString().split(' (')[0];
@@ -132,7 +133,7 @@ async function createRealQwenChat(header: Record<string, string>, accountId?: st
132
133
  'timezone': CACHED_TIMEZONE,
133
134
  },
134
135
  body,
135
- timeoutMs: 30000,
136
+ timeoutMs: config.timeouts.http,
136
137
  });
137
138
 
138
139
  if (result.status === 429) {
@@ -176,7 +177,7 @@ async function createRealQwenChat(header: Record<string, string>, accountId?: st
176
177
  ...getClientHintsHeaders(),
177
178
  },
178
179
  body,
179
- signal: AbortSignal.timeout(30000),
180
+ signal: AbortSignal.timeout(config.timeouts.http),
180
181
  });
181
182
 
182
183
  if (!response.ok) {
@@ -200,6 +201,69 @@ async function createRealQwenChat(header: Record<string, string>, accountId?: st
200
201
  return chatId;
201
202
  }
202
203
 
204
+ async function fetchUnusedChats(headers: Record<string, string>, accountId?: string): Promise<string[]> {
205
+ const page = getPageForAccount(accountId);
206
+ const url = 'https://chat.qwen.ai/api/v2/chats/?page=1&exclude_project=true';
207
+ const reqHeaders: Record<string, string> = {
208
+ 'accept': 'application/json, text/plain, */*',
209
+ 'x-request-id': crypto.randomUUID(),
210
+ 'timezone': CACHED_TIMEZONE,
211
+ 'source': 'web',
212
+ };
213
+
214
+ let body = '';
215
+ if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
216
+ try {
217
+ const result = await browserFetch(page, url, {
218
+ method: 'GET',
219
+ headers: reqHeaders,
220
+ timeoutMs: config.timeouts.http,
221
+ });
222
+ if (result.status && result.status < 400) {
223
+ body = result.body;
224
+ }
225
+ } catch (err: any) {
226
+ console.warn('[WarmPool] browserFetch failed for chat list, falling back:', err.message);
227
+ }
228
+ }
229
+
230
+ if (!body) {
231
+ const response = await fetch(url, {
232
+ headers: {
233
+ 'accept': 'application/json, text/plain, */*',
234
+ 'accept-language': 'pt-BR,pt;q=0.9',
235
+ 'cookie': headers['cookie'],
236
+ 'referer': 'https://chat.qwen.ai/',
237
+ 'user-agent': headers['user-agent'],
238
+ 'x-request-id': crypto.randomUUID(),
239
+ 'bx-v': headers['bx-v'],
240
+ 'bx-ua': headers['bx-ua'] || '',
241
+ 'bx-umidtoken': headers['bx-umidtoken'] || '',
242
+ 'timezone': CACHED_TIMEZONE,
243
+ 'source': 'web',
244
+ ...getClientHintsHeaders(),
245
+ },
246
+ signal: AbortSignal.timeout(config.timeouts.http),
247
+ });
248
+ if (!response.ok) return [];
249
+ body = await response.text();
250
+ }
251
+
252
+ try {
253
+ const json = JSON.parse(body);
254
+ if (!json.success || !Array.isArray(json.data)) return [];
255
+ const unused: string[] = [];
256
+ for (const chat of json.data) {
257
+ if (chat.title === 'Nova Conversa' && chat.created_at === chat.updated_at) {
258
+ unused.push(chat.id);
259
+ }
260
+ }
261
+ return unused;
262
+ } catch {
263
+ return [];
264
+ }
265
+ }
266
+
203
267
  async function refillPoolForAccount(accountId: string) {
204
268
  let pool = warmPool.get(accountId);
205
269
  if (!pool) { pool = []; warmPool.set(accountId, pool); }
@@ -217,7 +281,27 @@ async function refillPoolForAccount(accountId: string) {
217
281
  }
218
282
 
219
283
  const acctId = accountId === 'global' ? undefined : accountId;
220
- for (let i = 0; i < need; i++) {
284
+ const existingIds = new Set(pool.map(e => e.chatId));
285
+
286
+ let reused = 0;
287
+ try {
288
+ const unusedChats = await fetchUnusedChats(headers, acctId);
289
+ for (const chatId of unusedChats) {
290
+ if (reused >= need) break;
291
+ if (existingIds.has(chatId)) continue;
292
+ pool.push({ chatId, headers, accountId, timestamp: Date.now() });
293
+ existingIds.add(chatId);
294
+ reused++;
295
+ }
296
+ if (reused > 0) {
297
+ console.log(`[WarmPool] Reused ${reused} existing unused chats for ${accountId}`);
298
+ }
299
+ } catch (err: any) {
300
+ console.warn(`[WarmPool] Failed to fetch unused chats for ${accountId}:`, err.message);
301
+ }
302
+
303
+ const stillNeed = Math.max(0, need - reused);
304
+ for (let i = 0; i < stillNeed; i++) {
221
305
  if (i > 0) {
222
306
  await sleep(800 + Math.floor(Math.random() * 2200));
223
307
  }
@@ -355,7 +439,7 @@ export async function disableNativeTools(accountId?: string): Promise<void> {
355
439
  'timezone': CACHED_TIMEZONE,
356
440
  },
357
441
  body: JSON.stringify(payload),
358
- timeoutMs: 30000,
442
+ timeoutMs: config.timeouts.http,
359
443
  });
360
444
  if (result.status && result.status < 400) {
361
445
  console.log(`[Qwen] Native tools disabled successfully for ${cacheKey}.`);
@@ -370,7 +454,7 @@ export async function disableNativeTools(accountId?: string): Promise<void> {
370
454
  }
371
455
 
372
456
  const controller = new AbortController();
373
- const timeoutId = setTimeout(() => controller.abort(), 30000);
457
+ const timeoutId = setTimeout(() => controller.abort(), config.timeouts.http);
374
458
  const response = await fetch('https://chat.qwen.ai/api/v2/users/user/settings/update', {
375
459
  method: 'POST',
376
460
  headers: {
@@ -422,7 +506,7 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
422
506
  'timezone': CACHED_TIMEZONE,
423
507
  'source': 'web',
424
508
  },
425
- timeoutMs: 30000,
509
+ timeoutMs: config.timeouts.http,
426
510
  });
427
511
  if (result.status && result.status < 400) {
428
512
  return processModelsJson(JSON.parse(result.body));
@@ -526,7 +610,7 @@ export async function createQwenStream(
526
610
  method: 'POST',
527
611
  headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', 'x-request-id': crypto.randomUUID(), 'timezone': CACHED_TIMEZONE },
528
612
  body: guestBody,
529
- timeoutMs: 30000,
613
+ timeoutMs: config.timeouts.http,
530
614
  });
531
615
  if (!result.status || result.status >= 400) throw new Error(`Failed to create guest chat: ${result.status}`);
532
616
  const json = JSON.parse(result.body);
@@ -538,7 +622,7 @@ export async function createQwenStream(
538
622
  method: 'POST',
539
623
  headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', cookie: chatHeaders['cookie'], origin: 'https://chat.qwen.ai', referer: 'https://chat.qwen.ai/c/guest', 'user-agent': chatHeaders['user-agent'], 'x-request-id': crypto.randomUUID(), 'bx-v': chatHeaders['bx-v'], 'bx-ua': chatHeaders['bx-ua'], 'bx-umidtoken': chatHeaders['bx-umidtoken'], ...getClientHintsHeaders() },
540
624
  body: guestBody,
541
- signal: AbortSignal.timeout(30000),
625
+ signal: AbortSignal.timeout(config.timeouts.http),
542
626
  });
543
627
  if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
544
628
  const json = await response.json();
@@ -550,7 +634,7 @@ export async function createQwenStream(
550
634
  method: 'POST',
551
635
  headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', cookie: chatHeaders['cookie'], origin: 'https://chat.qwen.ai', referer: 'https://chat.qwen.ai/c/guest', 'user-agent': chatHeaders['user-agent'], 'x-request-id': crypto.randomUUID(), 'bx-v': chatHeaders['bx-v'], 'bx-ua': chatHeaders['bx-ua'], 'bx-umidtoken': chatHeaders['bx-umidtoken'], ...getClientHintsHeaders() },
552
636
  body: guestBody,
553
- signal: AbortSignal.timeout(30000),
637
+ signal: AbortSignal.timeout(config.timeouts.http),
554
638
  });
555
639
  if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
556
640
  const json = await response.json();