@pedrofariasx/qwenproxy 1.6.3 → 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.3",
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,7 +558,7 @@ 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 });
@@ -751,7 +751,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
751
751
 
752
752
  console.log(`[Playwright] Waiting for chat input for ${cacheKey}...`);
753
753
  const inputSelector = 'textarea:visible, [contenteditable="true"]:visible';
754
- await page.waitForSelector(inputSelector, { timeout: 30000 }).catch(() => {
754
+ await page.waitForSelector(inputSelector, { timeout: config.timeouts.page }).catch(() => {
755
755
  console.error(`[Playwright] Chat input not found for ${cacheKey}. Current URL:`, page.url());
756
756
  throw new Error(`Timeout waiting for chat input for ${cacheKey}. Are you logged in?`);
757
757
  });
@@ -767,7 +767,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
767
767
  console.error('[Playwright] Failed to save error screenshot:', err.message);
768
768
  }
769
769
  reject(new Error(`Timeout waiting for Qwen headers for ${cacheKey}`));
770
- }, 60000);
770
+ }, config.timeouts.headers);
771
771
 
772
772
  console.log(`[Playwright] Setting up route interception for ${cacheKey}...`);
773
773
  const routeHandler = async (route: any, request: any) => {
@@ -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();