qwenproxy-cli 1.4.3 → 1.4.8

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) {
@@ -1981,6 +2005,8 @@ export interface LoginAttemptResult {
1981
2005
  success: boolean;
1982
2006
  permanentFailure?: boolean;
1983
2007
  reason?: string;
2008
+ token?: string;
2009
+ refreshToken?: string;
1984
2010
  }
1985
2011
 
1986
2012
  export function classifyQwenAuthError(
@@ -2096,6 +2122,7 @@ async function loginToQwen(
2096
2122
  secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
2097
2123
  version: cache.headers["version"] || undefined,
2098
2124
  tokenExpiresAt: exp || undefined,
2125
+ refreshToken: apiResult.refreshToken,
2099
2126
  capturedAt: Date.now(),
2100
2127
  });
2101
2128
  }
@@ -2131,6 +2158,12 @@ async function loginToQwen(
2131
2158
  markAccountHeadersReady(accountId);
2132
2159
  const { saveAuthSession } = await import("../core/database.ts");
2133
2160
  const exp = parseJwtExpiry(tokenCookie.value);
2161
+ let rTok = uiResult.refreshToken;
2162
+ if (!rTok) {
2163
+ try {
2164
+ rTok = (await page.evaluate(() => localStorage.getItem("refresh_token") || "")) || undefined;
2165
+ } catch {}
2166
+ }
2134
2167
  saveAuthSession(accountId, {
2135
2168
  cookie: cookieStr,
2136
2169
  userAgent: cache.headers["user-agent"] || "",
@@ -2142,6 +2175,7 @@ async function loginToQwen(
2142
2175
  secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
2143
2176
  version: cache.headers["version"] || undefined,
2144
2177
  tokenExpiresAt: exp || undefined,
2178
+ refreshToken: rTok,
2145
2179
  capturedAt: Date.now(),
2146
2180
  });
2147
2181
  }
@@ -2206,41 +2240,65 @@ async function loginViaApi(
2206
2240
  .createHash("sha256")
2207
2241
  .update(password)
2208
2242
  .digest("hex");
2209
- const signinUrl = qwenUrl("/api/v2/auths/signin");
2210
-
2211
- let signinSuccess = false;
2212
- let data: any = null;
2213
2243
 
2214
- if (page.request && typeof page.request.post === "function") {
2215
- try {
2216
- const response = await page.request.post(signinUrl, {
2217
- data: {
2218
- email,
2219
- password: hashedPassword,
2220
- login_type: "email",
2221
- },
2222
- headers: {
2223
- "content-type": "application/json",
2224
- accept: "application/json, text/plain, */*",
2225
- referer: qwenUrl("/auth"),
2226
- origin: qwenOrigin(),
2227
- source: "web",
2228
- },
2229
- timeout: 10_000,
2230
- });
2231
- data = await response.json().catch(() => null);
2232
- signinSuccess = Boolean(data && (data.success === true || data.token));
2233
- } catch {}
2244
+ // Ensure the page is loaded at /auth so that same-site/CORS credentials and Baxia WAF headers are active
2245
+ if (!page.url().includes("/auth")) {
2246
+ await page.goto(qwenUrl("/auth"), {
2247
+ waitUntil: "domcontentloaded",
2248
+ timeout: config.timeouts.navigation,
2249
+ }).catch(() => {});
2250
+ await sleep(1000);
2234
2251
  }
2235
2252
 
2236
- if (!signinSuccess && !data) {
2237
- // Fallback to in-page evaluate fetch if page.request was unavailable
2238
- const requestId = crypto.randomUUID();
2239
- const evalRes = await page
2253
+ const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
2254
+ const requestId = crypto.randomUUID();
2255
+
2256
+ const evalRes = await page
2257
+ .evaluate(
2258
+ async ({ email, password, signinUrl, requestId }) => {
2259
+ try {
2260
+ const response = await fetch(signinUrl, {
2261
+ method: "POST",
2262
+ signal: AbortSignal.timeout(15_000),
2263
+ credentials: "include",
2264
+ headers: {
2265
+ accept: "application/json, text/plain, */*",
2266
+ "content-type": "application/json",
2267
+ source: "web",
2268
+ version: "0.3.11",
2269
+ "x-request-origin": "https://chat.qwen.ai",
2270
+ timezone: new Date().toString().split(" (")[0],
2271
+ "x-request-id": requestId,
2272
+ },
2273
+ body: JSON.stringify({ email, password }),
2274
+ });
2275
+ const json = await response.json().catch(() => null);
2276
+ return { ok: response.ok, status: response.status, data: json };
2277
+ } catch (e: any) {
2278
+ return { ok: false, error: e.message };
2279
+ }
2280
+ },
2281
+ { email, password: hashedPassword, signinUrl, requestId },
2282
+ )
2283
+ .catch(() => null);
2284
+
2285
+ let data: any = evalRes?.data;
2286
+ let signinSuccess = Boolean(
2287
+ data &&
2288
+ (data.success === true ||
2289
+ data.token ||
2290
+ data.data?.access_token ||
2291
+ data.data?.token),
2292
+ );
2293
+
2294
+ // Fallback to legacy chat.qwen.ai endpoint if auth.qwen.ai did not succeed
2295
+ if (!signinSuccess && !data?.data?.code) {
2296
+ const fallbackUrl = qwenUrl("/api/v2/auths/signin");
2297
+ const fallbackRes = await page
2240
2298
  .evaluate(
2241
- async ({ email, password, signinUrl, requestId }) => {
2299
+ async ({ email, password, fallbackUrl, requestId }) => {
2242
2300
  try {
2243
- const response = await fetch(signinUrl, {
2301
+ const response = await fetch(fallbackUrl, {
2244
2302
  method: "POST",
2245
2303
  signal: AbortSignal.timeout(10_000),
2246
2304
  credentials: "include",
@@ -2248,10 +2306,11 @@ async function loginViaApi(
2248
2306
  accept: "application/json, text/plain, */*",
2249
2307
  "content-type": "application/json",
2250
2308
  source: "web",
2309
+ version: "0.3.11",
2251
2310
  timezone: new Date().toString().split(" (")[0],
2252
2311
  "x-request-id": requestId,
2253
2312
  },
2254
- body: JSON.stringify({ email, password, login_type: "email" }),
2313
+ body: JSON.stringify({ email, password }),
2255
2314
  });
2256
2315
  const json = await response.json().catch(() => null);
2257
2316
  return { ok: response.ok, status: response.status, data: json };
@@ -2259,13 +2318,19 @@ async function loginViaApi(
2259
2318
  return { ok: false, error: e.message };
2260
2319
  }
2261
2320
  },
2262
- { email, password: hashedPassword, signinUrl, requestId },
2321
+ { email, password: hashedPassword, fallbackUrl, requestId },
2263
2322
  )
2264
2323
  .catch(() => null);
2265
2324
 
2266
- if (evalRes?.data) {
2267
- data = evalRes.data;
2268
- signinSuccess = Boolean(data && (data.success === true || data.token));
2325
+ if (fallbackRes?.data) {
2326
+ data = fallbackRes.data;
2327
+ signinSuccess = Boolean(
2328
+ data &&
2329
+ (data.success === true ||
2330
+ data.token ||
2331
+ data.data?.access_token ||
2332
+ data.data?.token),
2333
+ );
2269
2334
  }
2270
2335
  }
2271
2336
 
@@ -2281,7 +2346,14 @@ async function loginViaApi(
2281
2346
  }
2282
2347
 
2283
2348
  if (signinSuccess) {
2284
- const token = data?.data?.token || data?.token;
2349
+ const token =
2350
+ data?.data?.access_token ||
2351
+ data?.data?.token ||
2352
+ data?.access_token ||
2353
+ data?.token;
2354
+ const refreshToken =
2355
+ data?.data?.refresh_token || data?.refresh_token;
2356
+
2285
2357
  if (token) {
2286
2358
  try {
2287
2359
  await page.context().addCookies([
@@ -2308,16 +2380,21 @@ async function loginViaApi(
2308
2380
 
2309
2381
  if (token) {
2310
2382
  await page
2311
- .evaluate((tok) => {
2312
- try {
2313
- localStorage.removeItem("qwen_token_logged_out_marker");
2314
- localStorage.setItem("token", tok);
2315
- document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
2316
- } catch {}
2317
- }, token)
2383
+ .evaluate(
2384
+ ({ tok, rTok }) => {
2385
+ try {
2386
+ localStorage.removeItem("qwen_token_logged_out_marker");
2387
+ localStorage.setItem("token", tok);
2388
+ localStorage.setItem("access_token", tok);
2389
+ if (rTok) localStorage.setItem("refresh_token", rTok);
2390
+ document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
2391
+ } catch {}
2392
+ },
2393
+ { tok: token, rTok: refreshToken },
2394
+ )
2318
2395
  .catch(() => {});
2319
2396
  await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
2320
- return { success: true };
2397
+ return { success: true, token, refreshToken };
2321
2398
  }
2322
2399
  }
2323
2400
 
@@ -2346,11 +2423,28 @@ async function loginViaUi(
2346
2423
  password: string,
2347
2424
  ): Promise<LoginAttemptResult> {
2348
2425
  try {
2349
- await page.goto(qwenUrl("/auth"), {
2350
- waitUntil: "domcontentloaded",
2351
- timeout: config.timeouts.navigation,
2352
- });
2353
- await sleep(1500);
2426
+ // 0. If the page is ALREADY logged in, avoid touching /auth altogether!
2427
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 2000).catch(() => false))) {
2428
+ return { success: true };
2429
+ }
2430
+
2431
+ try {
2432
+ await page.goto(qwenUrl("/auth"), {
2433
+ waitUntil: "domcontentloaded",
2434
+ timeout: Math.min(config.timeouts.navigation, 15_000),
2435
+ });
2436
+ await sleep(1000);
2437
+ } catch (gotoErr: any) {
2438
+ // If navigation was aborted because the page redirected to / (because already logged in):
2439
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000).catch(() => false))) {
2440
+ return { success: true };
2441
+ }
2442
+ }
2443
+
2444
+ // Check if redirect occurred after navigation
2445
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000).catch(() => false))) {
2446
+ return { success: true };
2447
+ }
2354
2448
 
2355
2449
  // Wait for email input
2356
2450
  const emailSelector = [
@@ -2362,7 +2456,7 @@ async function loginViaUi(
2362
2456
  ].join(", ");
2363
2457
  try {
2364
2458
  await page.waitForSelector(emailSelector, {
2365
- timeout: Math.min(config.timeouts.page, 8_000),
2459
+ timeout: Math.min(config.timeouts.page, 5_000),
2366
2460
  });
2367
2461
  } catch {
2368
2462
  if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
@@ -2379,35 +2473,77 @@ async function loginViaUi(
2379
2473
  // (supporting English, Portuguese, Spanish, Chinese) to reveal the standard email + password form.
2380
2474
  await clearVisibleChallenge(page);
2381
2475
 
2382
- const findPasswordModeButton = () => {
2383
- return page
2384
- .locator('button, [role="button"], a, div')
2476
+ const isPasswordInputPresent = async () => {
2477
+ return (await page.locator('input[type="password"], input[name="password"]').count().catch(() => 0)) > 0;
2478
+ };
2479
+
2480
+ const revealPasswordMode = async () => {
2481
+ // If already on /forget, navigate back to /auth
2482
+ if (page.url().includes("/forget")) {
2483
+ await page.goto(qwenUrl("/auth"), { waitUntil: "domcontentloaded", timeout: 10_000 }).catch(() => {});
2484
+ await sleep(1000);
2485
+ }
2486
+
2487
+ // If password input is already in DOM, do NOT click any switch buttons!
2488
+ if (await isPasswordInputPresent()) return;
2489
+
2490
+ // 1. Playwright button locator with multilingual matching (strictly excluding "Esqueci a senha" / "Forgot password")
2491
+ const btn = page
2492
+ .locator("button")
2385
2493
  .filter({
2386
2494
  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,
2387
2495
  })
2496
+ .filter({
2497
+ hasNotText: /(?:esqueci|forgot|olvid|reset|recover)/i,
2498
+ })
2388
2499
  .first();
2389
- };
2390
2500
 
2391
- try {
2392
- const pwdModeBtn = findPasswordModeButton();
2393
- if (await pwdModeBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
2394
- await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
2501
+ if (await btn.isVisible({ timeout: 1500 }).catch(() => false)) {
2502
+ await btn.click({ timeout: 2000 }).catch(() => {});
2395
2503
  await sleep(500);
2396
2504
  }
2397
- } catch {}
2398
2505
 
2399
- // Fill email
2400
- await page.fill(emailSelector, email);
2401
- await sleep(300);
2506
+ if (await isPasswordInputPresent()) return;
2402
2507
 
2403
- // If "Log in with a password" button appeared after typing email, click it
2508
+ // 2. In-page evaluate fail-safe to trigger direct DOM click on the password button
2509
+ await page
2510
+ .evaluate(() => {
2511
+ try {
2512
+ const buttons = Array.from(document.querySelectorAll("button, [role='button'], a"));
2513
+ for (const b of buttons) {
2514
+ const txt = ((b as HTMLElement).innerText || "").trim().toLowerCase();
2515
+ if (
2516
+ /(?: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) &&
2517
+ !txt.includes("esqueci") && !txt.includes("forgot") && !txt.includes("olvid") && !txt.includes("reset")
2518
+ ) {
2519
+ (b as HTMLElement).click();
2520
+ return true;
2521
+ }
2522
+ }
2523
+ } catch {}
2524
+ return false;
2525
+ })
2526
+ .catch(() => false);
2527
+ await sleep(500);
2528
+ };
2529
+
2530
+ await revealPasswordMode();
2531
+
2532
+ // Fill email
2404
2533
  try {
2405
- const pwdModeBtn = findPasswordModeButton();
2406
- if (await pwdModeBtn.isVisible({ timeout: 1500 }).catch(() => false)) {
2407
- await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
2408
- await sleep(500);
2534
+ await page.fill(emailSelector, email, { timeout: 8_000 });
2535
+ await sleep(300);
2536
+ } catch {
2537
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
2538
+ return { success: true };
2409
2539
  }
2410
- } catch {}
2540
+ throw new Error(`Campo de e-mail não encontrado na tela de autenticação`);
2541
+ }
2542
+
2543
+ // If password field is still not visible after typing email, trigger reveal again
2544
+ if (!(await isPasswordInputPresent())) {
2545
+ await revealPasswordMode();
2546
+ }
2411
2547
 
2412
2548
  // In Qwen Web, the password field is present on the same form.
2413
2549
  // NEVER press Enter after filling email alone, as Qwen interprets that
@@ -2432,21 +2568,54 @@ async function loginViaUi(
2432
2568
  }
2433
2569
 
2434
2570
  // Fill password
2435
- await page.fill(passwordSelector, password);
2436
- await sleep(500);
2571
+ try {
2572
+ await page.fill(passwordSelector, password, { timeout: 8_000 });
2573
+ await sleep(500);
2574
+ } catch {
2575
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
2576
+ return { success: true };
2577
+ }
2578
+ throw new Error(`Campo de senha não encontrado na tela de autenticação`);
2579
+ }
2580
+
2581
+ // Dispatch input/change/blur events so Ant Design / React enables the submit button
2582
+ await page
2583
+ .evaluate(() => {
2584
+ try {
2585
+ const emailEl = document.querySelector('input[name="email"], input[type="email"], input[type="text"]');
2586
+ const passEl = document.querySelector('input[type="password"], input[name="password"]');
2587
+ if (emailEl) {
2588
+ emailEl.dispatchEvent(new Event("input", { bubbles: true }));
2589
+ emailEl.dispatchEvent(new Event("change", { bubbles: true }));
2590
+ emailEl.dispatchEvent(new Event("blur", { bubbles: true }));
2591
+ }
2592
+ if (passEl) {
2593
+ passEl.dispatchEvent(new Event("input", { bubbles: true }));
2594
+ passEl.dispatchEvent(new Event("change", { bubbles: true }));
2595
+ passEl.dispatchEvent(new Event("blur", { bubbles: true }));
2596
+ }
2597
+ } catch {}
2598
+ })
2599
+ .catch(() => {});
2437
2600
 
2438
- // Prefer clicking the submit button; fall back to pressing Enter.
2439
- // The button starts disabled and only enables once both fields are filled.
2601
+ // Prefer clicking the submit button; do NOT press Enter if disabled
2440
2602
  const submitSelector =
2441
2603
  '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("登录")';
2442
2604
  const submitButton = page.locator(submitSelector).first();
2443
2605
  try {
2444
2606
  await page.waitForSelector('button[type="submit"]:not([disabled])', {
2445
- timeout: 5_000,
2607
+ timeout: 4_000,
2446
2608
  });
2447
2609
  await submitButton.click();
2448
2610
  } catch {
2449
- await page.keyboard.press("Enter");
2611
+ const isDisabled = await submitButton.isDisabled().catch(() => true);
2612
+ if (!isDisabled) {
2613
+ await submitButton.click({ force: true }).catch(() => {});
2614
+ } else {
2615
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 2000))) {
2616
+ return { success: true };
2617
+ }
2618
+ }
2450
2619
  }
2451
2620
  // Post-submit verification loop: wait up to 15s for captcha, redirect, cookies, or errors
2452
2621
  const postSubmitDeadline = Date.now() + 15_000;
@@ -2531,6 +2700,161 @@ async function loginViaUi(
2531
2700
  }
2532
2701
  }
2533
2702
 
2703
+ /**
2704
+ * Silently refresh the short-lived access token (15m) using the 30-day refresh_token.
2705
+ * Uses official in-page fetch matching Qwen Web /api/v2/auths/refresh verified in HAR.
2706
+ */
2707
+ export async function refreshAccountToken(
2708
+ accountId: string,
2709
+ ): Promise<{ success: boolean; token?: string; error?: string }> {
2710
+ const page = accountPages.get(accountId);
2711
+ if (!page || page.isClosed()) {
2712
+ return { success: false, error: "Account page not active" };
2713
+ }
2714
+
2715
+ // 1. Get refresh token from SQLite session or localStorage
2716
+ const { getValidAuthSession, saveAuthSession } = await import("../core/database.ts");
2717
+ const session = getValidAuthSession(accountId);
2718
+ let refreshToken = session?.refreshToken;
2719
+
2720
+ if (!refreshToken) {
2721
+ try {
2722
+ refreshToken =
2723
+ (await page.evaluate(() => localStorage.getItem("refresh_token") || "")) ||
2724
+ undefined;
2725
+ } catch {}
2726
+ }
2727
+
2728
+ // 2. If we have a refresh token, invoke the working refresh endpoint in-page
2729
+ if (refreshToken) {
2730
+ try {
2731
+ const refreshResult = await page.evaluate(
2732
+ async ({ rTok }) => {
2733
+ try {
2734
+ const response = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
2735
+ method: "GET",
2736
+ credentials: "include",
2737
+ headers: {
2738
+ accept: "application/json, text/plain, */*",
2739
+ source: "web",
2740
+ version: "0.3.11",
2741
+ "x-request-origin": "https://chat.qwen.ai",
2742
+ timezone: new Date().toString().split(" (")[0],
2743
+ authorization: `Bearer ${rTok}`,
2744
+ },
2745
+ });
2746
+ const json = await response.json().catch(() => null);
2747
+ return { ok: response.ok, status: response.status, data: json };
2748
+ } catch (e: any) {
2749
+ return { ok: false, error: e.message };
2750
+ }
2751
+ },
2752
+ { rTok: refreshToken },
2753
+ );
2754
+
2755
+ const json = refreshResult?.data;
2756
+ if (json && json.success === true && json.data) {
2757
+ const newAccessToken = json.data.access_token || json.data.token;
2758
+ const newRefreshToken = json.data.refresh_token || refreshToken;
2759
+
2760
+ if (newAccessToken) {
2761
+ // Update browser cookie and localStorage
2762
+ await page.context().addCookies([
2763
+ {
2764
+ name: "token",
2765
+ value: newAccessToken,
2766
+ domain: ".qwen.ai",
2767
+ path: "/",
2768
+ expires: Math.floor(Date.now() / 1000) + 31536000,
2769
+ httpOnly: false,
2770
+ secure: true,
2771
+ sameSite: "Lax",
2772
+ },
2773
+ ]);
2774
+
2775
+ await page
2776
+ .evaluate(
2777
+ ({ tok, rTok }) => {
2778
+ try {
2779
+ localStorage.removeItem("qwen_token_logged_out_marker");
2780
+ localStorage.setItem("token", tok);
2781
+ localStorage.setItem("access_token", tok);
2782
+ if (rTok) localStorage.setItem("refresh_token", rTok);
2783
+ document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
2784
+ } catch {}
2785
+ },
2786
+ { tok: newAccessToken, rTok: newRefreshToken },
2787
+ )
2788
+ .catch(() => {});
2789
+
2790
+ // Update memory cache
2791
+ const liveCookies = await page.context().cookies();
2792
+ const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
2793
+ const cache = getHeaderCache(accountId);
2794
+ cache.headers.cookie = cookieStr;
2795
+ cache.lastRefresh = Date.now();
2796
+ markAccountHeadersReady(accountId);
2797
+
2798
+ // Update SQLite session with new tokens
2799
+ saveAuthSession(accountId, {
2800
+ cookie: cookieStr,
2801
+ userAgent: cache.headers["user-agent"] || session?.userAgent || "",
2802
+ bxV: cache.headers["bx-v"] || session?.bxV || "2.5.37",
2803
+ bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
2804
+ bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
2805
+ secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
2806
+ secChUaMobile: cache.headers["sec-ch-ua-mobile"] || session?.secChUaMobile,
2807
+ secChUaPlatform: cache.headers["sec-ch-ua-platform"] || session?.secChUaPlatform,
2808
+ version: cache.headers["version"] || session?.version,
2809
+ userId: session?.userId,
2810
+ tokenExpiresAt: parseJwtExpiry(newAccessToken) || undefined,
2811
+ refreshToken: newRefreshToken,
2812
+ capturedAt: Date.now(),
2813
+ });
2814
+
2815
+ return { success: true, token: newAccessToken };
2816
+ }
2817
+ }
2818
+ } catch {}
2819
+ }
2820
+
2821
+ // 3. Fallback: if refresh_token was invalid/absent or refresh failed, do clean API login
2822
+ const { getAccountCredentials } = await import("../core/accounts.ts");
2823
+ const creds = getAccountCredentials(accountId);
2824
+ if (creds && creds.email && creds.password) {
2825
+ const apiResult = await loginViaApi(page, creds.email, creds.password);
2826
+ if (apiResult.success) {
2827
+ const liveCookies = await page.context().cookies();
2828
+ const tokenCookie = liveCookies.find((c) => c.name === "token");
2829
+ if (tokenCookie) {
2830
+ const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
2831
+ const cache = getHeaderCache(accountId);
2832
+ cache.headers.cookie = cookieStr;
2833
+ cache.lastRefresh = Date.now();
2834
+ markAccountHeadersReady(accountId);
2835
+ saveAuthSession(accountId, {
2836
+ cookie: cookieStr,
2837
+ userAgent: cache.headers["user-agent"] || session?.userAgent || "",
2838
+ bxV: cache.headers["bx-v"] || session?.bxV || "2.5.37",
2839
+ bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
2840
+ bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
2841
+ secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
2842
+ secChUaMobile: cache.headers["sec-ch-ua-mobile"] || session?.secChUaMobile,
2843
+ secChUaPlatform: cache.headers["sec-ch-ua-platform"] || session?.secChUaPlatform,
2844
+ version: cache.headers["version"] || session?.version,
2845
+ userId: session?.userId,
2846
+ tokenExpiresAt: parseJwtExpiry(tokenCookie.value) || undefined,
2847
+ refreshToken: apiResult.refreshToken,
2848
+ capturedAt: Date.now(),
2849
+ });
2850
+ return { success: true, token: tokenCookie.value };
2851
+ }
2852
+ }
2853
+ }
2854
+
2855
+ return { success: false, error: "Could not refresh or re-authenticate token" };
2856
+ }
2857
+
2534
2858
  // ─── Header Capture ───────────────────────────────────────────────────────────
2535
2859
 
2536
2860
  /**
@@ -3251,6 +3575,11 @@ async function refreshHeadersInternal(
3251
3575
  };
3252
3576
 
3253
3577
  if (forceReauth) {
3578
+ // Try silent token refresh first using the 30-day refresh_token or direct API signin!
3579
+ const refreshed = await refreshAccountToken(accountId);
3580
+ if (refreshed.success) {
3581
+ return;
3582
+ }
3254
3583
  await executeReauth();
3255
3584
  } else {
3256
3585
  try {
@@ -3848,21 +4177,17 @@ export async function keepAlivePlaywrightAccount(
3848
4177
  const now = Date.now();
3849
4178
  const currentUrl = page.url();
3850
4179
 
3851
- // Proactive session check: if token expires within 45 minutes, refresh proactively
4180
+ // Proactive session check: if token expires within 5 minutes, refresh in-page silently using refreshAccountToken
3852
4181
  const cookie = await getCookies(accountId);
3853
- if (cookie && isTokenExpiringSoon(cookie, 45)) {
4182
+ if (cookie && isTokenExpiringSoon(cookie, 5)) {
3854
4183
  console.log(
3855
- `💓 [SessionKeeper] Account ${accountId} token expires within 45m; proactively renewing session...`,
4184
+ `💓 [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`,
3856
4185
  );
3857
- try {
3858
- await refreshHeadersInternal(accountId, config.timeouts.headers, true);
4186
+ const res = await refreshAccountToken(accountId);
4187
+ if (res.success) {
3859
4188
  lastKeepAliveNavigation.set(accountId, now);
3860
4189
  touchAccountActivity(accountId);
3861
4190
  return true;
3862
- } catch (err) {
3863
- console.warn(
3864
- `[SessionKeeper] Proactive session renewal failed for ${accountId}: ${getErrorMessage(err)}`,
3865
- );
3866
4191
  }
3867
4192
  }
3868
4193
 
@@ -77,6 +77,15 @@ async function createQwenChatSession(
77
77
  rawLower.includes("unauthorized") ||
78
78
  rawLower.includes("permission to access")
79
79
  ) {
80
+ if (accountId) {
81
+ try {
82
+ const { refreshAccountToken } = await import("./playwright.ts");
83
+ const refreshed = await refreshAccountToken(accountId);
84
+ if (refreshed.success) {
85
+ return createQwenChatSession(headers, model, accountId, chatMode);
86
+ }
87
+ } catch {}
88
+ }
80
89
  throw new QwenUpstreamError(
81
90
  `Qwen create chat unauthorized: ${raw.substring(0, 300)}`,
82
91
  "Unauthorized",