qwenproxy-cli 1.3.5 → 1.4.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/bin/qwenproxy.js +26 -8
- package/dist/clean-cache.js +5076 -0
- package/dist/delete-chats.js +9859 -0
- package/dist/index.js +33946 -0
- package/dist/login.js +6421 -0
- package/dist/reset-cooldowns.js +5462 -0
- package/dist/sync-clients.js +2824 -0
- package/package.json +7 -3
- package/src/api/server.ts +27 -10
- package/src/core/accounts.ts +2 -1
- package/src/core/config.ts +1 -1
- package/src/core/database.ts +139 -0
- package/src/core/paths.ts +0 -6
- package/src/core/version.ts +22 -0
- package/src/routes/chat/retry-policy.ts +4 -0
- package/src/routes/upload.ts +31 -10
- package/src/services/playwright.ts +403 -117
- package/src/services/qwen-chat-pool.ts +26 -0
- package/src/services/qwen-headers.ts +8 -0
- package/src/services/qwen.ts +105 -37
- package/src/tools/parser.ts +7 -1
- package/src/tui/app.ts +2 -12
- package/src/tui/server-manager.ts +3 -1
|
@@ -350,6 +350,10 @@ export async function isPageLoggedIn(
|
|
|
350
350
|
const probe = page
|
|
351
351
|
.evaluate(async () => {
|
|
352
352
|
try {
|
|
353
|
+
if (localStorage.getItem("qwen_token_logged_out_marker")) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
353
357
|
const res = await fetch("/api/v1/auths/", { method: "GET" });
|
|
354
358
|
if (res.status !== 200) return false;
|
|
355
359
|
const json: any = await res.json().catch(() => null);
|
|
@@ -379,6 +383,52 @@ export async function isPageLoggedIn(
|
|
|
379
383
|
);
|
|
380
384
|
if (!hasIdentity) return false;
|
|
381
385
|
|
|
386
|
+
// Check if Alibaba revoked the session upstream via same-origin settings
|
|
387
|
+
try {
|
|
388
|
+
const settingsRes = await fetch("/api/v2/users/user/settings", {
|
|
389
|
+
method: "GET",
|
|
390
|
+
credentials: "include",
|
|
391
|
+
signal: AbortSignal.timeout(3000),
|
|
392
|
+
});
|
|
393
|
+
if (settingsRes.status === 401 || settingsRes.status === 403) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
const settingsJson: any = await settingsRes.json().catch(() => null);
|
|
397
|
+
if (settingsJson && settingsJson.success === false) {
|
|
398
|
+
const code = String(settingsJson.data?.code || settingsJson.code || "").toLowerCase();
|
|
399
|
+
const details = String(settingsJson.data?.details || settingsJson.details || "").toLowerCase();
|
|
400
|
+
if (
|
|
401
|
+
code.includes("unauthorized") ||
|
|
402
|
+
details.includes("401") ||
|
|
403
|
+
details.includes("revogado") ||
|
|
404
|
+
details.includes("revoked")
|
|
405
|
+
) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch {}
|
|
410
|
+
|
|
411
|
+
// Fallback cross-origin refresh probe
|
|
412
|
+
try {
|
|
413
|
+
const refreshRes = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
|
|
414
|
+
method: "GET",
|
|
415
|
+
credentials: "include",
|
|
416
|
+
signal: AbortSignal.timeout(3000),
|
|
417
|
+
});
|
|
418
|
+
if (refreshRes.status === 200) {
|
|
419
|
+
const refreshJson: any = await refreshRes.json().catch(() => null);
|
|
420
|
+
if (refreshJson && refreshJson.success === false) {
|
|
421
|
+
const code = refreshJson.data?.code || refreshJson.code;
|
|
422
|
+
const details = String(refreshJson.data?.details || "");
|
|
423
|
+
if (code === "Unauthorized" || details.includes("revogado") || details.includes("revoked")) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} else if (refreshRes.status === 401 || refreshRes.status === 403) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
} catch {}
|
|
431
|
+
|
|
382
432
|
// If an authenticated user object is confirmed with a real user identity,
|
|
383
433
|
// the session is 100% valid and verified by upstream.
|
|
384
434
|
return true;
|
|
@@ -474,10 +524,10 @@ interface AccountHeaderCache {
|
|
|
474
524
|
}
|
|
475
525
|
|
|
476
526
|
const headerCaches = new Map<string, AccountHeaderCache>();
|
|
477
|
-
// Real TTL measured from Qwen: auth token = 30 days
|
|
478
|
-
//
|
|
479
|
-
const HEADER_CACHE_TTL =
|
|
480
|
-
const HEADER_REFRESH_THRESHOLD = 0.8; // Background refresh at 80% of TTL
|
|
527
|
+
// Real TTL measured from Qwen: auth token = 30 days. Anti-bot tokens remain stable
|
|
528
|
+
// across the browser session; cookies are dynamically updated from the browser context.
|
|
529
|
+
const HEADER_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
530
|
+
const HEADER_REFRESH_THRESHOLD = 0.8; // Background refresh at 80% of TTL
|
|
481
531
|
const COOKIE_CACHE_TTL = 15 * 60 * 1000; // 15 minutes
|
|
482
532
|
const cookieCaches = new Map<string, { cookie: string; timestamp: number }>();
|
|
483
533
|
const lastAccountActivity = new Map<string, number>();
|
|
@@ -1077,6 +1127,9 @@ async function tryLightweightCookieRefresh(
|
|
|
1077
1127
|
);
|
|
1078
1128
|
const cookieStr = cookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
1079
1129
|
cookieCaches.set(accountId, { cookie: cookieStr, timestamp: Date.now() });
|
|
1130
|
+
if (cache && cache.headers) {
|
|
1131
|
+
cache.headers["cookie"] = cookieStr;
|
|
1132
|
+
}
|
|
1080
1133
|
return true;
|
|
1081
1134
|
} catch {
|
|
1082
1135
|
return false;
|
|
@@ -1085,11 +1138,13 @@ async function tryLightweightCookieRefresh(
|
|
|
1085
1138
|
|
|
1086
1139
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
1087
1140
|
|
|
1088
|
-
export async function getCookies(accountId: string): Promise<string> {
|
|
1141
|
+
export async function getCookies(accountId: string, forceFresh = false): Promise<string> {
|
|
1089
1142
|
const now = Date.now();
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1143
|
+
if (!forceFresh) {
|
|
1144
|
+
const cached = cookieCaches.get(accountId);
|
|
1145
|
+
if (cached && now - cached.timestamp < COOKIE_CACHE_TTL) {
|
|
1146
|
+
return cached.cookie;
|
|
1147
|
+
}
|
|
1093
1148
|
}
|
|
1094
1149
|
|
|
1095
1150
|
const page = accountPages.get(accountId);
|
|
@@ -1233,8 +1288,7 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1233
1288
|
const cookieSnapshot = await getCookieSnapshot(accountId);
|
|
1234
1289
|
if (
|
|
1235
1290
|
cookieSnapshot &&
|
|
1236
|
-
isAuthTokenValidFrom(cookieSnapshot)
|
|
1237
|
-
isShortestCookieValidFrom(cookieSnapshot)
|
|
1291
|
+
isAuthTokenValidFrom(cookieSnapshot)
|
|
1238
1292
|
) {
|
|
1239
1293
|
// Token is still valid - just refresh cookies, keep cached headers
|
|
1240
1294
|
const cookie = cookieSnapshot
|
|
@@ -1282,6 +1336,53 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1282
1336
|
});
|
|
1283
1337
|
}
|
|
1284
1338
|
|
|
1339
|
+
if (!hasRequiredQwenHeaders(cache.headers)) {
|
|
1340
|
+
try {
|
|
1341
|
+
const { getValidAuthSession } = await import("../core/database.ts");
|
|
1342
|
+
const persisted = getValidAuthSession(accountId);
|
|
1343
|
+
if (persisted) {
|
|
1344
|
+
const restoredHeaders: Record<string, string> = {
|
|
1345
|
+
cookie: persisted.cookie,
|
|
1346
|
+
"user-agent": persisted.userAgent,
|
|
1347
|
+
"bx-v": persisted.bxV,
|
|
1348
|
+
"bx-ua": persisted.bxUa,
|
|
1349
|
+
"bx-umidtoken": persisted.bxUmidtoken,
|
|
1350
|
+
...(persisted.secChUa ? { "sec-ch-ua": persisted.secChUa } : {}),
|
|
1351
|
+
...(persisted.secChUaMobile ? { "sec-ch-ua-mobile": persisted.secChUaMobile } : {}),
|
|
1352
|
+
...(persisted.secChUaPlatform ? { "sec-ch-ua-platform": persisted.secChUaPlatform } : {}),
|
|
1353
|
+
...(persisted.version ? { version: persisted.version } : {}),
|
|
1354
|
+
};
|
|
1355
|
+
if (hasRequiredQwenHeaders(restoredHeaders) && hasValidAuthToken(restoredHeaders.cookie)) {
|
|
1356
|
+
const page = accountPages.get(accountId);
|
|
1357
|
+
if (page && !page.isClosed()) {
|
|
1358
|
+
try {
|
|
1359
|
+
const cookiesToAdd = parseCookiesForBrowser(restoredHeaders.cookie);
|
|
1360
|
+
if (cookiesToAdd.length > 0) {
|
|
1361
|
+
await page.context().addCookies(cookiesToAdd).catch(() => {});
|
|
1362
|
+
}
|
|
1363
|
+
const tokenMatch = restoredHeaders.cookie.match(/(?:^|;\s*)token=([^;]+)/);
|
|
1364
|
+
if (tokenMatch) {
|
|
1365
|
+
const tok = decodeURIComponent(tokenMatch[1].trim());
|
|
1366
|
+
await page.evaluate((t) => {
|
|
1367
|
+
try {
|
|
1368
|
+
localStorage.setItem("token", t);
|
|
1369
|
+
document.cookie = `token=${encodeURIComponent(t)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
1370
|
+
} catch {}
|
|
1371
|
+
}, tok).catch(() => {});
|
|
1372
|
+
}
|
|
1373
|
+
} catch {}
|
|
1374
|
+
}
|
|
1375
|
+
cache.headers = restoredHeaders;
|
|
1376
|
+
if (persisted.version) {
|
|
1377
|
+
updateQwenWebVersion(persisted.version);
|
|
1378
|
+
}
|
|
1379
|
+
cache.lastRefresh = persisted.capturedAt;
|
|
1380
|
+
markAccountHeadersReady(accountId);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
} catch {}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1285
1386
|
if (!hasRequiredQwenHeaders(cache.headers)) {
|
|
1286
1387
|
console.log(
|
|
1287
1388
|
`🔄 [Playwright] Missing required anti-bot headers for ${accountId}, triggering header interception...`,
|
|
@@ -1307,7 +1408,11 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1307
1408
|
const bxV = cache.headers["bx-v"] || "2.5.37";
|
|
1308
1409
|
|
|
1309
1410
|
// Read cookie AFTER all refreshes (re-login may have updated it)
|
|
1310
|
-
|
|
1411
|
+
cookieCaches.delete(accountId);
|
|
1412
|
+
const cookie = await getCookies(accountId, true);
|
|
1413
|
+
if (cache.headers) {
|
|
1414
|
+
cache.headers["cookie"] = cookie;
|
|
1415
|
+
}
|
|
1311
1416
|
|
|
1312
1417
|
return {
|
|
1313
1418
|
cookie,
|
|
@@ -1432,6 +1537,62 @@ export async function initPlaywrightForAccount(
|
|
|
1432
1537
|
await hook(acctContext);
|
|
1433
1538
|
}
|
|
1434
1539
|
|
|
1540
|
+
// Block telemetry pixels, third-party trackers, ad scripts and heavy video/audio media to speed up page loads by 30-50%
|
|
1541
|
+
// Invariant 5: NEVER abort image or stylesheet requests needed by Baxia slider (Canvas tiles on img.alicdn.com)
|
|
1542
|
+
await acctContext.route("**/*", (route) => {
|
|
1543
|
+
const req = route.request();
|
|
1544
|
+
const url = req.url();
|
|
1545
|
+
const type = req.resourceType();
|
|
1546
|
+
|
|
1547
|
+
// 1. Heavy video/audio media (never used for text completions)
|
|
1548
|
+
if (type === "media") {
|
|
1549
|
+
return route.abort().catch(() => {});
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// 2. Google Tag Manager: fulfill with stub script to avoid runtime undefined errors
|
|
1553
|
+
if (url.includes("googletagmanager.com/gtag/js") || url.includes("google-analytics.com")) {
|
|
1554
|
+
return route.fulfill({
|
|
1555
|
+
status: 200,
|
|
1556
|
+
contentType: "application/javascript",
|
|
1557
|
+
body: "window.dataLayer=window.dataLayer||[];window.gtag=window.gtag||function(){dataLayer.push(arguments)};",
|
|
1558
|
+
}).catch(() => {});
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// 3. Telemetry tracking pixels, ad networks and event monitors
|
|
1562
|
+
const isTelemetryOrTracker =
|
|
1563
|
+
url.includes("fourier.taobao.com") ||
|
|
1564
|
+
url.includes("googletagmanager") ||
|
|
1565
|
+
url.includes("analytics.google") ||
|
|
1566
|
+
url.includes("doubleclick.net") ||
|
|
1567
|
+
url.includes("connect.facebook") ||
|
|
1568
|
+
url.includes("facebook.com") ||
|
|
1569
|
+
url.includes("adjust.com") ||
|
|
1570
|
+
url.includes("ucweb.com") ||
|
|
1571
|
+
url.includes("hotjar.com") ||
|
|
1572
|
+
url.includes("clarity.ms") ||
|
|
1573
|
+
url.includes("/AWSC/et/"); // Alibaba Event Tracking (safe to block, verified)
|
|
1574
|
+
|
|
1575
|
+
if (isTelemetryOrTracker) {
|
|
1576
|
+
if (type === "script") {
|
|
1577
|
+
return route.fulfill({
|
|
1578
|
+
status: 200,
|
|
1579
|
+
contentType: "application/javascript",
|
|
1580
|
+
body: "",
|
|
1581
|
+
}).catch(() => {});
|
|
1582
|
+
}
|
|
1583
|
+
if (type === "image") {
|
|
1584
|
+
return route.fulfill({
|
|
1585
|
+
status: 200,
|
|
1586
|
+
contentType: "image/gif",
|
|
1587
|
+
body: Buffer.from("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", "base64"),
|
|
1588
|
+
}).catch(() => {});
|
|
1589
|
+
}
|
|
1590
|
+
return route.abort().catch(() => {});
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return route.continue().catch(() => {});
|
|
1594
|
+
}).catch(() => {});
|
|
1595
|
+
|
|
1435
1596
|
// If native profile cookies are empty but a backup storage_state.json exists, restore cookies
|
|
1436
1597
|
const storageState = loadStorageState(account.id);
|
|
1437
1598
|
if (storageState) {
|
|
@@ -1471,7 +1632,78 @@ export async function initPlaywrightForAccount(
|
|
|
1471
1632
|
installContextDeathHandlers(account.id, acctContext, acctPage);
|
|
1472
1633
|
touchAccountActivity(account.id);
|
|
1473
1634
|
|
|
1474
|
-
//
|
|
1635
|
+
// 1. Fast boot: if a valid session exists in SQLite, restore headers and cookies instantly.
|
|
1636
|
+
// Eliminates redundant navigations, DOM probes, and fake UI typing on healthy accounts.
|
|
1637
|
+
let restoredFromDb = false;
|
|
1638
|
+
if (!options.skipHeaderCapture) {
|
|
1639
|
+
try {
|
|
1640
|
+
const { getValidAuthSession } = await import("../core/database.ts");
|
|
1641
|
+
const persisted = getValidAuthSession(account.id);
|
|
1642
|
+
if (persisted) {
|
|
1643
|
+
const restoredHeaders: Record<string, string> = {
|
|
1644
|
+
cookie: persisted.cookie,
|
|
1645
|
+
"user-agent": persisted.userAgent,
|
|
1646
|
+
"bx-v": persisted.bxV,
|
|
1647
|
+
"bx-ua": persisted.bxUa,
|
|
1648
|
+
"bx-umidtoken": persisted.bxUmidtoken,
|
|
1649
|
+
...(persisted.secChUa ? { "sec-ch-ua": persisted.secChUa } : {}),
|
|
1650
|
+
...(persisted.secChUaMobile ? { "sec-ch-ua-mobile": persisted.secChUaMobile } : {}),
|
|
1651
|
+
...(persisted.secChUaPlatform ? { "sec-ch-ua-platform": persisted.secChUaPlatform } : {}),
|
|
1652
|
+
...(persisted.version ? { version: persisted.version } : {}),
|
|
1653
|
+
};
|
|
1654
|
+
if (hasRequiredQwenHeaders(restoredHeaders) && hasValidAuthToken(restoredHeaders.cookie)) {
|
|
1655
|
+
// Inject cookies into Chromium browser context so in-page requests are authenticated
|
|
1656
|
+
try {
|
|
1657
|
+
const cookiesToAdd = parseCookiesForBrowser(restoredHeaders.cookie);
|
|
1658
|
+
if (cookiesToAdd.length > 0) {
|
|
1659
|
+
await acctContext.addCookies(cookiesToAdd).catch(() => {});
|
|
1660
|
+
}
|
|
1661
|
+
const tokenMatch = restoredHeaders.cookie.match(/(?:^|;\s*)token=([^;]+)/);
|
|
1662
|
+
if (tokenMatch && !acctPage.isClosed()) {
|
|
1663
|
+
const tok = decodeURIComponent(tokenMatch[1].trim());
|
|
1664
|
+
await acctPage.evaluate((t) => {
|
|
1665
|
+
try {
|
|
1666
|
+
localStorage.setItem("token", t);
|
|
1667
|
+
document.cookie = `token=${encodeURIComponent(t)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
1668
|
+
} catch {}
|
|
1669
|
+
}, tok).catch(() => {});
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Read live cookies from browser context
|
|
1673
|
+
const liveCookies = await acctContext.cookies();
|
|
1674
|
+
if (liveCookies.length > 0 && liveCookies.some((c) => c.name === "token")) {
|
|
1675
|
+
restoredHeaders.cookie = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
1676
|
+
cookieCaches.set(account.id, { cookie: restoredHeaders.cookie, timestamp: Date.now() });
|
|
1677
|
+
}
|
|
1678
|
+
} catch {}
|
|
1679
|
+
|
|
1680
|
+
const cache = getHeaderCache(account.id);
|
|
1681
|
+
cache.headers = restoredHeaders;
|
|
1682
|
+
if (persisted.version) {
|
|
1683
|
+
updateQwenWebVersion(persisted.version);
|
|
1684
|
+
}
|
|
1685
|
+
cache.lastRefresh = persisted.capturedAt;
|
|
1686
|
+
markAccountHeadersReady(account.id);
|
|
1687
|
+
restoredFromDb = true;
|
|
1688
|
+
console.log(
|
|
1689
|
+
`⚡ [Playwright] Restored anti-bot headers from database for ${maskEmail(account.email)} (age: ${Math.round((Date.now() - persisted.capturedAt) / 60000)}m, bypassed UI typing)`,
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
} catch {}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
if (restoredFromDb) {
|
|
1697
|
+
if (!acctPage.isClosed() && (acctPage.url() === "about:blank" || !acctPage.url().startsWith(qwenOrigin()))) {
|
|
1698
|
+
void acctPage.goto(qwenUrl("/"), {
|
|
1699
|
+
waitUntil: "domcontentloaded",
|
|
1700
|
+
timeout: config.timeouts.navigation,
|
|
1701
|
+
}).catch(() => {});
|
|
1702
|
+
}
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// 2. Initial setup for fresh accounts without prior session: check cookies or authenticate
|
|
1475
1707
|
const cookies = await acctContext.cookies();
|
|
1476
1708
|
const hasAuthCookie = cookies.some(
|
|
1477
1709
|
(c) =>
|
|
@@ -1483,57 +1715,27 @@ export async function initPlaywrightForAccount(
|
|
|
1483
1715
|
await loginToQwen(account.id, account.email, account.password);
|
|
1484
1716
|
}
|
|
1485
1717
|
|
|
1486
|
-
//
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
if (!loggedIn) {
|
|
1501
|
-
if (account.email && account.password) {
|
|
1502
|
-
console.log(
|
|
1503
|
-
`[Playwright] Session expired or guest state detected for ${maskEmail(account.email)}, re-authenticating...`,
|
|
1504
|
-
);
|
|
1505
|
-
const ok = await loginToQwen(account.id, account.email, account.password);
|
|
1506
|
-
if (!ok || !(await isPageLoggedIn(acctPage, 4_000))) {
|
|
1507
|
-
validationError = new Error(
|
|
1508
|
-
`Session expired for ${maskEmail(account.email)} and re-authentication failed`,
|
|
1509
|
-
);
|
|
1510
|
-
continue;
|
|
1511
|
-
}
|
|
1512
|
-
} else {
|
|
1513
|
-
validationError = new Error(
|
|
1514
|
-
`Session expired for account ${account.id} but no credentials available for re-login (run 'qpx login')`,
|
|
1515
|
-
);
|
|
1516
|
-
break;
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
validationError = null;
|
|
1520
|
-
break;
|
|
1521
|
-
} catch (err: any) {
|
|
1522
|
-
validationError = err;
|
|
1523
|
-
if (vAttempt < maxValidationAttempts) {
|
|
1524
|
-
console.warn(
|
|
1525
|
-
`⚠️ [Playwright] Session validation attempt ${vAttempt}/${maxValidationAttempts} failed for ${maskEmail(account.email)}: ${err.message}, retrying...`,
|
|
1526
|
-
);
|
|
1527
|
-
await sleep(3000);
|
|
1528
|
-
}
|
|
1718
|
+
// Initial navigation to chat home
|
|
1719
|
+
try {
|
|
1720
|
+
await acctPage.goto(qwenUrl("/"), {
|
|
1721
|
+
waitUntil: "domcontentloaded",
|
|
1722
|
+
timeout: config.timeouts.navigation,
|
|
1723
|
+
});
|
|
1724
|
+
await sleep(1500);
|
|
1725
|
+
const currentUrl = acctPage.url();
|
|
1726
|
+
const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
|
|
1727
|
+
if (isAuthUrl && account.email && account.password) {
|
|
1728
|
+
console.log(
|
|
1729
|
+
`[Playwright] Initial login needed for ${maskEmail(account.email)}, authenticating...`,
|
|
1730
|
+
);
|
|
1731
|
+
await loginToQwen(account.id, account.email, account.password);
|
|
1529
1732
|
}
|
|
1530
|
-
}
|
|
1531
|
-
if (validationError) {
|
|
1733
|
+
} catch (err: any) {
|
|
1532
1734
|
console.warn(
|
|
1533
|
-
|
|
1735
|
+
`⚠️ [Playwright] Initial navigation check for ${maskEmail(account.email)}: ${err.message}`,
|
|
1534
1736
|
);
|
|
1535
|
-
throw validationError;
|
|
1536
1737
|
}
|
|
1738
|
+
|
|
1537
1739
|
if (!options.skipHeaderCapture) {
|
|
1538
1740
|
(acctPage as any).__qwenChatHomeLoaded = true;
|
|
1539
1741
|
await captureQwenHeaders(account.id);
|
|
@@ -1809,9 +2011,27 @@ async function loginToQwen(
|
|
|
1809
2011
|
|
|
1810
2012
|
const maxAttempts = 3;
|
|
1811
2013
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
2014
|
+
if (page.isClosed()) {
|
|
2015
|
+
console.warn(
|
|
2016
|
+
`⚠️ [Playwright] Login aborted: page was closed for ${maskEmail(email)}`,
|
|
2017
|
+
);
|
|
2018
|
+
return false;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1812
2021
|
// Try API login first
|
|
1813
2022
|
const apiResult = await loginViaApi(page, email, password);
|
|
1814
2023
|
if (apiResult.success) {
|
|
2024
|
+
cookieCaches.delete(accountId);
|
|
2025
|
+
const cache = headerCaches.get(accountId);
|
|
2026
|
+
if (cache) {
|
|
2027
|
+
cache.headers = {};
|
|
2028
|
+
cache.lastRefresh = 0;
|
|
2029
|
+
}
|
|
2030
|
+
unmarkAccountHeadersReady(accountId);
|
|
2031
|
+
try {
|
|
2032
|
+
const { deleteAuthSession } = await import("../core/database.ts");
|
|
2033
|
+
deleteAuthSession(accountId);
|
|
2034
|
+
} catch {}
|
|
1815
2035
|
await saveStorageState(page.context(), accountId);
|
|
1816
2036
|
return true;
|
|
1817
2037
|
}
|
|
@@ -1831,6 +2051,17 @@ async function loginToQwen(
|
|
|
1831
2051
|
// Fallback to UI login
|
|
1832
2052
|
const uiResult = await loginViaUi(page, email, password);
|
|
1833
2053
|
if (uiResult.success) {
|
|
2054
|
+
cookieCaches.delete(accountId);
|
|
2055
|
+
const cache = headerCaches.get(accountId);
|
|
2056
|
+
if (cache) {
|
|
2057
|
+
cache.headers = {};
|
|
2058
|
+
cache.lastRefresh = 0;
|
|
2059
|
+
}
|
|
2060
|
+
unmarkAccountHeadersReady(accountId);
|
|
2061
|
+
try {
|
|
2062
|
+
const { deleteAuthSession } = await import("../core/database.ts");
|
|
2063
|
+
deleteAuthSession(accountId);
|
|
2064
|
+
} catch {}
|
|
1834
2065
|
await saveStorageState(page.context(), accountId);
|
|
1835
2066
|
return true;
|
|
1836
2067
|
}
|
|
@@ -1854,6 +2085,20 @@ async function loginToQwen(
|
|
|
1854
2085
|
);
|
|
1855
2086
|
await sleep(backoffMs);
|
|
1856
2087
|
}
|
|
2088
|
+
|
|
2089
|
+
if (page.isClosed()) {
|
|
2090
|
+
console.warn(
|
|
2091
|
+
`⚠️ [Playwright] Login aborted: page was closed for ${maskEmail(email)}`,
|
|
2092
|
+
);
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
if (page.isClosed()) {
|
|
2098
|
+
console.warn(
|
|
2099
|
+
`⚠️ [Playwright] Login aborted: page was closed for ${maskEmail(email)}`,
|
|
2100
|
+
);
|
|
2101
|
+
return false;
|
|
1857
2102
|
}
|
|
1858
2103
|
|
|
1859
2104
|
console.error(
|
|
@@ -1873,17 +2118,6 @@ async function loginViaApi(
|
|
|
1873
2118
|
password: string,
|
|
1874
2119
|
): Promise<LoginAttemptResult> {
|
|
1875
2120
|
try {
|
|
1876
|
-
await page.goto(qwenUrl("/auth"), {
|
|
1877
|
-
waitUntil: "domcontentloaded",
|
|
1878
|
-
timeout: config.timeouts.navigation,
|
|
1879
|
-
});
|
|
1880
|
-
await sleep(2000);
|
|
1881
|
-
|
|
1882
|
-
// Check if already logged in
|
|
1883
|
-
if (!page.url().includes("/auth")) {
|
|
1884
|
-
return { success: true };
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
2121
|
const hashedPassword = crypto
|
|
1888
2122
|
.createHash("sha256")
|
|
1889
2123
|
.update(password)
|
|
@@ -1906,6 +2140,7 @@ async function loginViaApi(
|
|
|
1906
2140
|
accept: "application/json, text/plain, */*",
|
|
1907
2141
|
referer: qwenUrl("/auth"),
|
|
1908
2142
|
origin: qwenOrigin(),
|
|
2143
|
+
source: "web",
|
|
1909
2144
|
},
|
|
1910
2145
|
timeout: 10_000,
|
|
1911
2146
|
});
|
|
@@ -1991,17 +2226,13 @@ async function loginViaApi(
|
|
|
1991
2226
|
await page
|
|
1992
2227
|
.evaluate((tok) => {
|
|
1993
2228
|
try {
|
|
2229
|
+
localStorage.removeItem("qwen_token_logged_out_marker");
|
|
1994
2230
|
localStorage.setItem("token", tok);
|
|
1995
2231
|
document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
1996
2232
|
} catch {}
|
|
1997
2233
|
}, token)
|
|
1998
2234
|
.catch(() => {});
|
|
1999
2235
|
await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
await sleep(1500);
|
|
2003
|
-
const loggedIn = await isPageLoggedIn(page, 5_000);
|
|
2004
|
-
if (loggedIn) {
|
|
2005
2236
|
return { success: true };
|
|
2006
2237
|
}
|
|
2007
2238
|
}
|
|
@@ -2031,20 +2262,12 @@ async function loginViaUi(
|
|
|
2031
2262
|
password: string,
|
|
2032
2263
|
): Promise<LoginAttemptResult> {
|
|
2033
2264
|
try {
|
|
2034
|
-
if (page.url().startsWith(qwenOrigin()) && !page.url().includes("/auth") && !page.url().includes("/login")) {
|
|
2035
|
-
return { success: true };
|
|
2036
|
-
}
|
|
2037
2265
|
await page.goto(qwenUrl("/auth"), {
|
|
2038
2266
|
waitUntil: "domcontentloaded",
|
|
2039
2267
|
timeout: config.timeouts.navigation,
|
|
2040
2268
|
});
|
|
2041
2269
|
await sleep(1500);
|
|
2042
2270
|
|
|
2043
|
-
// Check if already logged in
|
|
2044
|
-
if (!page.url().includes("/auth") && !page.url().includes("/login")) {
|
|
2045
|
-
return { success: true };
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
2271
|
// Wait for email input
|
|
2049
2272
|
const emailSelector = [
|
|
2050
2273
|
'input[type="email"]',
|
|
@@ -2055,9 +2278,12 @@ async function loginViaUi(
|
|
|
2055
2278
|
].join(", ");
|
|
2056
2279
|
try {
|
|
2057
2280
|
await page.waitForSelector(emailSelector, {
|
|
2058
|
-
timeout: config.timeouts.page,
|
|
2281
|
+
timeout: Math.min(config.timeouts.page, 8_000),
|
|
2059
2282
|
});
|
|
2060
2283
|
} catch {
|
|
2284
|
+
if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
|
|
2285
|
+
return { success: true };
|
|
2286
|
+
}
|
|
2061
2287
|
if (!page.url().includes("/auth")) return { success: true };
|
|
2062
2288
|
console.warn(
|
|
2063
2289
|
`⚠️ [Playwright] Email input not found on ${page.url()} (possible captcha or anti-bot challenge)`,
|
|
@@ -2409,6 +2635,26 @@ export async function captureQwenHeaders(
|
|
|
2409
2635
|
cookieCaches.delete(accountId);
|
|
2410
2636
|
touchAccountActivity(accountId);
|
|
2411
2637
|
|
|
2638
|
+
// Persist captured anti-bot headers to SQLite for instant boot
|
|
2639
|
+
try {
|
|
2640
|
+
const { saveAuthSession } = await import("../core/database.ts");
|
|
2641
|
+
const { parseJwtExpiry } = await import("../utils/jwt.ts");
|
|
2642
|
+
const tokenExpiry = parseJwtExpiry(capturedHeaders.cookie);
|
|
2643
|
+
saveAuthSession(accountId, {
|
|
2644
|
+
cookie: capturedHeaders.cookie,
|
|
2645
|
+
userAgent: capturedHeaders["user-agent"],
|
|
2646
|
+
bxV: capturedHeaders["bx-v"],
|
|
2647
|
+
bxUa: capturedHeaders["bx-ua"],
|
|
2648
|
+
bxUmidtoken: capturedHeaders["bx-umidtoken"],
|
|
2649
|
+
secChUa: capturedHeaders["sec-ch-ua"],
|
|
2650
|
+
secChUaMobile: capturedHeaders["sec-ch-ua-mobile"],
|
|
2651
|
+
secChUaPlatform: capturedHeaders["sec-ch-ua-platform"],
|
|
2652
|
+
version: capturedHeaders["version"],
|
|
2653
|
+
tokenExpiresAt: tokenExpiry || undefined,
|
|
2654
|
+
capturedAt: Date.now(),
|
|
2655
|
+
});
|
|
2656
|
+
} catch {}
|
|
2657
|
+
|
|
2412
2658
|
await route.abort("aborted").catch(() => {});
|
|
2413
2659
|
await sleep(HEADER_CAPTURE_SETTLE_MS);
|
|
2414
2660
|
settle();
|
|
@@ -2440,13 +2686,13 @@ export async function captureQwenHeaders(
|
|
|
2440
2686
|
|
|
2441
2687
|
// Session-expiry check: if the page redirected to /auth or /login,
|
|
2442
2688
|
// re-login immediately.
|
|
2443
|
-
// On
|
|
2444
|
-
//
|
|
2445
|
-
//
|
|
2446
|
-
const currentUrl = page.url();
|
|
2689
|
+
// On attempt 1, skip probe because the session was just verified during init.
|
|
2690
|
+
// On attempt 2+, probe isPageLoggedIn to catch revoked sessions or guest state
|
|
2691
|
+
// immediately without waiting for repeated silent timeouts.
|
|
2692
|
+
const currentUrl = typeof page.url === "function" ? page.url() : "";
|
|
2447
2693
|
const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
|
|
2448
|
-
const isSuspectedGuest = attempt >=
|
|
2449
|
-
if (
|
|
2694
|
+
const isSuspectedGuest = isAuthUrl || (attempt >= 2 && !(await isPageLoggedIn(page, 5000)));
|
|
2695
|
+
if (isSuspectedGuest) {
|
|
2450
2696
|
const { getAccountCredentials } = await import("../core/accounts.ts");
|
|
2451
2697
|
const creds = getAccountCredentials(accountId);
|
|
2452
2698
|
if (creds && creds.email && creds.password) {
|
|
@@ -2490,7 +2736,7 @@ export async function captureQwenHeaders(
|
|
|
2490
2736
|
// picking a sibling textarea/contenteditable), then fall back to generic.
|
|
2491
2737
|
// Mirrors upstream 5b3fd3e (robust account header capture).
|
|
2492
2738
|
const inputSelector =
|
|
2493
|
-
|
|
2739
|
+
"textarea.message-input-textarea, textarea[placeholder*='Ask' i], textarea[placeholder*='Pergunte' i], textarea";
|
|
2494
2740
|
// Bound the appearance wait: a page that never renders the chat input is
|
|
2495
2741
|
// blocked (WAF interstitial, punish document, or failed SPA hydration).
|
|
2496
2742
|
// Unbounded, page.focus would burn its 60s default timeout on every
|
|
@@ -2525,26 +2771,17 @@ export async function captureQwenHeaders(
|
|
|
2525
2771
|
if (settled || page.isClosed()) return;
|
|
2526
2772
|
try {
|
|
2527
2773
|
if (typeof (inputLocator as any).focus === "function") {
|
|
2528
|
-
await (inputLocator as any).focus({ timeout: inputActionTimeoutMs });
|
|
2774
|
+
await (inputLocator as any).focus({ timeout: inputActionTimeoutMs }).catch(() => {});
|
|
2529
2775
|
} else {
|
|
2530
|
-
await page.focus(inputSelector, { timeout: inputActionTimeoutMs });
|
|
2776
|
+
await page.focus(inputSelector, { timeout: inputActionTimeoutMs }).catch(() => {});
|
|
2531
2777
|
}
|
|
2532
2778
|
if (settled || page.isClosed()) return;
|
|
2533
2779
|
if (typeof (inputLocator as any).fill === "function") {
|
|
2534
|
-
await (inputLocator as any).fill("a", { timeout: inputActionTimeoutMs });
|
|
2780
|
+
await (inputLocator as any).fill("a", { timeout: inputActionTimeoutMs }).catch(() => {});
|
|
2535
2781
|
} else {
|
|
2536
|
-
await page.fill(inputSelector, "a", { timeout: inputActionTimeoutMs });
|
|
2537
|
-
}
|
|
2538
|
-
// Dispatch native React input events and keyboard stroke to guarantee React updates state
|
|
2539
|
-
if (typeof page.keyboard?.press === "function") {
|
|
2540
|
-
await page.keyboard.press("End").catch(() => {});
|
|
2541
|
-
}
|
|
2542
|
-
if (typeof (page.keyboard as any)?.type === "function") {
|
|
2543
|
-
await (page.keyboard as any).type(" ").catch(() => {});
|
|
2544
|
-
}
|
|
2545
|
-
if (typeof page.keyboard?.press === "function") {
|
|
2546
|
-
await page.keyboard.press("Backspace").catch(() => {});
|
|
2782
|
+
await page.fill(inputSelector, "a", { timeout: inputActionTimeoutMs }).catch(() => {});
|
|
2547
2783
|
}
|
|
2784
|
+
|
|
2548
2785
|
if (typeof page.evaluate === "function") {
|
|
2549
2786
|
await page.evaluate((sel) => {
|
|
2550
2787
|
const el = document.querySelector(sel) as HTMLTextAreaElement | null;
|
|
@@ -2591,17 +2828,18 @@ export async function captureQwenHeaders(
|
|
|
2591
2828
|
try {
|
|
2592
2829
|
await page.waitForFunction(() => {
|
|
2593
2830
|
const btn = document.querySelector(
|
|
2594
|
-
".message-input-right-button-send .send-button, .chat-prompt-send-button, button.send-button, button[aria-label*='Send' i], button[aria-label*='Enviar' i], .send-button-container button"
|
|
2831
|
+
".message-input-right-button-send .send-button, .message-input-right-button-send button, .chat-prompt-send-button, button.send-button, button[aria-label*='Send' i], button[aria-label*='Enviar' i], .send-button-container button"
|
|
2595
2832
|
) as HTMLButtonElement | null;
|
|
2596
2833
|
return btn && !btn.disabled && !btn.classList.contains("disabled") && btn.getAttribute("aria-disabled") !== "true";
|
|
2597
2834
|
}, { timeout: 3000 });
|
|
2598
2835
|
} catch {
|
|
2599
|
-
await sleep(
|
|
2836
|
+
await sleep(500);
|
|
2600
2837
|
}
|
|
2601
2838
|
if (settled || page.isClosed()) return;
|
|
2602
2839
|
|
|
2603
2840
|
const sendSelectors = [
|
|
2604
2841
|
".message-input-right-button-send .send-button",
|
|
2842
|
+
".message-input-right-button-send button",
|
|
2605
2843
|
".chat-prompt-send-button",
|
|
2606
2844
|
"button.send-button",
|
|
2607
2845
|
"button[aria-label*='Send' i]",
|
|
@@ -2624,16 +2862,7 @@ export async function captureQwenHeaders(
|
|
|
2624
2862
|
);
|
|
2625
2863
|
}, btn);
|
|
2626
2864
|
if (!isDisabled) {
|
|
2627
|
-
await
|
|
2628
|
-
const element = document.querySelector(sel) as HTMLElement;
|
|
2629
|
-
if (element) {
|
|
2630
|
-
element.focus();
|
|
2631
|
-
element.click();
|
|
2632
|
-
}
|
|
2633
|
-
}, selector);
|
|
2634
|
-
if (!settled && !page.isClosed()) {
|
|
2635
|
-
await btn.click({ force: true, delay: 50 }).catch(() => {});
|
|
2636
|
-
}
|
|
2865
|
+
await btn.click({ delay: 50 }).catch(() => {});
|
|
2637
2866
|
clicked = true;
|
|
2638
2867
|
break;
|
|
2639
2868
|
}
|
|
@@ -2788,6 +3017,30 @@ export function hasValidAuthToken(cookieHeader?: string): boolean {
|
|
|
2788
3017
|
return true;
|
|
2789
3018
|
}
|
|
2790
3019
|
|
|
3020
|
+
export function parseCookiesForBrowser(cookieHeader: string): Array<{ name: string; value: string; domain: string; path: string; expires: number }> {
|
|
3021
|
+
if (!cookieHeader) return [];
|
|
3022
|
+
const pairs = cookieHeader.split(";").map((s) => s.trim()).filter(Boolean);
|
|
3023
|
+
const result: Array<{ name: string; value: string; domain: string; path: string; expires: number }> = [];
|
|
3024
|
+
const expires = Math.floor(Date.now() / 1000) + 3600 * 24 * 365;
|
|
3025
|
+
for (const pair of pairs) {
|
|
3026
|
+
const eqIdx = pair.indexOf("=");
|
|
3027
|
+
if (eqIdx !== -1) {
|
|
3028
|
+
const name = pair.slice(0, eqIdx).trim();
|
|
3029
|
+
const value = pair.slice(eqIdx + 1).trim();
|
|
3030
|
+
if (name) {
|
|
3031
|
+
result.push({
|
|
3032
|
+
name,
|
|
3033
|
+
value,
|
|
3034
|
+
domain: ".qwen.ai",
|
|
3035
|
+
path: "/",
|
|
3036
|
+
expires,
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
return result;
|
|
3042
|
+
}
|
|
3043
|
+
|
|
2791
3044
|
/**
|
|
2792
3045
|
* Check if the auth token cookie is still valid.
|
|
2793
3046
|
* Used to skip unnecessary header recaptures when the token is still fresh.
|
|
@@ -2858,6 +3111,7 @@ async function refreshHeadersInternal(
|
|
|
2858
3111
|
try {
|
|
2859
3112
|
// Check if session is expired before capturing headers
|
|
2860
3113
|
const page = accountPages.get(accountId);
|
|
3114
|
+
let reauthExecuted = false;
|
|
2861
3115
|
if (page) {
|
|
2862
3116
|
const executeReauth = async () => {
|
|
2863
3117
|
console.warn(
|
|
@@ -2874,6 +3128,7 @@ async function refreshHeadersInternal(
|
|
|
2874
3128
|
`Re-login for ${accountId} did not restore an authenticated session`,
|
|
2875
3129
|
);
|
|
2876
3130
|
}
|
|
3131
|
+
reauthExecuted = true;
|
|
2877
3132
|
} else {
|
|
2878
3133
|
unmarkAccountHeadersReady(accountId);
|
|
2879
3134
|
throw new Error(
|
|
@@ -2908,6 +3163,37 @@ async function refreshHeadersInternal(
|
|
|
2908
3163
|
await executeReauth();
|
|
2909
3164
|
}
|
|
2910
3165
|
}
|
|
3166
|
+
|
|
3167
|
+
// Fast re-auth completion: if re-auth succeeded and required anti-bot tokens are already cached,
|
|
3168
|
+
// refresh cookies directly without running slow UI typing interception.
|
|
3169
|
+
if (reauthExecuted && hasRequiredQwenHeaders(cache.headers)) {
|
|
3170
|
+
const liveCookies = await page.context().cookies();
|
|
3171
|
+
if (liveCookies.some((c) => c.name === "token")) {
|
|
3172
|
+
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
3173
|
+
cache.headers.cookie = cookieStr;
|
|
3174
|
+
cache.lastRefresh = Date.now();
|
|
3175
|
+
markAccountHeadersReady(accountId);
|
|
3176
|
+
try {
|
|
3177
|
+
const { saveAuthSession } = await import("../core/database.ts");
|
|
3178
|
+
const tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
3179
|
+
const exp = tokenCookie ? parseJwtExpiry(tokenCookie.value) : undefined;
|
|
3180
|
+
saveAuthSession(accountId, {
|
|
3181
|
+
cookie: cookieStr,
|
|
3182
|
+
userAgent: cache.headers["user-agent"] || "",
|
|
3183
|
+
bxV: cache.headers["bx-v"] || "2.5.37",
|
|
3184
|
+
bxUa: cache.headers["bx-ua"] || "",
|
|
3185
|
+
bxUmidtoken: cache.headers["bx-umidtoken"] || "",
|
|
3186
|
+
secChUa: cache.headers["sec-ch-ua"] || undefined,
|
|
3187
|
+
secChUaMobile: cache.headers["sec-ch-ua-mobile"] || undefined,
|
|
3188
|
+
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
|
|
3189
|
+
version: cache.headers["version"] || undefined,
|
|
3190
|
+
tokenExpiresAt: exp || undefined,
|
|
3191
|
+
capturedAt: Date.now(),
|
|
3192
|
+
});
|
|
3193
|
+
} catch {}
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
2911
3197
|
}
|
|
2912
3198
|
|
|
2913
3199
|
await captureQwenHeaders(accountId, undefined, boundedTimeoutMs);
|