@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 +1 -1
- package/src/core/config.ts +5 -3
- package/src/services/playwright.ts +11 -11
- package/src/services/qwen.ts +93 -9
package/package.json
CHANGED
package/src/core/config.ts
CHANGED
|
@@ -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('
|
|
12
|
-
PAGE_TIMEOUT: z.string().default('
|
|
13
|
-
HTTP_TIMEOUT: z.string().default('
|
|
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:
|
|
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:
|
|
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:
|
|
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')),
|
|
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:
|
|
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:
|
|
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
|
-
},
|
|
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:
|
|
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:
|
|
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 ||
|
|
1125
|
+
}, options.timeoutMs || config.timeouts.chat);
|
|
1126
1126
|
|
|
1127
1127
|
streamCallbacks.set(reqId, {
|
|
1128
1128
|
onMeta: (meta) => {
|
package/src/services/qwen.ts
CHANGED
|
@@ -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:
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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(),
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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();
|