qwenproxy-cli 1.3.6 → 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.
@@ -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, shortest cookie (acw_tc) = 24 min.
478
- // 20 min is safe: under the 24-min acw_tc, and bx-ua expiry is handled by 403 retry.
479
- const HEADER_CACHE_TTL = 20 * 60 * 1000; // 20 minutes
480
- const HEADER_REFRESH_THRESHOLD = 0.8; // Background refresh at 80% of TTL (16 min)
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
- const cached = cookieCaches.get(accountId);
1091
- if (cached && now - cached.timestamp < COOKIE_CACHE_TTL) {
1092
- return cached.cookie;
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
- const cookie = await getCookies(accountId);
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
- // Check if already logged in
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
- // Navigate to the stable chat page to validate the session and populate cookies.
1487
- // Retry up to 2 times on transient timeouts before giving up.
1488
- const maxValidationAttempts = 2;
1489
- let validationError: Error | null = null;
1490
- for (let vAttempt = 1; vAttempt <= maxValidationAttempts; vAttempt++) {
1491
- try {
1492
- await acctPage.goto(qwenUrl("/"), {
1493
- waitUntil: "domcontentloaded",
1494
- timeout: config.timeouts.navigation,
1495
- });
1496
- await sleep(1500);
1497
- const currentUrl = acctPage.url();
1498
- const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
1499
- const loggedIn = !isAuthUrl && (await isPageLoggedIn(acctPage, 4_000));
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
- `❌ [Playwright] Failed to validate session for ${maskEmail(account.email)} after ${maxValidationAttempts} attempts: ${validationError.message}`,
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 attempts 1 and 2, do NOT run a tight 3s fetch probe because the session
2444
- // was already verified during init. Only run isPageLoggedIn if attempt >= 3
2445
- // (as a last-resort recovery before giving up) and with a generous 8s timeout.
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 >= 3 && !(await isPageLoggedIn(page, 8000));
2449
- if (isAuthUrl || isSuspectedGuest) {
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) {
@@ -2771,6 +3017,30 @@ export function hasValidAuthToken(cookieHeader?: string): boolean {
2771
3017
  return true;
2772
3018
  }
2773
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
+
2774
3044
  /**
2775
3045
  * Check if the auth token cookie is still valid.
2776
3046
  * Used to skip unnecessary header recaptures when the token is still fresh.
@@ -2841,6 +3111,7 @@ async function refreshHeadersInternal(
2841
3111
  try {
2842
3112
  // Check if session is expired before capturing headers
2843
3113
  const page = accountPages.get(accountId);
3114
+ let reauthExecuted = false;
2844
3115
  if (page) {
2845
3116
  const executeReauth = async () => {
2846
3117
  console.warn(
@@ -2857,6 +3128,7 @@ async function refreshHeadersInternal(
2857
3128
  `Re-login for ${accountId} did not restore an authenticated session`,
2858
3129
  );
2859
3130
  }
3131
+ reauthExecuted = true;
2860
3132
  } else {
2861
3133
  unmarkAccountHeadersReady(accountId);
2862
3134
  throw new Error(
@@ -2891,6 +3163,37 @@ async function refreshHeadersInternal(
2891
3163
  await executeReauth();
2892
3164
  }
2893
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
+ }
2894
3197
  }
2895
3198
 
2896
3199
  await captureQwenHeaders(accountId, undefined, boundedTimeoutMs);