qwenproxy-cli 1.4.2 → 1.4.7

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.
@@ -995,8 +995,29 @@ export function getStealthScript(profile: FingerprintProfile): string {
995
995
  const _toBlob = HTMLCanvasElement.prototype.toBlob;
996
996
  const _getImageData = CanvasRenderingContext2D.prototype.getImageData;
997
997
 
998
+ function isCaptchaCanvas(canvas) {
999
+ try {
1000
+ if (!canvas) return false;
1001
+ const id = (canvas.id || "").toLowerCase();
1002
+ const cls = (canvas.className || "").toLowerCase();
1003
+ return (
1004
+ id.includes("baxia") ||
1005
+ id.includes("nc_") ||
1006
+ id.includes("captcha") ||
1007
+ id.includes("punish") ||
1008
+ cls.includes("baxia") ||
1009
+ cls.includes("nc_") ||
1010
+ cls.includes("captcha") ||
1011
+ Boolean(canvas.closest?.("#baxia-dialog, #baxia-punish, [class*='baxia'], [class*='nc_'], .auth-layout"))
1012
+ );
1013
+ } catch(e) {
1014
+ return false;
1015
+ }
1016
+ }
1017
+
998
1018
  function addNoise(canvas) {
999
1019
  try {
1020
+ if (isCaptchaCanvas(canvas)) return;
1000
1021
  const ctx = canvas.getContext('2d');
1001
1022
  if (!ctx) return;
1002
1023
  const style = ctx.fillStyle;
@@ -1020,6 +1041,9 @@ export function getStealthScript(profile: FingerprintProfile): string {
1020
1041
 
1021
1042
  CanvasRenderingContext2D.prototype.getImageData = function(x, y, w, h) {
1022
1043
  const imageData = _getImageData.apply(this, arguments);
1044
+ if (isCaptchaCanvas(this.canvas)) {
1045
+ return imageData;
1046
+ }
1023
1047
  const data = imageData.data;
1024
1048
  const maxPixels = Math.min(data.length / 4, 2500);
1025
1049
  for (let i = 0; i < maxPixels * 4; i += 4) {
@@ -2163,8 +2187,9 @@ async function loginToQwen(
2163
2187
 
2164
2188
  if (attempt < maxAttempts) {
2165
2189
  const backoffMs = attempt * 5_000;
2190
+ const failReason = uiResult.reason || apiResult.reason || "falha temporária";
2166
2191
  console.warn(
2167
- `⚠️ [Playwright] Login attempt ${attempt}/${maxAttempts} failed for ${maskEmail(email)} (${apiResult.reason || uiResult.reason || "falha temporária"}), retrying in ${backoffMs / 1000}s`,
2192
+ `⚠️ [Playwright] Login attempt ${attempt}/${maxAttempts} failed for ${maskEmail(email)} (${failReason}), retrying in ${backoffMs / 1000}s`,
2168
2193
  );
2169
2194
  await sleep(backoffMs);
2170
2195
  }
@@ -2205,41 +2230,65 @@ async function loginViaApi(
2205
2230
  .createHash("sha256")
2206
2231
  .update(password)
2207
2232
  .digest("hex");
2208
- const signinUrl = qwenUrl("/api/v2/auths/signin");
2209
-
2210
- let signinSuccess = false;
2211
- let data: any = null;
2212
2233
 
2213
- if (page.request && typeof page.request.post === "function") {
2214
- try {
2215
- const response = await page.request.post(signinUrl, {
2216
- data: {
2217
- email,
2218
- password: hashedPassword,
2219
- login_type: "email",
2220
- },
2221
- headers: {
2222
- "content-type": "application/json",
2223
- accept: "application/json, text/plain, */*",
2224
- referer: qwenUrl("/auth"),
2225
- origin: qwenOrigin(),
2226
- source: "web",
2227
- },
2228
- timeout: 10_000,
2229
- });
2230
- data = await response.json().catch(() => null);
2231
- signinSuccess = Boolean(data && (data.success === true || data.token));
2232
- } catch {}
2234
+ // Ensure the page is loaded at /auth so that same-site/CORS credentials and Baxia WAF headers are active
2235
+ if (!page.url().includes("/auth")) {
2236
+ await page.goto(qwenUrl("/auth"), {
2237
+ waitUntil: "domcontentloaded",
2238
+ timeout: config.timeouts.navigation,
2239
+ }).catch(() => {});
2240
+ await sleep(1000);
2233
2241
  }
2234
2242
 
2235
- if (!signinSuccess && !data) {
2236
- // Fallback to in-page evaluate fetch if page.request was unavailable
2237
- const requestId = crypto.randomUUID();
2238
- const evalRes = await page
2243
+ const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
2244
+ const requestId = crypto.randomUUID();
2245
+
2246
+ const evalRes = await page
2247
+ .evaluate(
2248
+ async ({ email, password, signinUrl, requestId }) => {
2249
+ try {
2250
+ const response = await fetch(signinUrl, {
2251
+ method: "POST",
2252
+ signal: AbortSignal.timeout(15_000),
2253
+ credentials: "include",
2254
+ headers: {
2255
+ accept: "application/json, text/plain, */*",
2256
+ "content-type": "application/json",
2257
+ source: "web",
2258
+ version: "0.3.11",
2259
+ "x-request-origin": "https://chat.qwen.ai",
2260
+ timezone: new Date().toString().split(" (")[0],
2261
+ "x-request-id": requestId,
2262
+ },
2263
+ body: JSON.stringify({ email, password }),
2264
+ });
2265
+ const json = await response.json().catch(() => null);
2266
+ return { ok: response.ok, status: response.status, data: json };
2267
+ } catch (e: any) {
2268
+ return { ok: false, error: e.message };
2269
+ }
2270
+ },
2271
+ { email, password: hashedPassword, signinUrl, requestId },
2272
+ )
2273
+ .catch(() => null);
2274
+
2275
+ let data: any = evalRes?.data;
2276
+ let signinSuccess = Boolean(
2277
+ data &&
2278
+ (data.success === true ||
2279
+ data.token ||
2280
+ data.data?.access_token ||
2281
+ data.data?.token),
2282
+ );
2283
+
2284
+ // Fallback to legacy chat.qwen.ai endpoint if auth.qwen.ai did not succeed
2285
+ if (!signinSuccess && !data?.data?.code) {
2286
+ const fallbackUrl = qwenUrl("/api/v2/auths/signin");
2287
+ const fallbackRes = await page
2239
2288
  .evaluate(
2240
- async ({ email, password, signinUrl, requestId }) => {
2289
+ async ({ email, password, fallbackUrl, requestId }) => {
2241
2290
  try {
2242
- const response = await fetch(signinUrl, {
2291
+ const response = await fetch(fallbackUrl, {
2243
2292
  method: "POST",
2244
2293
  signal: AbortSignal.timeout(10_000),
2245
2294
  credentials: "include",
@@ -2247,10 +2296,11 @@ async function loginViaApi(
2247
2296
  accept: "application/json, text/plain, */*",
2248
2297
  "content-type": "application/json",
2249
2298
  source: "web",
2299
+ version: "0.3.11",
2250
2300
  timezone: new Date().toString().split(" (")[0],
2251
2301
  "x-request-id": requestId,
2252
2302
  },
2253
- body: JSON.stringify({ email, password, login_type: "email" }),
2303
+ body: JSON.stringify({ email, password }),
2254
2304
  });
2255
2305
  const json = await response.json().catch(() => null);
2256
2306
  return { ok: response.ok, status: response.status, data: json };
@@ -2258,13 +2308,19 @@ async function loginViaApi(
2258
2308
  return { ok: false, error: e.message };
2259
2309
  }
2260
2310
  },
2261
- { email, password: hashedPassword, signinUrl, requestId },
2311
+ { email, password: hashedPassword, fallbackUrl, requestId },
2262
2312
  )
2263
2313
  .catch(() => null);
2264
2314
 
2265
- if (evalRes?.data) {
2266
- data = evalRes.data;
2267
- signinSuccess = Boolean(data && (data.success === true || data.token));
2315
+ if (fallbackRes?.data) {
2316
+ data = fallbackRes.data;
2317
+ signinSuccess = Boolean(
2318
+ data &&
2319
+ (data.success === true ||
2320
+ data.token ||
2321
+ data.data?.access_token ||
2322
+ data.data?.token),
2323
+ );
2268
2324
  }
2269
2325
  }
2270
2326
 
@@ -2280,7 +2336,14 @@ async function loginViaApi(
2280
2336
  }
2281
2337
 
2282
2338
  if (signinSuccess) {
2283
- const token = data?.data?.token || data?.token;
2339
+ const token =
2340
+ data?.data?.access_token ||
2341
+ data?.data?.token ||
2342
+ data?.access_token ||
2343
+ data?.token;
2344
+ const refreshToken =
2345
+ data?.data?.refresh_token || data?.refresh_token;
2346
+
2284
2347
  if (token) {
2285
2348
  try {
2286
2349
  await page.context().addCookies([
@@ -2307,13 +2370,18 @@ async function loginViaApi(
2307
2370
 
2308
2371
  if (token) {
2309
2372
  await page
2310
- .evaluate((tok) => {
2311
- try {
2312
- localStorage.removeItem("qwen_token_logged_out_marker");
2313
- localStorage.setItem("token", tok);
2314
- document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
2315
- } catch {}
2316
- }, token)
2373
+ .evaluate(
2374
+ ({ tok, rTok }) => {
2375
+ try {
2376
+ localStorage.removeItem("qwen_token_logged_out_marker");
2377
+ localStorage.setItem("token", tok);
2378
+ localStorage.setItem("access_token", tok);
2379
+ if (rTok) localStorage.setItem("refresh_token", rTok);
2380
+ document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
2381
+ } catch {}
2382
+ },
2383
+ { tok: token, rTok: refreshToken },
2384
+ )
2317
2385
  .catch(() => {});
2318
2386
  await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
2319
2387
  return { success: true };
@@ -2345,11 +2413,28 @@ async function loginViaUi(
2345
2413
  password: string,
2346
2414
  ): Promise<LoginAttemptResult> {
2347
2415
  try {
2348
- await page.goto(qwenUrl("/auth"), {
2349
- waitUntil: "domcontentloaded",
2350
- timeout: config.timeouts.navigation,
2351
- });
2352
- await sleep(1500);
2416
+ // 0. If the page is ALREADY logged in, avoid touching /auth altogether!
2417
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 2000).catch(() => false))) {
2418
+ return { success: true };
2419
+ }
2420
+
2421
+ try {
2422
+ await page.goto(qwenUrl("/auth"), {
2423
+ waitUntil: "domcontentloaded",
2424
+ timeout: Math.min(config.timeouts.navigation, 15_000),
2425
+ });
2426
+ await sleep(1000);
2427
+ } catch (gotoErr: any) {
2428
+ // If navigation was aborted because the page redirected to / (because already logged in):
2429
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000).catch(() => false))) {
2430
+ return { success: true };
2431
+ }
2432
+ }
2433
+
2434
+ // Check if redirect occurred after navigation
2435
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000).catch(() => false))) {
2436
+ return { success: true };
2437
+ }
2353
2438
 
2354
2439
  // Wait for email input
2355
2440
  const emailSelector = [
@@ -2361,7 +2446,7 @@ async function loginViaUi(
2361
2446
  ].join(", ");
2362
2447
  try {
2363
2448
  await page.waitForSelector(emailSelector, {
2364
- timeout: Math.min(config.timeouts.page, 8_000),
2449
+ timeout: Math.min(config.timeouts.page, 5_000),
2365
2450
  });
2366
2451
  } catch {
2367
2452
  if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
@@ -2378,35 +2463,77 @@ async function loginViaUi(
2378
2463
  // (supporting English, Portuguese, Spanish, Chinese) to reveal the standard email + password form.
2379
2464
  await clearVisibleChallenge(page);
2380
2465
 
2381
- const findPasswordModeButton = () => {
2382
- return page
2383
- .locator('button, [role="button"], a, div')
2466
+ const isPasswordInputPresent = async () => {
2467
+ return (await page.locator('input[type="password"], input[name="password"]').count().catch(() => 0)) > 0;
2468
+ };
2469
+
2470
+ const revealPasswordMode = async () => {
2471
+ // If already on /forget, navigate back to /auth
2472
+ if (page.url().includes("/forget")) {
2473
+ await page.goto(qwenUrl("/auth"), { waitUntil: "domcontentloaded", timeout: 10_000 }).catch(() => {});
2474
+ await sleep(1000);
2475
+ }
2476
+
2477
+ // If password input is already in DOM, do NOT click any switch buttons!
2478
+ if (await isPasswordInputPresent()) return;
2479
+
2480
+ // 1. Playwright button locator with multilingual matching (strictly excluding "Esqueci a senha" / "Forgot password")
2481
+ const btn = page
2482
+ .locator("button")
2384
2483
  .filter({
2385
2484
  hasText: /(?:log\s*in\s*with\s*(?:a\s*)?password|fazer\s*login\s*com\s*senha|entrar\s*com\s*(?:uma\s*)?senha|iniciar\s*sesi[oó]n\s*con\s*contrase[nñ]a|密码登录)/i,
2386
2485
  })
2486
+ .filter({
2487
+ hasNotText: /(?:esqueci|forgot|olvid|reset|recover)/i,
2488
+ })
2387
2489
  .first();
2388
- };
2389
2490
 
2390
- try {
2391
- const pwdModeBtn = findPasswordModeButton();
2392
- if (await pwdModeBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
2393
- await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
2491
+ if (await btn.isVisible({ timeout: 1500 }).catch(() => false)) {
2492
+ await btn.click({ timeout: 2000 }).catch(() => {});
2394
2493
  await sleep(500);
2395
2494
  }
2396
- } catch {}
2397
2495
 
2398
- // Fill email
2399
- await page.fill(emailSelector, email);
2400
- await sleep(300);
2496
+ if (await isPasswordInputPresent()) return;
2401
2497
 
2402
- // If "Log in with a password" button appeared after typing email, click it
2498
+ // 2. In-page evaluate fail-safe to trigger direct DOM click on the password button
2499
+ await page
2500
+ .evaluate(() => {
2501
+ try {
2502
+ const buttons = Array.from(document.querySelectorAll("button, [role='button'], a"));
2503
+ for (const b of buttons) {
2504
+ const txt = ((b as HTMLElement).innerText || "").trim().toLowerCase();
2505
+ if (
2506
+ /(?:log\s*in\s*with\s*(?:a\s*)?password|fazer\s*login\s*com\s*senha|entrar\s*com\s*(?:uma\s*)?senha|iniciar\s*sesi[oó]n\s*con\s*contrase[nñ]a|密码登录)/i.test(txt) &&
2507
+ !txt.includes("esqueci") && !txt.includes("forgot") && !txt.includes("olvid") && !txt.includes("reset")
2508
+ ) {
2509
+ (b as HTMLElement).click();
2510
+ return true;
2511
+ }
2512
+ }
2513
+ } catch {}
2514
+ return false;
2515
+ })
2516
+ .catch(() => false);
2517
+ await sleep(500);
2518
+ };
2519
+
2520
+ await revealPasswordMode();
2521
+
2522
+ // Fill email
2403
2523
  try {
2404
- const pwdModeBtn = findPasswordModeButton();
2405
- if (await pwdModeBtn.isVisible({ timeout: 1500 }).catch(() => false)) {
2406
- await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
2407
- await sleep(500);
2524
+ await page.fill(emailSelector, email, { timeout: 8_000 });
2525
+ await sleep(300);
2526
+ } catch {
2527
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
2528
+ return { success: true };
2408
2529
  }
2409
- } catch {}
2530
+ throw new Error(`Campo de e-mail não encontrado na tela de autenticação`);
2531
+ }
2532
+
2533
+ // If password field is still not visible after typing email, trigger reveal again
2534
+ if (!(await isPasswordInputPresent())) {
2535
+ await revealPasswordMode();
2536
+ }
2410
2537
 
2411
2538
  // In Qwen Web, the password field is present on the same form.
2412
2539
  // NEVER press Enter after filling email alone, as Qwen interprets that
@@ -2431,74 +2558,125 @@ async function loginViaUi(
2431
2558
  }
2432
2559
 
2433
2560
  // Fill password
2434
- await page.fill(passwordSelector, password);
2435
- await sleep(500);
2561
+ try {
2562
+ await page.fill(passwordSelector, password, { timeout: 8_000 });
2563
+ await sleep(500);
2564
+ } catch {
2565
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
2566
+ return { success: true };
2567
+ }
2568
+ throw new Error(`Campo de senha não encontrado na tela de autenticação`);
2569
+ }
2570
+
2571
+ // Dispatch input/change/blur events so Ant Design / React enables the submit button
2572
+ await page
2573
+ .evaluate(() => {
2574
+ try {
2575
+ const emailEl = document.querySelector('input[name="email"], input[type="email"], input[type="text"]');
2576
+ const passEl = document.querySelector('input[type="password"], input[name="password"]');
2577
+ if (emailEl) {
2578
+ emailEl.dispatchEvent(new Event("input", { bubbles: true }));
2579
+ emailEl.dispatchEvent(new Event("change", { bubbles: true }));
2580
+ emailEl.dispatchEvent(new Event("blur", { bubbles: true }));
2581
+ }
2582
+ if (passEl) {
2583
+ passEl.dispatchEvent(new Event("input", { bubbles: true }));
2584
+ passEl.dispatchEvent(new Event("change", { bubbles: true }));
2585
+ passEl.dispatchEvent(new Event("blur", { bubbles: true }));
2586
+ }
2587
+ } catch {}
2588
+ })
2589
+ .catch(() => {});
2436
2590
 
2437
- // Prefer clicking the submit button; fall back to pressing Enter.
2438
- // The button starts disabled and only enables once both fields are filled.
2591
+ // Prefer clicking the submit button; do NOT press Enter if disabled
2439
2592
  const submitSelector =
2440
2593
  'button.qwenchat-auth-pc-submit-button, button[type="submit"], button:has-text("Log in"), button:has-text("Sign in"), button:has-text("Fazer login"), button:has-text("Entrar"), button:has-text("Iniciar sesión"), button:has-text("登录")';
2441
2594
  const submitButton = page.locator(submitSelector).first();
2442
2595
  try {
2443
2596
  await page.waitForSelector('button[type="submit"]:not([disabled])', {
2444
- timeout: 5_000,
2597
+ timeout: 4_000,
2445
2598
  });
2446
2599
  await submitButton.click();
2447
2600
  } catch {
2448
- await page.keyboard.press("Enter");
2601
+ const isDisabled = await submitButton.isDisabled().catch(() => true);
2602
+ if (!isDisabled) {
2603
+ await submitButton.click({ force: true }).catch(() => {});
2604
+ } else {
2605
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 2000))) {
2606
+ return { success: true };
2607
+ }
2608
+ }
2449
2609
  }
2450
- await sleep(3000);
2451
-
2452
- // If a slider puzzle captcha appeared after submit, solve it automatically
2453
- await solveBaxiaCaptcha(page, {
2454
- waitForMs: 2000,
2455
- maxAttempts: config.captcha.maxAttempts,
2456
- retryDelayMs: config.captcha.retryDelayMs,
2457
- }).catch(() => {});
2610
+ // Post-submit verification loop: wait up to 15s for captcha, redirect, cookies, or errors
2611
+ const postSubmitDeadline = Date.now() + 15_000;
2612
+ while (Date.now() < postSubmitDeadline) {
2613
+ if (page.isClosed()) break;
2614
+
2615
+ // If a slider puzzle captcha appeared after submit, solve it automatically
2616
+ if (config.captcha.enabled) {
2617
+ await solveBaxiaCaptcha(page, {
2618
+ waitForMs: 1500,
2619
+ maxAttempts: config.captcha.maxAttempts,
2620
+ retryDelayMs: config.captcha.retryDelayMs,
2621
+ }).catch(() => {});
2622
+ }
2458
2623
 
2459
- // Check if an OTP code verification screen appeared (e.g. 2FA / email verification code)
2460
- const codeSelector =
2461
- 'input[placeholder*="code" i], input[placeholder*="código" i], input[name*="code" i]';
2462
- const codeInput = page.locator(codeSelector).first();
2463
- if (await codeInput.isVisible().catch(() => false)) {
2464
- return {
2465
- success: false,
2466
- reason: "Conta requer código de verificação enviado por e-mail (2FA/OTP)",
2467
- };
2468
- }
2624
+ // Check for UI error elements in DOM (Ant Design errors, alerts, toasts)
2625
+ const errorSelector = [
2626
+ ".qwen-chat-v2-toast-text",
2627
+ ".qwen-chat-v2-toast-content",
2628
+ ".ant-form-item-explain-error",
2629
+ ".ant-message-error",
2630
+ ".ant-message-notice",
2631
+ '[role="alert"]',
2632
+ ".qwenchat-auth-error",
2633
+ ".auth-error-message",
2634
+ ].join(", ");
2635
+ const errorEl = page.locator(errorSelector).first();
2636
+ if (await errorEl.isVisible().catch(() => false)) {
2637
+ const errorText = (await errorEl.innerText().catch(() => "")).trim();
2638
+ if (errorText) {
2639
+ const classified = classifyQwenAuthError(undefined, errorText);
2640
+ return {
2641
+ success: false,
2642
+ permanentFailure: classified.isPermanent,
2643
+ reason: `Formulário: ${classified.reason}`,
2644
+ };
2645
+ }
2646
+ }
2469
2647
 
2470
- // Check for UI error elements in DOM (Ant Design errors, alerts, toasts)
2471
- const errorSelector = [
2472
- ".qwen-chat-v2-toast-text",
2473
- ".qwen-chat-v2-toast-content",
2474
- ".ant-form-item-explain-error",
2475
- ".ant-message-error",
2476
- ".ant-message-notice",
2477
- '[role="alert"]',
2478
- ".qwenchat-auth-error",
2479
- ".auth-error-message",
2480
- ].join(", ");
2481
- const errorEl = page.locator(errorSelector).first();
2482
- if (await errorEl.isVisible().catch(() => false)) {
2483
- const errorText = (await errorEl.innerText().catch(() => "")).trim();
2484
- if (errorText) {
2485
- const classified = classifyQwenAuthError(undefined, errorText);
2648
+ // Check if an OTP code verification screen appeared (e.g. 2FA / email verification code)
2649
+ const codeSelector =
2650
+ 'input[placeholder*="code" i], input[placeholder*="código" i], input[name*="code" i]';
2651
+ const codeInput = page.locator(codeSelector).first();
2652
+ if (await codeInput.isVisible().catch(() => false)) {
2486
2653
  return {
2487
2654
  success: false,
2488
- permanentFailure: classified.isPermanent,
2489
- reason: `Formulário: ${classified.reason}`,
2655
+ reason: "Conta requer código de verificação enviado por e-mail (2FA/OTP)",
2490
2656
  };
2491
2657
  }
2492
- }
2493
2658
 
2494
- // Check if login was successful
2495
- const isLoggedIn = await isPageLoggedIn(page);
2659
+ // Check if auth cookie 'token' appeared
2660
+ try {
2661
+ const liveCookies = await page.context().cookies();
2662
+ if (liveCookies.some((c) => c.name === "token")) {
2663
+ return { success: true };
2664
+ }
2665
+ } catch {}
2496
2666
 
2497
- if (isLoggedIn) {
2498
- await page.goto(qwenUrl("/"), {
2499
- waitUntil: "domcontentloaded",
2500
- timeout: config.timeouts.navigation,
2501
- });
2667
+ // Check if page redirected away from /auth and is logged in
2668
+ const currentUrl = page.url();
2669
+ if (!currentUrl.includes("/auth") && !currentUrl.includes("/login")) {
2670
+ if (await isPageLoggedIn(page, 3000)) {
2671
+ return { success: true };
2672
+ }
2673
+ }
2674
+
2675
+ await sleep(1000);
2676
+ }
2677
+
2678
+ // Final check
2679
+ if (await isPageLoggedIn(page, 3000)) {
2502
2680
  return { success: true };
2503
2681
  }
2504
2682
 
@@ -3232,6 +3410,18 @@ async function refreshHeadersInternal(
3232
3410
  };
3233
3411
 
3234
3412
  if (forceReauth) {
3413
+ // If the page is already logged in, do NOT execute destructive password re-login!
3414
+ const alreadyIn = await isPageLoggedIn(page, 2000).catch(() => false);
3415
+ if (alreadyIn) {
3416
+ const liveCookies = await page.context().cookies();
3417
+ if (liveCookies.some((c) => c.name === "token")) {
3418
+ const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
3419
+ cache.headers.cookie = cookieStr;
3420
+ cache.lastRefresh = Date.now();
3421
+ markAccountHeadersReady(accountId);
3422
+ return;
3423
+ }
3424
+ }
3235
3425
  await executeReauth();
3236
3426
  } else {
3237
3427
  try {
@@ -3829,21 +4019,53 @@ export async function keepAlivePlaywrightAccount(
3829
4019
  const now = Date.now();
3830
4020
  const currentUrl = page.url();
3831
4021
 
3832
- // Proactive session check: if token expires within 45 minutes, refresh proactively
4022
+ // Proactive session check: if token expires within 2 minutes, refresh in-page silently
3833
4023
  const cookie = await getCookies(accountId);
3834
- if (cookie && isTokenExpiringSoon(cookie, 45)) {
3835
- console.log(
3836
- `💓 [SessionKeeper] Account ${accountId} token expires within 45m; proactively renewing session...`,
3837
- );
4024
+ if (cookie && isTokenExpiringSoon(cookie, 2)) {
3838
4025
  try {
3839
- await refreshHeadersInternal(accountId, config.timeouts.headers, true);
3840
- lastKeepAliveNavigation.set(accountId, now);
3841
- touchAccountActivity(accountId);
3842
- return true;
3843
- } catch (err) {
3844
- console.warn(
3845
- `[SessionKeeper] Proactive session renewal failed for ${accountId}: ${getErrorMessage(err)}`,
4026
+ await page
4027
+ .evaluate(async () => {
4028
+ await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
4029
+ method: "GET",
4030
+ credentials: "include",
4031
+ }).catch(() => {});
4032
+ await fetch("/api/v1/auths/", {
4033
+ method: "GET",
4034
+ credentials: "include",
4035
+ }).catch(() => {});
4036
+ })
4037
+ .catch(() => {});
4038
+
4039
+ const liveCookies = await page.context().cookies();
4040
+ const tokenCookie = liveCookies.find((c) => c.name === "token");
4041
+ if (tokenCookie) {
4042
+ const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
4043
+ const cache = getHeaderCache(accountId);
4044
+ cache.headers.cookie = cookieStr;
4045
+ cache.lastRefresh = Date.now();
4046
+ markAccountHeadersReady(accountId);
4047
+ lastKeepAliveNavigation.set(accountId, now);
4048
+ touchAccountActivity(accountId);
4049
+ return true;
4050
+ }
4051
+ } catch {}
4052
+
4053
+ // If token refresh probe didn't restore cookies and the page is genuinely logged out:
4054
+ const loggedIn = await isPageLoggedIn(page, 2000).catch(() => false);
4055
+ if (!loggedIn) {
4056
+ console.log(
4057
+ `💓 [SessionKeeper] Account ${accountId} session expired; renewing session...`,
3846
4058
  );
4059
+ try {
4060
+ await refreshHeadersInternal(accountId, config.timeouts.headers, true);
4061
+ lastKeepAliveNavigation.set(accountId, now);
4062
+ touchAccountActivity(accountId);
4063
+ return true;
4064
+ } catch (err) {
4065
+ console.warn(
4066
+ `[SessionKeeper] Proactive session renewal failed for ${accountId}: ${getErrorMessage(err)}`,
4067
+ );
4068
+ }
3847
4069
  }
3848
4070
  }
3849
4071