@pedrofariasx/qwenproxy 1.1.0
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/LICENSE +13 -0
- package/README.md +292 -0
- package/bin/qwenproxy.mjs +11 -0
- package/package.json +56 -0
- package/src/api/models.ts +183 -0
- package/src/api/server.ts +126 -0
- package/src/cache/memory-cache.ts +186 -0
- package/src/core/account-manager.ts +132 -0
- package/src/core/accounts.ts +78 -0
- package/src/core/config.ts +91 -0
- package/src/core/database.ts +92 -0
- package/src/core/logger.ts +96 -0
- package/src/core/metrics.ts +169 -0
- package/src/core/model-registry.ts +30 -0
- package/src/core/stream-registry.ts +40 -0
- package/src/core/watchdog.ts +130 -0
- package/src/index.ts +7 -0
- package/src/linter/extraction-engine.ts +165 -0
- package/src/linter/index.ts +258 -0
- package/src/linter/repair-normalize.ts +245 -0
- package/src/linter/safety-gate.ts +219 -0
- package/src/linter/streaming-state-machine.ts +252 -0
- package/src/linter/structural-parser.ts +352 -0
- package/src/linter/types.ts +74 -0
- package/src/login.ts +228 -0
- package/src/routes/chat.ts +801 -0
- package/src/routes/upload.ts +700 -0
- package/src/services/playwright.ts +778 -0
- package/src/services/qwen.ts +500 -0
- package/src/tests/advanced.test.ts +227 -0
- package/src/tests/agenticStress.test.ts +360 -0
- package/src/tests/concurrency.test.ts +103 -0
- package/src/tests/concurrentChat.test.ts +71 -0
- package/src/tests/delta.test.ts +63 -0
- package/src/tests/index.test.ts +356 -0
- package/src/tests/jsonFix.test.ts +98 -0
- package/src/tests/linter.test.ts +151 -0
- package/src/tests/parallel.test.ts +42 -0
- package/src/tests/parser.test.ts +89 -0
- package/src/tests/rotation.test.ts +45 -0
- package/src/tests/streamingOptimizations.test.ts +328 -0
- package/src/tests/structureVerification.test.ts +176 -0
- package/src/tools/ast.ts +15 -0
- package/src/tools/coercion.ts +67 -0
- package/src/tools/confidence.ts +48 -0
- package/src/tools/detector.ts +40 -0
- package/src/tools/executor.ts +236 -0
- package/src/tools/parser.ts +446 -0
- package/src/tools/pipeline.ts +122 -0
- package/src/tools/registry-runtime.ts +34 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/repair.ts +42 -0
- package/src/tools/schema.ts +285 -0
- package/src/tools/types.ts +104 -0
- package/src/tools/validator.ts +33 -0
- package/src/utils/context-truncation.ts +61 -0
- package/src/utils/json.ts +114 -0
- package/src/utils/qwen-stream-parser.ts +286 -0
- package/src/utils/types.ts +101 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* File: playwright.ts
|
|
3
|
+
* Project: qwenproxy
|
|
4
|
+
* Author: Pedro Farias
|
|
5
|
+
* Created: 2026-05-09
|
|
6
|
+
*
|
|
7
|
+
* Last Modified: Sat May 09 2026
|
|
8
|
+
* Modified By: Pedro Farias
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { chromium, firefox, webkit, BrowserContext, Page } from 'playwright';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import { QwenAccount } from '../core/accounts.ts';
|
|
15
|
+
import { config } from '../core/config.ts';
|
|
16
|
+
|
|
17
|
+
export type BrowserType = 'chromium' | 'firefox' | 'webkit' | 'chrome' | 'edge';
|
|
18
|
+
|
|
19
|
+
let context: BrowserContext | null = null;
|
|
20
|
+
export let activePage: Page | null = null;
|
|
21
|
+
const accountContexts = new Map<string, BrowserContext>();
|
|
22
|
+
const accountPages = new Map<string, Page>();
|
|
23
|
+
|
|
24
|
+
interface AccountHeaderCache {
|
|
25
|
+
currentHeaders: Record<string, string>;
|
|
26
|
+
cachedQwenHeaders: { headers: Record<string, string>, chatSessionId: string, parentMessageId: string | null } | null;
|
|
27
|
+
lastHeadersTime: number;
|
|
28
|
+
refreshInProgress: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const accountHeaderCaches = new Map<string, AccountHeaderCache>();
|
|
32
|
+
|
|
33
|
+
function getAccountHeaderCache(accountId: string): AccountHeaderCache {
|
|
34
|
+
let cache = accountHeaderCaches.get(accountId);
|
|
35
|
+
if (!cache) {
|
|
36
|
+
cache = {
|
|
37
|
+
currentHeaders: {},
|
|
38
|
+
cachedQwenHeaders: null,
|
|
39
|
+
lastHeadersTime: 0,
|
|
40
|
+
refreshInProgress: false,
|
|
41
|
+
};
|
|
42
|
+
accountHeaderCaches.set(accountId, cache);
|
|
43
|
+
}
|
|
44
|
+
return cache;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const HEADERS_TTL = 60 * 60 * 1000;
|
|
48
|
+
const COOKIE_CACHE_TTL = 5 * 60 * 1000;
|
|
49
|
+
const cookieCaches = new Map<string, { cookie: string, timestamp: number }>();
|
|
50
|
+
const REFRESH_THRESHOLD = 0.7;
|
|
51
|
+
|
|
52
|
+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
53
|
+
|
|
54
|
+
export class Mutex {
|
|
55
|
+
private queue: (() => void)[] = [];
|
|
56
|
+
private locked = false;
|
|
57
|
+
|
|
58
|
+
async acquire(): Promise<() => void> {
|
|
59
|
+
if (!this.locked) {
|
|
60
|
+
this.locked = true;
|
|
61
|
+
return () => this.release();
|
|
62
|
+
}
|
|
63
|
+
return new Promise<() => void>(resolve => {
|
|
64
|
+
this.queue.push(() => {
|
|
65
|
+
resolve(() => this.release());
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private release(): void {
|
|
71
|
+
const next = this.queue.shift();
|
|
72
|
+
if (next) {
|
|
73
|
+
next();
|
|
74
|
+
} else {
|
|
75
|
+
this.locked = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const uiMutexes = new Map<string, Mutex>();
|
|
81
|
+
function getUiMutex(accountId: string): Mutex {
|
|
82
|
+
let m = uiMutexes.get(accountId);
|
|
83
|
+
if (!m) {
|
|
84
|
+
m = new Mutex();
|
|
85
|
+
uiMutexes.set(accountId, m);
|
|
86
|
+
}
|
|
87
|
+
return m;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function getCookies(accountId?: string): Promise<string> {
|
|
91
|
+
if (process.env.TEST_MOCK_PLAYWRIGHT) return 'token=mock';
|
|
92
|
+
const cacheKey = accountId || 'global';
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const cached = cookieCaches.get(cacheKey);
|
|
95
|
+
if (cached && (now - cached.timestamp) < COOKIE_CACHE_TTL) {
|
|
96
|
+
return cached.cookie;
|
|
97
|
+
}
|
|
98
|
+
const page = accountId ? accountPages.get(accountId) : activePage;
|
|
99
|
+
if (!page) return '';
|
|
100
|
+
const cookies = await page.context().cookies();
|
|
101
|
+
const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
102
|
+
cookieCaches.set(cacheKey, { cookie: cookieStr, timestamp: now });
|
|
103
|
+
return cookieStr;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function getBasicHeaders(accountId?: string): Promise<{ cookie: string, userAgent: string, bxV: string, bxUa?: string, bxUmidtoken?: string }> {
|
|
107
|
+
if (process.env.TEST_MOCK_PLAYWRIGHT) return { cookie: 'token=mock', userAgent: 'mock', bxV: '2.5.36' };
|
|
108
|
+
|
|
109
|
+
let page = accountId ? accountPages.get(accountId) : activePage;
|
|
110
|
+
if (accountId && !page) {
|
|
111
|
+
const { getAccountCredentials } = await import('../core/accounts.ts');
|
|
112
|
+
const creds = getAccountCredentials(accountId);
|
|
113
|
+
if (creds) {
|
|
114
|
+
await initPlaywrightForAccount(creds, config.browser.headless);
|
|
115
|
+
page = accountPages.get(accountId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!page) throw new Error('Playwright not initialized');
|
|
120
|
+
|
|
121
|
+
const cookie = await getCookies(accountId);
|
|
122
|
+
const userAgent = await page.evaluate(() => navigator.userAgent);
|
|
123
|
+
|
|
124
|
+
const cacheKey = accountId || 'global';
|
|
125
|
+
const cache = getAccountHeaderCache(cacheKey);
|
|
126
|
+
const bxV = cache.currentHeaders['bx-v'] || '2.5.36';
|
|
127
|
+
const bxUa = cache.currentHeaders['bx-ua'];
|
|
128
|
+
const bxUmidtoken = cache.currentHeaders['bx-umidtoken'];
|
|
129
|
+
|
|
130
|
+
return { cookie, userAgent, bxV, bxUa, bxUmidtoken };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function initPlaywright(headless = true, browserType: BrowserType = 'chromium') {
|
|
134
|
+
if (process.env.TEST_MOCK_PLAYWRIGHT) return;
|
|
135
|
+
if (context) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const profilePath = path.resolve('qwen_profiles', '_default');
|
|
140
|
+
|
|
141
|
+
let browserEngine;
|
|
142
|
+
let channel: string | undefined;
|
|
143
|
+
|
|
144
|
+
switch (browserType) {
|
|
145
|
+
case 'firefox':
|
|
146
|
+
browserEngine = firefox;
|
|
147
|
+
break;
|
|
148
|
+
case 'webkit':
|
|
149
|
+
browserEngine = webkit;
|
|
150
|
+
break;
|
|
151
|
+
case 'chrome':
|
|
152
|
+
browserEngine = chromium;
|
|
153
|
+
channel = 'chrome';
|
|
154
|
+
break;
|
|
155
|
+
case 'edge':
|
|
156
|
+
browserEngine = chromium;
|
|
157
|
+
channel = 'msedge';
|
|
158
|
+
break;
|
|
159
|
+
case 'chromium':
|
|
160
|
+
default:
|
|
161
|
+
browserEngine = chromium;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(`[Playwright] Launching ${browserType}...`);
|
|
166
|
+
|
|
167
|
+
context = await browserEngine.launchPersistentContext(profilePath, {
|
|
168
|
+
headless,
|
|
169
|
+
channel,
|
|
170
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
|
171
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
172
|
+
args: [
|
|
173
|
+
'--disable-blink-features=AutomationControlled'
|
|
174
|
+
]
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await context.addInitScript(() => {
|
|
178
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
179
|
+
get: () => undefined,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
activePage = await context.newPage();
|
|
184
|
+
|
|
185
|
+
const hasCredentials = !!(process.env.QWEN_EMAIL && process.env.QWEN_PASSWORD);
|
|
186
|
+
const hasValidSession = await checkValidSession();
|
|
187
|
+
|
|
188
|
+
if (!hasValidSession && !hasCredentials) {
|
|
189
|
+
console.warn('[Playwright] No valid session AND no credentials in .env. Manual login will be required.');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!hasValidSession) {
|
|
193
|
+
await attemptAutoLogin();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function checkValidSession(): Promise<boolean> {
|
|
198
|
+
if (!activePage) return false;
|
|
199
|
+
try {
|
|
200
|
+
const cookies = await activePage.context().cookies();
|
|
201
|
+
const hasAuthCookie = cookies.some(c => c.name.toLowerCase().includes('token') || c.name.toLowerCase().includes('session'));
|
|
202
|
+
if (!hasAuthCookie) return false;
|
|
203
|
+
await activePage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
204
|
+
const isLogged = !activePage.url().includes('auth') && !activePage.url().includes('login');
|
|
205
|
+
return isLogged;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function attemptAutoLogin(): Promise<void> {
|
|
212
|
+
const email = process.env.QWEN_EMAIL;
|
|
213
|
+
const password = process.env.QWEN_PASSWORD;
|
|
214
|
+
if (!email || !password) return;
|
|
215
|
+
console.log('[Playwright] Attempting auto-login with credentials from .env...');
|
|
216
|
+
try {
|
|
217
|
+
const success = await loginToQwen(email, password);
|
|
218
|
+
if (success) {
|
|
219
|
+
console.log('[Playwright] Auto-login successful.');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
console.warn('[Playwright] API login failed, trying UI fallback...');
|
|
223
|
+
const uiSuccess = await loginToQwenUI(email, password);
|
|
224
|
+
if (uiSuccess) {
|
|
225
|
+
console.log('[Playwright] UI login fallback successful.');
|
|
226
|
+
} else {
|
|
227
|
+
console.warn('[Playwright] Both API and UI login failed. Manual login may be required.');
|
|
228
|
+
}
|
|
229
|
+
} catch (err: any) {
|
|
230
|
+
console.error('[Playwright] Auto-login error:', err.message);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function closePlaywright() {
|
|
235
|
+
if (process.env.TEST_MOCK_PLAYWRIGHT) return;
|
|
236
|
+
for (const cache of accountHeaderCaches.values()) {
|
|
237
|
+
cache.refreshInProgress = false;
|
|
238
|
+
}
|
|
239
|
+
if (context) {
|
|
240
|
+
await context.close();
|
|
241
|
+
context = null;
|
|
242
|
+
activePage = null;
|
|
243
|
+
}
|
|
244
|
+
for (const acctId of accountContexts.keys()) {
|
|
245
|
+
await closePlaywrightForAccount(acctId);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function loginToQwen(email: string, password: string): Promise<boolean> {
|
|
250
|
+
if (!activePage) throw new Error('Playwright not initialized');
|
|
251
|
+
|
|
252
|
+
console.log(`[Playwright] Attempting API login for ${email}...`);
|
|
253
|
+
|
|
254
|
+
await activePage.goto('https://chat.qwen.ai/auth', { waitUntil: 'domcontentloaded' });
|
|
255
|
+
|
|
256
|
+
const hashedPassword = crypto.createHash('sha256').update(password).digest('hex');
|
|
257
|
+
|
|
258
|
+
const result = await activePage.evaluate(async ({ email, password }) => {
|
|
259
|
+
try {
|
|
260
|
+
const response = await fetch("https://chat.qwen.ai/api/v2/auths/signin", {
|
|
261
|
+
method: "POST",
|
|
262
|
+
headers: {
|
|
263
|
+
"accept": "application/json, text/plain, */*",
|
|
264
|
+
"content-type": "application/json",
|
|
265
|
+
"source": "web",
|
|
266
|
+
"timezone": new Date().toString().split(' (')[0],
|
|
267
|
+
"x-request-id": crypto.randomUUID()
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify({ email, password, login_type: "email" })
|
|
270
|
+
});
|
|
271
|
+
const data = await response.json();
|
|
272
|
+
return { ok: response.ok, data };
|
|
273
|
+
} catch (e: any) {
|
|
274
|
+
return { ok: false, error: e.message };
|
|
275
|
+
}
|
|
276
|
+
}, { email, password: hashedPassword });
|
|
277
|
+
|
|
278
|
+
if (result.ok) {
|
|
279
|
+
console.log('[Playwright] API login request successful.');
|
|
280
|
+
await activePage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded' });
|
|
281
|
+
const isLogged = !(activePage.url().includes('auth') || activePage.url().includes('login'));
|
|
282
|
+
if (isLogged) {
|
|
283
|
+
console.log('[Playwright] Login confirmed.');
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.error('[Playwright] Login failed:', result.data || result.error);
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function loginToQwenUI(email: string, password: string): Promise<boolean> {
|
|
293
|
+
if (!activePage) throw new Error('Playwright not initialized');
|
|
294
|
+
|
|
295
|
+
console.log('[Playwright] Attempting UI login...');
|
|
296
|
+
await activePage.goto('https://chat.qwen.ai/auth', { waitUntil: 'domcontentloaded' });
|
|
297
|
+
await sleep(2000);
|
|
298
|
+
|
|
299
|
+
if (!activePage.url().includes('/auth')) {
|
|
300
|
+
console.log('[Playwright] Already logged in');
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await activePage.waitForSelector('input[type="email"], input[placeholder*="Email"]', { timeout: 5000 });
|
|
306
|
+
} catch {
|
|
307
|
+
if (activePage.url().includes('/auth')) throw new Error('Email input not found');
|
|
308
|
+
console.log('[Playwright] Already logged in');
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
console.log('[Playwright] UI: Filling email...');
|
|
313
|
+
await activePage.fill('input[type="email"], input[placeholder*="Email"]', email);
|
|
314
|
+
await activePage.keyboard.press('Enter');
|
|
315
|
+
await sleep(1000);
|
|
316
|
+
|
|
317
|
+
await activePage.waitForSelector('input[type="password"]', { timeout: 10000 });
|
|
318
|
+
console.log('[Playwright] UI: Filling password...');
|
|
319
|
+
await activePage.fill('input[type="password"]', password);
|
|
320
|
+
await activePage.keyboard.press('Enter');
|
|
321
|
+
|
|
322
|
+
await sleep(2000);
|
|
323
|
+
|
|
324
|
+
const isLogged = !activePage.url().includes('auth') && !activePage.url().includes('login');
|
|
325
|
+
if (isLogged) {
|
|
326
|
+
console.log('[Playwright] UI login OK');
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log('[Playwright] UI login failed');
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function getQwenHeaders(forceNew = false, accountId?: string): Promise<{ headers: Record<string, string>, chatSessionId: string, parentMessageId: string | null }> {
|
|
335
|
+
const cacheKey = accountId || 'global';
|
|
336
|
+
const cache = getAccountHeaderCache(cacheKey);
|
|
337
|
+
|
|
338
|
+
if (!forceNew && cache.cachedQwenHeaders) {
|
|
339
|
+
const age = Date.now() - cache.lastHeadersTime;
|
|
340
|
+
if (age < HEADERS_TTL) {
|
|
341
|
+
if (age > HEADERS_TTL * REFRESH_THRESHOLD && !cache.refreshInProgress) {
|
|
342
|
+
cache.refreshInProgress = true;
|
|
343
|
+
getQwenHeaders(true, accountId).finally(() => {
|
|
344
|
+
cache.refreshInProgress = false;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return cache.cachedQwenHeaders;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const release = await getUiMutex(cacheKey).acquire();
|
|
352
|
+
try {
|
|
353
|
+
if (!forceNew && cache.cachedQwenHeaders && (Date.now() - cache.lastHeadersTime < HEADERS_TTL)) {
|
|
354
|
+
return cache.cachedQwenHeaders;
|
|
355
|
+
}
|
|
356
|
+
return await _getQwenHeadersInternal(forceNew, accountId);
|
|
357
|
+
} finally {
|
|
358
|
+
release();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Lightweight cookie/cookie refresh via direct API call instead of full browser automation.
|
|
364
|
+
* This attempts to extract cookies from the page context without triggering route interception.
|
|
365
|
+
*/
|
|
366
|
+
async function tryLightweightCookieRefresh(accountId?: string): Promise<{ headers: Record<string, string>, chatSessionId: string, parentMessageId: string | null } | null> {
|
|
367
|
+
const cacheKey = accountId || 'global';
|
|
368
|
+
const cache = getAccountHeaderCache(cacheKey);
|
|
369
|
+
|
|
370
|
+
const page = accountId ? accountPages.get(accountId) : activePage;
|
|
371
|
+
if (!page) return null;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const cookies = await page.context().cookies();
|
|
375
|
+
const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
376
|
+
const userAgent = await page.evaluate(() => navigator.userAgent);
|
|
377
|
+
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
cookieCaches.set(cacheKey, { cookie: cookieStr, timestamp: now });
|
|
380
|
+
|
|
381
|
+
if (cache.cachedQwenHeaders && cache.currentHeaders.cookie) {
|
|
382
|
+
const updatedHeaders = {
|
|
383
|
+
...cache.cachedQwenHeaders.headers,
|
|
384
|
+
cookie: cookieStr,
|
|
385
|
+
'user-agent': userAgent,
|
|
386
|
+
};
|
|
387
|
+
cache.cachedQwenHeaders = {
|
|
388
|
+
...cache.cachedQwenHeaders,
|
|
389
|
+
headers: updatedHeaders,
|
|
390
|
+
};
|
|
391
|
+
cache.lastHeadersTime = now;
|
|
392
|
+
cache.currentHeaders = {
|
|
393
|
+
...cache.currentHeaders,
|
|
394
|
+
cookie: cookieStr,
|
|
395
|
+
'user-agent': userAgent,
|
|
396
|
+
};
|
|
397
|
+
return cache.cachedQwenHeaders;
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
// Lightweight refresh failed, fall back to full interception
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Promise<{ headers: Record<string, string>, chatSessionId: string, parentMessageId: string | null }> {
|
|
407
|
+
const cacheKey = accountId || 'global';
|
|
408
|
+
const cache = getAccountHeaderCache(cacheKey);
|
|
409
|
+
|
|
410
|
+
if (process.env.TEST_MOCK_PLAYWRIGHT) {
|
|
411
|
+
const mockSessionId = process.env.TEST_SESSION_ID || 'mock-session';
|
|
412
|
+
return {
|
|
413
|
+
headers: {
|
|
414
|
+
'authorization': 'Bearer MOCK',
|
|
415
|
+
'cookie': 'token=mock',
|
|
416
|
+
'user-agent': 'mock',
|
|
417
|
+
'bx-v': '2.5.36'
|
|
418
|
+
},
|
|
419
|
+
chatSessionId: mockSessionId,
|
|
420
|
+
parentMessageId: null
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// If headers are cached and not forceNew, try lightweight cookie refresh first
|
|
425
|
+
if (!forceNew && cache.cachedQwenHeaders) {
|
|
426
|
+
const lightResult = await tryLightweightCookieRefresh(accountId);
|
|
427
|
+
if (lightResult) {
|
|
428
|
+
return lightResult;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (accountId && !accountPages.has(accountId)) {
|
|
433
|
+
const { getAccountCredentials } = await import('../core/accounts.ts');
|
|
434
|
+
const creds = getAccountCredentials(accountId);
|
|
435
|
+
if (creds) {
|
|
436
|
+
await initPlaywrightForAccount(creds, config.browser.headless);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const page = accountId ? accountPages.get(accountId) : activePage;
|
|
441
|
+
if (!page) {
|
|
442
|
+
throw new Error(`Playwright not initialized for account: ${cacheKey}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const currentUrl = page.url();
|
|
446
|
+
const isOnQwen = currentUrl.includes('chat.qwen.ai');
|
|
447
|
+
const isOnSpecificChat = isOnQwen && /\/c\//.test(currentUrl);
|
|
448
|
+
|
|
449
|
+
if (!isOnQwen || forceNew || isOnSpecificChat) {
|
|
450
|
+
console.log(`[Playwright] Navigating to Qwen home for ${cacheKey}... (Current: ${currentUrl})`);
|
|
451
|
+
await page.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded' });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const isLoginPage = page.url().includes('login') || (await page.$('input[type="email"], input[placeholder*="Email"]'));
|
|
455
|
+
if (isLoginPage) {
|
|
456
|
+
if (!accountId) {
|
|
457
|
+
const email = process.env.QWEN_EMAIL;
|
|
458
|
+
const password = process.env.QWEN_PASSWORD;
|
|
459
|
+
|
|
460
|
+
if (email && password) {
|
|
461
|
+
console.log('[Playwright] Detected login page. Attempting automated login...');
|
|
462
|
+
try {
|
|
463
|
+
const loggedIn = await loginToQwen(email, password);
|
|
464
|
+
if (!loggedIn) {
|
|
465
|
+
throw new Error('loginToQwen returned false');
|
|
466
|
+
}
|
|
467
|
+
console.log('[Playwright] Automated login successful.');
|
|
468
|
+
} catch (err: any) {
|
|
469
|
+
console.error('[Playwright] Automated login failed:', err.message);
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
console.warn('[Playwright] Detected login page but QWEN_EMAIL/PASSWORD not provided in .env');
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
const { getAccountCredentials } = await import('../core/accounts.ts');
|
|
476
|
+
const creds = getAccountCredentials(accountId);
|
|
477
|
+
if (creds && creds.email && creds.password) {
|
|
478
|
+
console.log(`[Playwright] Detected login page for account ${creds.email}. Attempting login...`);
|
|
479
|
+
const acctContext = accountContexts.get(accountId);
|
|
480
|
+
if (acctContext) {
|
|
481
|
+
await loginToQwenWithContext(acctContext, page, creds.email, creds.password);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
console.log(`[Playwright] Waiting for chat input for ${cacheKey}...`);
|
|
488
|
+
const inputSelector = 'textarea:visible, [contenteditable="true"]:visible';
|
|
489
|
+
await page.waitForSelector(inputSelector, { timeout: 30000 }).catch(() => {
|
|
490
|
+
console.error(`[Playwright] Chat input not found for ${cacheKey}. Current URL:`, page.url());
|
|
491
|
+
throw new Error(`Timeout waiting for chat input for ${cacheKey}. Are you logged in?`);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
return new Promise((resolve, reject) => {
|
|
495
|
+
const timeout = setTimeout(async () => {
|
|
496
|
+
console.error(`[Playwright] Timeout waiting for Qwen headers for ${cacheKey}. Current URL:`, page.url());
|
|
497
|
+
try {
|
|
498
|
+
const screenshotPath = path.resolve(`qwen_profiles/error_${cacheKey}.png`);
|
|
499
|
+
await page.screenshot({ path: screenshotPath });
|
|
500
|
+
console.log(`[Playwright] Error screenshot saved to ${screenshotPath}`);
|
|
501
|
+
} catch (err: any) {
|
|
502
|
+
console.error('[Playwright] Failed to save error screenshot:', err.message);
|
|
503
|
+
}
|
|
504
|
+
reject(new Error(`Timeout waiting for Qwen headers for ${cacheKey}`));
|
|
505
|
+
}, 60000);
|
|
506
|
+
|
|
507
|
+
console.log(`[Playwright] Setting up route interception for ${cacheKey}...`);
|
|
508
|
+
const routeHandler = async (route: any, request: any) => {
|
|
509
|
+
clearTimeout(timeout);
|
|
510
|
+
|
|
511
|
+
const reqHeaders = request.headers();
|
|
512
|
+
let uiSessionId = '';
|
|
513
|
+
let uiParentMessageId: string | null = null;
|
|
514
|
+
|
|
515
|
+
const postData = request.postData();
|
|
516
|
+
if (postData) {
|
|
517
|
+
try {
|
|
518
|
+
const payload = JSON.parse(postData);
|
|
519
|
+
if (payload.chat_id) {
|
|
520
|
+
uiSessionId = payload.chat_id;
|
|
521
|
+
}
|
|
522
|
+
if (payload.parent_id !== undefined) {
|
|
523
|
+
uiParentMessageId = payload.parent_id;
|
|
524
|
+
}
|
|
525
|
+
} catch (e) {
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const extractedHeaders = {
|
|
530
|
+
'cookie': reqHeaders['cookie'] || '',
|
|
531
|
+
'bx-ua': reqHeaders['bx-ua'] || '',
|
|
532
|
+
'bx-umidtoken': reqHeaders['bx-umidtoken'] || '',
|
|
533
|
+
'bx-v': reqHeaders['bx-v'] || '',
|
|
534
|
+
'x-request-id': reqHeaders['x-request-id'] || '',
|
|
535
|
+
'user-agent': reqHeaders['user-agent'] || ''
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
if (!extractedHeaders.cookie || !extractedHeaders['bx-ua']) {
|
|
539
|
+
console.log(`[Playwright] Intercepted request missing critical headers for ${cacheKey}, skipping...`);
|
|
540
|
+
await route.continue();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
console.log(`[Playwright] Successfully intercepted headers for ${cacheKey}.`);
|
|
545
|
+
cache.currentHeaders = extractedHeaders;
|
|
546
|
+
cache.cachedQwenHeaders = { headers: extractedHeaders, chatSessionId: uiSessionId, parentMessageId: uiParentMessageId };
|
|
547
|
+
cache.lastHeadersTime = Date.now();
|
|
548
|
+
cache.refreshInProgress = false;
|
|
549
|
+
|
|
550
|
+
import('./qwen.ts').then(m => m.disableNativeTools(accountId).catch(() => {}));
|
|
551
|
+
|
|
552
|
+
await route.abort('aborted');
|
|
553
|
+
|
|
554
|
+
await page.unroute('**/api/v2/chat/completions*', routeHandler);
|
|
555
|
+
|
|
556
|
+
resolve(cache.cachedQwenHeaders);
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
page.route('**/api/v2/chat/completions*', routeHandler).then(async () => {
|
|
560
|
+
console.log(`[Playwright] Triggering request for ${cacheKey}...`);
|
|
561
|
+
const inputSelector = 'textarea:visible, [contenteditable="true"]:visible';
|
|
562
|
+
|
|
563
|
+
await page.focus(inputSelector);
|
|
564
|
+
await page.fill(inputSelector, '');
|
|
565
|
+
await page.type(inputSelector, 'a', { delay: 100 });
|
|
566
|
+
console.log(`[Playwright] Typed char for ${cacheKey}, waiting for UI to update...`);
|
|
567
|
+
await sleep(2000);
|
|
568
|
+
|
|
569
|
+
const selectors = [
|
|
570
|
+
'.message-input-right-button-send .send-button',
|
|
571
|
+
'.chat-prompt-send-button',
|
|
572
|
+
'button.send-button'
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
let clicked = false;
|
|
576
|
+
for (const selector of selectors) {
|
|
577
|
+
try {
|
|
578
|
+
const btn = await page.$(selector);
|
|
579
|
+
if (btn && await btn.isVisible()) {
|
|
580
|
+
console.log(`[Playwright] Attempting click on: ${selector}`);
|
|
581
|
+
|
|
582
|
+
await page.evaluate((sel) => {
|
|
583
|
+
const element = document.querySelector(sel) as HTMLElement;
|
|
584
|
+
if (element) {
|
|
585
|
+
element.focus();
|
|
586
|
+
element.click();
|
|
587
|
+
}
|
|
588
|
+
}, selector);
|
|
589
|
+
|
|
590
|
+
await btn.click({ force: true, delay: 50 }).catch(() => {});
|
|
591
|
+
|
|
592
|
+
clicked = true;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
} catch (e) {
|
|
596
|
+
console.error(`[Playwright] Error clicking ${selector} for ${cacheKey}:`, e);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (!clicked) {
|
|
601
|
+
console.log(`[Playwright] No send button found/clicked for ${cacheKey}, fallback to Enter...`);
|
|
602
|
+
await page.focus(inputSelector);
|
|
603
|
+
await page.keyboard.press('Enter');
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export async function initPlaywrightForAccount(account: QwenAccount, headless = true, browserType: BrowserType = 'chromium') {
|
|
610
|
+
const profilePath = path.resolve('qwen_profiles', account.id);
|
|
611
|
+
|
|
612
|
+
let browserEngine;
|
|
613
|
+
let channel: string | undefined;
|
|
614
|
+
|
|
615
|
+
switch (browserType) {
|
|
616
|
+
case 'firefox':
|
|
617
|
+
browserEngine = firefox;
|
|
618
|
+
break;
|
|
619
|
+
case 'webkit':
|
|
620
|
+
browserEngine = webkit;
|
|
621
|
+
break;
|
|
622
|
+
case 'chrome':
|
|
623
|
+
browserEngine = chromium;
|
|
624
|
+
channel = 'chrome';
|
|
625
|
+
break;
|
|
626
|
+
case 'edge':
|
|
627
|
+
browserEngine = chromium;
|
|
628
|
+
channel = 'msedge';
|
|
629
|
+
break;
|
|
630
|
+
case 'chromium':
|
|
631
|
+
default:
|
|
632
|
+
browserEngine = chromium;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
console.log(`[Playwright] Launching ${browserType} for account ${account.email}...`);
|
|
637
|
+
|
|
638
|
+
const acctContext = await browserEngine.launchPersistentContext(profilePath, {
|
|
639
|
+
headless,
|
|
640
|
+
channel,
|
|
641
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
|
642
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
643
|
+
args: [
|
|
644
|
+
'--disable-blink-features=AutomationControlled'
|
|
645
|
+
]
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
await acctContext.addInitScript(() => {
|
|
649
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
650
|
+
get: () => undefined,
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const acctPage = await acctContext.newPage();
|
|
655
|
+
accountContexts.set(account.id, acctContext);
|
|
656
|
+
accountPages.set(account.id, acctPage);
|
|
657
|
+
|
|
658
|
+
const cookies = await acctContext.cookies();
|
|
659
|
+
const hasAuthCookie = cookies.some(c => c.name.toLowerCase().includes('token') || c.name.toLowerCase().includes('session'));
|
|
660
|
+
|
|
661
|
+
if (!hasAuthCookie && account.email && account.password) {
|
|
662
|
+
await loginToQwenWithContext(acctContext, acctPage, account.email, account.password);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export async function launchManualLoginAccount(accountId: string, browserType: BrowserType = 'chromium'): Promise<{ context: BrowserContext, page: Page }> {
|
|
667
|
+
const profilePath = path.resolve('qwen_profiles', accountId);
|
|
668
|
+
|
|
669
|
+
let browserEngine;
|
|
670
|
+
let channel: string | undefined;
|
|
671
|
+
|
|
672
|
+
switch (browserType) {
|
|
673
|
+
case 'firefox':
|
|
674
|
+
browserEngine = firefox;
|
|
675
|
+
break;
|
|
676
|
+
case 'webkit':
|
|
677
|
+
browserEngine = webkit;
|
|
678
|
+
break;
|
|
679
|
+
case 'chrome':
|
|
680
|
+
browserEngine = chromium;
|
|
681
|
+
channel = 'chrome';
|
|
682
|
+
break;
|
|
683
|
+
case 'edge':
|
|
684
|
+
browserEngine = chromium;
|
|
685
|
+
channel = 'msedge';
|
|
686
|
+
break;
|
|
687
|
+
case 'chromium':
|
|
688
|
+
default:
|
|
689
|
+
browserEngine = chromium;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const acctContext = await browserEngine.launchPersistentContext(profilePath, {
|
|
694
|
+
headless: false,
|
|
695
|
+
channel,
|
|
696
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
|
697
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
698
|
+
args: [
|
|
699
|
+
'--disable-blink-features=AutomationControlled'
|
|
700
|
+
]
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
await acctContext.addInitScript(() => {
|
|
704
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
705
|
+
get: () => undefined,
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const acctPage = await acctContext.newPage();
|
|
710
|
+
await acctPage.goto('https://chat.qwen.ai/auth', { waitUntil: 'domcontentloaded' });
|
|
711
|
+
|
|
712
|
+
return { context: acctContext, page: acctPage };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export async function extractAccountInfoFromContext(page: Page): Promise<{ email: string | null, hasSession: boolean }> {
|
|
716
|
+
const cookies = await page.context().cookies();
|
|
717
|
+
const hasSession = cookies.some(c => c.name.toLowerCase().includes('token') || c.name.toLowerCase().includes('session'));
|
|
718
|
+
|
|
719
|
+
let email: string | null = null;
|
|
720
|
+
if (hasSession) {
|
|
721
|
+
try {
|
|
722
|
+
email = await page.evaluate(() => {
|
|
723
|
+
const el = document.querySelector('[data-testid="user-email"], .user-email, [class*="email"]');
|
|
724
|
+
return el?.textContent?.trim() || null;
|
|
725
|
+
});
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return { email, hasSession };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export async function closePlaywrightForAccount(accountId: string) {
|
|
734
|
+
const acctContext = accountContexts.get(accountId);
|
|
735
|
+
if (acctContext) {
|
|
736
|
+
await acctContext.close();
|
|
737
|
+
accountContexts.delete(accountId);
|
|
738
|
+
accountPages.delete(accountId);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function loginToQwenWithContext(acctContext: BrowserContext, acctPage: Page, email: string, password: string): Promise<boolean> {
|
|
743
|
+
await acctPage.goto('https://chat.qwen.ai/auth', { waitUntil: 'domcontentloaded' });
|
|
744
|
+
|
|
745
|
+
const hashedPassword = crypto.createHash('sha256').update(password).digest('hex');
|
|
746
|
+
|
|
747
|
+
const result = await acctPage.evaluate(async ({ email, password }) => {
|
|
748
|
+
try {
|
|
749
|
+
const response = await fetch("https://chat.qwen.ai/api/v2/auths/signin", {
|
|
750
|
+
method: "POST",
|
|
751
|
+
headers: {
|
|
752
|
+
"accept": "application/json, text/plain, */*",
|
|
753
|
+
"content-type": "application/json",
|
|
754
|
+
"source": "web",
|
|
755
|
+
"timezone": new Date().toString().split(' (')[0],
|
|
756
|
+
"x-request-id": crypto.randomUUID()
|
|
757
|
+
},
|
|
758
|
+
body: JSON.stringify({ email, password, login_type: "email" })
|
|
759
|
+
});
|
|
760
|
+
const data = await response.json();
|
|
761
|
+
return { ok: response.ok, data };
|
|
762
|
+
} catch (e: any) {
|
|
763
|
+
return { ok: false, error: e.message };
|
|
764
|
+
}
|
|
765
|
+
}, { email, password: hashedPassword });
|
|
766
|
+
|
|
767
|
+
if (result.ok) {
|
|
768
|
+
await acctPage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded' });
|
|
769
|
+
const isLogged = !(acctPage.url().includes('auth') || acctPage.url().includes('login'));
|
|
770
|
+
if (isLogged) {
|
|
771
|
+
console.log(`[Playwright] Login confirmed for ${email}.`);
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
console.error(`[Playwright] Login failed for ${email}:`, result.data || result.error);
|
|
777
|
+
return false;
|
|
778
|
+
}
|